From e4fa40e35b2fc5a037323788a46ad598c7533b54 Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Sat, 23 May 2026 23:54:15 -0700 Subject: [PATCH 01/14] feat(harmont-py): add hm.py.uv toolchain namespace New py.uv sub-namespace for uv-managed Python projects. Adds run(), build(), lock_check(), publish() beyond the existing test/lint/fmt/typecheck surface. hm.python() stays unchanged for backwards compat. --- dsls/harmont-py/harmont/__init__.py | 3 +- dsls/harmont-py/harmont/_unwrap.py | 5 +- dsls/harmont-py/harmont/py/__init__.py | 5 + dsls/harmont-py/harmont/py/uv.py | 171 ++++++++++++++++++++++ dsls/harmont-py/tests/test_py_uv.py | 194 +++++++++++++++++++++++++ 5 files changed, 376 insertions(+), 2 deletions(-) create mode 100644 dsls/harmont-py/harmont/py/__init__.py create mode 100644 dsls/harmont-py/harmont/py/uv.py create mode 100644 dsls/harmont-py/tests/test_py_uv.py diff --git a/dsls/harmont-py/harmont/__init__.py b/dsls/harmont-py/harmont/__init__.py index ae07945..1b1b07f 100644 --- a/dsls/harmont-py/harmont/__init__.py +++ b/dsls/harmont-py/harmont/__init__.py @@ -29,7 +29,7 @@ from typing import TYPE_CHECKING, Any -from . import _decorator, dev +from . import _decorator, dev, py from ._deploy import Deployment, deploy from ._envelope import dump_registry_json from ._step import Step, scratch, wait @@ -157,6 +157,7 @@ def sh( "on_change", "perl", "pipeline", + "py", "pipeline_to_json", "pull_request", "push", diff --git a/dsls/harmont-py/harmont/_unwrap.py b/dsls/harmont-py/harmont/_unwrap.py index c2ddb87..718bd36 100644 --- a/dsls/harmont-py/harmont/_unwrap.py +++ b/dsls/harmont-py/harmont/_unwrap.py @@ -18,6 +18,7 @@ from .elm import ElmProject from .haskell import HaskellPackage from .npm import NpmProject +from .py.uv import UvProject from .rust import RustToolchain @@ -32,12 +33,14 @@ def _one(obj: object) -> tuple[Step, ...]: return (obj.install(),) if isinstance(obj, ElmProject): return (obj.make("src/Main.elm"),) + if isinstance(obj, UvProject): + return (obj.test(),) if isinstance(obj, (tuple, list)): return as_leaves(obj) msg = ( f"hm.target: cannot use {type(obj).__name__} as a pipeline leaf\n" " → return one of: Step, tuple[Step, ...], HaskellPackage, " - "RustToolchain, NpmProject, ElmProject" + "RustToolchain, NpmProject, ElmProject, UvProject" ) raise TypeError(msg) diff --git a/dsls/harmont-py/harmont/py/__init__.py b/dsls/harmont-py/harmont/py/__init__.py new file mode 100644 index 0000000..e739792 --- /dev/null +++ b/dsls/harmont-py/harmont/py/__init__.py @@ -0,0 +1,5 @@ +"""Python toolchain namespace (``hm.py``).""" + +from .uv import UvProject, uv + +__all__ = ["UvProject", "uv"] diff --git a/dsls/harmont-py/harmont/py/uv.py b/dsls/harmont-py/harmont/py/uv.py new file mode 100644 index 0000000..a528470 --- /dev/null +++ b/dsls/harmont-py/harmont/py/uv.py @@ -0,0 +1,171 @@ +"""uv-managed Python project toolchain (``hm.py.uv``). + +Public surface lives on the module-level singleton :data:`uv`. Call +it to construct a :class:`UvProject`, or use the bare-form action +methods (``uv.test()``, ``uv.lint()``, etc.) for a one-shot leaf. + +The chain is: + + scratch -> apt-base -> uv-install -> uv-sync -> action leaves + +The ``uv-install`` step is cached forever (keyed on the uv version baked +into the command). The ``uv-sync`` step is cached on the project's +``uv.lock`` and ``pyproject.toml``. +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any + +from harmont._toolchain import make_install_chain +from harmont.cache import CacheForever, CacheOnChange + +if TYPE_CHECKING: + from harmont._step import Step + +APT_PACKAGES = ("curl", "ca-certificates", "python3", "python3-venv") + +_ACTION_KWARGS = frozenset(("cache", "env", "timeout_seconds", "label", "key")) + +_VERSION_RE = re.compile(r"^([0-9]+\.[0-9]+\.[0-9]+|latest)$") + + +def _uv_install_cmd(version: str) -> str: + pin = "" if version == "latest" else f"UV_VERSION={version} " + return ( + f"{pin}curl -LsSf https://astral.sh/uv/install.sh | sh && " + "ln -sf /root/.local/bin/uv /usr/local/bin/uv && uv --version" + ) + + +@dataclass(frozen=True) +class UvProject: + path: str + installed: Step # uv-sync Step + + def _emit(self, cmd: str, default_label: str, **kw: Any) -> Step: + if kw.get("label") is None: + kw["label"] = default_label + return self.installed.sh(cmd, **kw) + + def test(self, **kw: Any) -> Step: + return self._emit( + f"cd {self.path} && uv run pytest", ":python: test", **kw, + ) + + def lint(self, **kw: Any) -> Step: + return self._emit( + f"cd {self.path} && uv run ruff check .", ":python: lint", **kw, + ) + + def fmt(self, **kw: Any) -> Step: + return self._emit( + f"cd {self.path} && uv run ruff format --check .", + ":python: fmt", **kw, + ) + + def typecheck(self, **kw: Any) -> Step: + return self._emit( + f"cd {self.path} && uv run mypy .", ":python: typecheck", **kw, + ) + + def run(self, cmd: str, **kw: Any) -> Step: + first_word = cmd.split()[0] if cmd.split() else "run" + return self._emit( + f"cd {self.path} && uv run {cmd}", + f":python: {first_word}", **kw, + ) + + def build(self, **kw: Any) -> Step: + return self._emit( + f"cd {self.path} && uv build", ":python: build", **kw, + ) + + def lock_check(self, **kw: Any) -> Step: + return self._emit( + f"cd {self.path} && uv lock --check", ":python: lock-check", **kw, + ) + + def publish(self, **kw: Any) -> Step: + return self._emit( + f"cd {self.path} && uv publish", ":python: publish", **kw, + ) + + +def _make_uv( + *, + path: str = ".", + version: str = "latest", + image: str | None = None, + base: Step | None = None, +) -> UvProject: + if not _VERSION_RE.match(version): + msg = ( + f"py.uv: invalid version {version!r}\n" + ' → use "latest" or a pinned version like "0.4.18"' + ) + raise ValueError(msg) + uv_installed = make_install_chain( + apt_packages=APT_PACKAGES, + install_cmd=_uv_install_cmd(version), + install_cache=CacheForever(env_keys=()), + lang_tag="python", + install_tag="uv-install", + image=image, + base=base, + ) + synced = uv_installed.sh( + f"cd {path} && uv sync --all-extras", + label=":python: uv-sync", + cache=CacheOnChange(paths=(f"{path}/uv.lock", f"{path}/pyproject.toml")), + ) + return UvProject(path=path, installed=synced) + + +class _UvEntry: + def __call__( + self, + *, + path: str = ".", + version: str = "latest", + image: str | None = None, + base: Step | None = None, + ) -> UvProject: + return _make_uv(path=path, version=version, image=image, base=base) + + def test(self, **kw: Any) -> Step: + action_kw = {k: kw.pop(k) for k in list(kw) if k in _ACTION_KWARGS} + return self(**kw).test(**action_kw) + + def lint(self, **kw: Any) -> Step: + action_kw = {k: kw.pop(k) for k in list(kw) if k in _ACTION_KWARGS} + return self(**kw).lint(**action_kw) + + def fmt(self, **kw: Any) -> Step: + action_kw = {k: kw.pop(k) for k in list(kw) if k in _ACTION_KWARGS} + return self(**kw).fmt(**action_kw) + + def typecheck(self, **kw: Any) -> Step: + action_kw = {k: kw.pop(k) for k in list(kw) if k in _ACTION_KWARGS} + return self(**kw).typecheck(**action_kw) + + def run(self, cmd: str, **kw: Any) -> Step: + action_kw = {k: kw.pop(k) for k in list(kw) if k in _ACTION_KWARGS} + return self(**kw).run(cmd, **action_kw) + + def build(self, **kw: Any) -> Step: + action_kw = {k: kw.pop(k) for k in list(kw) if k in _ACTION_KWARGS} + return self(**kw).build(**action_kw) + + def lock_check(self, **kw: Any) -> Step: + action_kw = {k: kw.pop(k) for k in list(kw) if k in _ACTION_KWARGS} + return self(**kw).lock_check(**action_kw) + + def publish(self, **kw: Any) -> Step: + action_kw = {k: kw.pop(k) for k in list(kw) if k in _ACTION_KWARGS} + return self(**kw).publish(**action_kw) + + +uv = _UvEntry() diff --git a/dsls/harmont-py/tests/test_py_uv.py b/dsls/harmont-py/tests/test_py_uv.py new file mode 100644 index 0000000..cf2af6a --- /dev/null +++ b/dsls/harmont-py/tests/test_py_uv.py @@ -0,0 +1,194 @@ +"""py.uv toolchain namespace tests.""" + +from __future__ import annotations + +import pytest + +import harmont as hm +from harmont.cache import CacheOnChange + + +def _cmds(p: dict) -> list[str]: + return [n["step"]["cmd"] for n in p["graph"]["nodes"]] + + +def _step_by_substring(p: dict, needle: str) -> dict: + for n in p["graph"]["nodes"]: + if needle in (n["step"].get("cmd") or ""): + return n["step"] + msg = f"no command step containing {needle!r}" + raise AssertionError(msg) + + +# ── TestUvObjectForm ───────────────────────────────────────────── + + +class TestUvObjectForm: + def test_full_chain(self): + proj = hm.py.uv(path="svc") + p = hm.pipeline(proj.test(), default_image="ubuntu:24.04") + cmds = _cmds(p) + assert any("apt-get install" in c for c in cmds) + assert any("astral.sh/uv/install.sh" in c for c in cmds) + assert any("cd svc && uv sync" in c for c in cmds) + assert any("cd svc && uv run pytest" in c for c in cmds) + + def test_shared_install(self): + proj = hm.py.uv(path="svc") + p = hm.pipeline( + proj.test(), proj.lint(), proj.fmt(), proj.typecheck(), + default_image="ubuntu:24.04", + ) + cmds = _cmds(p) + assert len([c for c in cmds if "astral.sh/uv/install.sh" in c]) == 1 + assert len([c for c in cmds if "apt-get install" in c]) == 1 + assert any("uv run pytest" in c for c in cmds) + assert any("uv run ruff check" in c for c in cmds) + assert any("uv run ruff format --check" in c for c in cmds) + assert any("uv run mypy" in c for c in cmds) + + def test_sync_cached_on_change(self): + proj = hm.py.uv(path="svc") + p = hm.pipeline(proj.test()) + sync = _step_by_substring(p, "uv sync") + assert sync["cache"]["policy"] == "on_change" + assert "svc/uv.lock" in sync["cache"]["paths"] + assert "svc/pyproject.toml" in sync["cache"]["paths"] + + def test_install_cache_forever(self): + proj = hm.py.uv(path=".") + p = hm.pipeline(proj.test()) + install = _step_by_substring(p, "astral.sh/uv/install.sh") + assert install["cache"]["policy"] == "forever" + + +# ── TestUvActions ──────────────────────────────────────────────── + + +class TestUvActions: + def test_labels_auto_generated(self): + proj = hm.py.uv(path=".") + assert proj.test().label == ":python: test" + assert proj.lint().label == ":python: lint" + assert proj.fmt().label == ":python: fmt" + assert proj.typecheck().label == ":python: typecheck" + assert proj.build().label == ":python: build" + assert proj.lock_check().label == ":python: lock-check" + assert proj.publish().label == ":python: publish" + + def test_label_override(self): + proj = hm.py.uv(path=".") + assert proj.test(label=":python: smoke").label == ":python: smoke" + + def test_cache_forwarded(self): + proj = hm.py.uv(path=".") + s = proj.test(cache=CacheOnChange(paths=("pyproject.toml",))) + assert s.cache == CacheOnChange(paths=("pyproject.toml",)) + + def test_run_command(self): + proj = hm.py.uv(path="svc") + p = hm.pipeline(proj.run("flask run --port 8080")) + cmds = _cmds(p) + assert any("cd svc && uv run flask run --port 8080" in c for c in cmds) + + def test_run_auto_label_uses_first_word(self): + proj = hm.py.uv(path=".") + assert proj.run("flask run --port 8080").label == ":python: flask" + + def test_build_command(self): + proj = hm.py.uv(path="svc") + p = hm.pipeline(proj.build()) + cmds = _cmds(p) + assert any("cd svc && uv build" in c for c in cmds) + + def test_lock_check_command(self): + proj = hm.py.uv(path="svc") + p = hm.pipeline(proj.lock_check()) + cmds = _cmds(p) + assert any("cd svc && uv lock --check" in c for c in cmds) + + def test_publish_command(self): + proj = hm.py.uv(path="svc") + p = hm.pipeline(proj.publish()) + cmds = _cmds(p) + assert any("cd svc && uv publish" in c for c in cmds) + + +# ── TestUvChainSetup ──────────────────────────────────────────── + + +class TestUvChainSetup: + def test_image_emitted_on_apt_step(self): + proj = hm.py.uv(path=".", image="ubuntu:24.04") + p = hm.pipeline(proj.test()) + apt = _step_by_substring(p, "apt-get install") + assert apt.get("image") == "ubuntu:24.04" + + def test_base_skips_apt(self): + base = hm.scratch().sh("custom base", label="base") + proj = hm.py.uv(path="svc", base=base) + p = hm.pipeline(proj.test(), default_image="ubuntu:24.04") + cmds = _cmds(p) + assert not any("apt-get install" in c for c in cmds) + assert any("custom base" in c for c in cmds) + assert any("astral.sh/uv/install.sh" in c for c in cmds) + + def test_installed_escape_hatch(self): + proj = hm.py.uv(path="svc") + custom = proj.installed.sh( + "cd svc && uv run python -m mytool", + label=":python: custom", + ) + p = hm.pipeline(custom) + cmds = _cmds(p) + assert any("mytool" in c for c in cmds) + + +# ── TestUvVersionValidation ───────────────────────────────────── + + +class TestUvVersionValidation: + def test_pinned_version(self): + proj = hm.py.uv(path=".", version="0.4.18") + p = hm.pipeline(proj.test()) + install = _step_by_substring(p, "astral.sh/uv/install.sh") + assert "UV_VERSION=0.4.18" in install["cmd"] + + def test_invalid_version_rejected(self): + with pytest.raises(ValueError, match="invalid version"): + hm.py.uv(version="not a valid; version") + + +# ── TestUvBareForm ─────────────────────────────────────────────── + + +class TestUvBareForm: + def test_bare_test(self): + p = hm.pipeline(hm.py.uv.test()) + cmds = _cmds(p) + assert any("cd . && uv run pytest" in c for c in cmds) + + def test_bare_lint(self): + p = hm.pipeline(hm.py.uv.lint()) + cmds = _cmds(p) + assert any("cd . && uv run ruff check" in c for c in cmds) + + def test_bare_fmt(self): + p = hm.pipeline(hm.py.uv.fmt()) + cmds = _cmds(p) + assert any("cd . && uv run ruff format --check" in c for c in cmds) + + def test_bare_typecheck(self): + p = hm.pipeline(hm.py.uv.typecheck()) + cmds = _cmds(p) + assert any("cd . && uv run mypy" in c for c in cmds) + + def test_bare_run(self): + p = hm.pipeline(hm.py.uv.run("serve")) + cmds = _cmds(p) + assert any("cd . && uv run serve" in c for c in cmds) + + def test_bare_build(self): + p = hm.pipeline(hm.py.uv.build()) + cmds = _cmds(p) + assert any("cd . && uv build" in c for c in cmds) From edd525e73d5d17b9601074d4286c05c5ea3c3e6b Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Sat, 23 May 2026 23:57:43 -0700 Subject: [PATCH 02/14] feat(harmont-ts): add py.uv toolchain namespace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the Python DSL's hm.py.uv() — uv-managed Python projects with run(), build(), lockCheck(), publish() methods. --- dsls/harmont-ts/src/toolchains/index.ts | 1 + dsls/harmont-ts/src/toolchains/py/index.ts | 1 + dsls/harmont-ts/src/toolchains/py/uv.ts | 127 ++++++++++++++++++ .../harmont-ts/tests/toolchains/py/uv.test.ts | 126 +++++++++++++++++ 4 files changed, 255 insertions(+) create mode 100644 dsls/harmont-ts/src/toolchains/py/index.ts create mode 100644 dsls/harmont-ts/src/toolchains/py/uv.ts create mode 100644 dsls/harmont-ts/tests/toolchains/py/uv.test.ts diff --git a/dsls/harmont-ts/src/toolchains/index.ts b/dsls/harmont-ts/src/toolchains/index.ts index 2de2c28..b24cec7 100644 --- a/dsls/harmont-ts/src/toolchains/index.ts +++ b/dsls/harmont-ts/src/toolchains/index.ts @@ -26,3 +26,4 @@ export { HaskellPackage, type HaskellOptions, } from "./haskell.js"; +export * as py from "./py/index.js"; diff --git a/dsls/harmont-ts/src/toolchains/py/index.ts b/dsls/harmont-ts/src/toolchains/py/index.ts new file mode 100644 index 0000000..ee526dc --- /dev/null +++ b/dsls/harmont-ts/src/toolchains/py/index.ts @@ -0,0 +1 @@ +export { uv, UvProject, type UvOptions } from "./uv.js"; diff --git a/dsls/harmont-ts/src/toolchains/py/uv.ts b/dsls/harmont-ts/src/toolchains/py/uv.ts new file mode 100644 index 0000000..fe72496 --- /dev/null +++ b/dsls/harmont-ts/src/toolchains/py/uv.ts @@ -0,0 +1,127 @@ +import type { Step, StepOptions } from "../../step.js"; +import { forever, onChange } from "../../cache.js"; +import { makeInstallChain } from "../shared.js"; + +const APT_PACKAGES = [ + "curl", + "ca-certificates", + "python3", + "python3-venv", +] as const; +const VERSION_RE = /^([0-9]+\.[0-9]+\.[0-9]+|latest)$/; + +export interface UvOptions { + readonly path?: string; + readonly version?: string; + readonly image?: string; + readonly base?: Step; +} + +type ActionOptions = Omit; + +export class UvProject { + readonly path: string; + private readonly _installed: Step; + + constructor(path: string, installed: Step) { + this.path = path; + this._installed = installed; + } + + install(): Step { + return this._installed; + } + + test(opts?: ActionOptions): Step { + return this._installed.sh(`cd ${this.path} && uv run pytest`, { + label: ":python: test", + ...opts, + }); + } + + lint(opts?: ActionOptions): Step { + return this._installed.sh(`cd ${this.path} && uv run ruff check .`, { + label: ":python: lint", + ...opts, + }); + } + + fmt(opts?: ActionOptions): Step { + return this._installed.sh( + `cd ${this.path} && uv run ruff format --check .`, + { label: ":python: fmt", ...opts }, + ); + } + + typecheck(opts?: ActionOptions): Step { + return this._installed.sh(`cd ${this.path} && uv run mypy .`, { + label: ":python: typecheck", + ...opts, + }); + } + + run(cmd: string, opts?: ActionOptions): Step { + const firstWord = cmd.split(/\s+/)[0] ?? "run"; + return this._installed.sh(`cd ${this.path} && uv run ${cmd}`, { + label: `:python: ${firstWord}`, + ...opts, + }); + } + + build(opts?: ActionOptions): Step { + return this._installed.sh(`cd ${this.path} && uv build`, { + label: ":python: build", + ...opts, + }); + } + + lockCheck(opts?: ActionOptions): Step { + return this._installed.sh(`cd ${this.path} && uv lock --check`, { + label: ":python: lock-check", + ...opts, + }); + } + + publish(opts?: ActionOptions): Step { + return this._installed.sh(`cd ${this.path} && uv publish`, { + label: ":python: publish", + ...opts, + }); + } +} + +export function uv(opts?: UvOptions): UvProject { + const path = opts?.path ?? "."; + const version = opts?.version ?? "latest"; + + if (!VERSION_RE.test(version)) { + throw new Error( + `py.uv: invalid version "${version}"\n → use "latest" or a semver like "0.4.18"`, + ); + } + + const uvEnvPrefix = + version === "latest" ? "" : `UV_VERSION=${version} `; + const uvInstallCmd = [ + `${uvEnvPrefix}curl -LsSf https://astral.sh/uv/install.sh | sh`, + "ln -sf /root/.local/bin/uv /usr/local/bin/uv", + "uv --version", + ].join(" && "); + + const uvInstalled = makeInstallChain({ + aptPackages: [...APT_PACKAGES], + installCmd: uvInstallCmd, + installCache: forever(), + langTag: "python", + installTag: "uv-install", + image: opts?.image, + base: opts?.base, + }); + + const synced = uvInstalled.sh(`cd ${path} && uv sync --all-extras`, { + label: ":python: uv-sync", + cache: onChange(`${path}/uv.lock`, `${path}/pyproject.toml`), + }); + + return new UvProject(path, synced); +} diff --git a/dsls/harmont-ts/tests/toolchains/py/uv.test.ts b/dsls/harmont-ts/tests/toolchains/py/uv.test.ts new file mode 100644 index 0000000..ec26c1d --- /dev/null +++ b/dsls/harmont-ts/tests/toolchains/py/uv.test.ts @@ -0,0 +1,126 @@ +import { describe, expect, it } from "vitest"; +import { py } from "../../../src/toolchains/index.js"; +import { sh } from "../../../src/step.js"; +import { pipeline } from "../../../src/pipeline.js"; + +describe("py.uv factory", () => { + it("returns a UvProject with defaults", () => { + const p = py.uv(); + expect(p.path).toBe("."); + expect(p.install()._cmd).toContain("uv sync"); + }); + + it("accepts path and version", () => { + const p = py.uv({ path: "backend", version: "0.4.18" }); + expect(p.path).toBe("backend"); + expect(p.install()._parent!._cmd).toContain("UV_VERSION=0.4.18"); + }); + + it("rejects invalid version", () => { + expect(() => py.uv({ version: "abc" })).toThrow("invalid version"); + }); + + it("latest omits UV_VERSION env prefix", () => { + const p = py.uv({ version: "latest" }); + expect(p.install()._parent!._cmd).not.toContain("UV_VERSION"); + }); +}); + +describe("py.uv actions", () => { + it("test runs uv run pytest", () => { + const p = py.uv(); + expect(p.test()._cmd).toContain("uv run pytest"); + }); + + it("lint runs uv run ruff check", () => { + const p = py.uv(); + expect(p.lint()._cmd).toContain("uv run ruff check ."); + }); + + it("fmt runs uv run ruff format --check", () => { + const p = py.uv(); + expect(p.fmt()._cmd).toContain("uv run ruff format --check ."); + }); + + it("typecheck runs uv run mypy", () => { + const p = py.uv(); + expect(p.typecheck()._cmd).toContain("uv run mypy ."); + }); + + it("build runs uv build", () => { + const p = py.uv(); + expect(p.build()._cmd).toContain("uv build"); + }); + + it("lockCheck runs uv lock --check", () => { + const p = py.uv(); + expect(p.lockCheck()._cmd).toContain("uv lock --check"); + }); + + it("publish runs uv publish", () => { + const p = py.uv(); + expect(p.publish()._cmd).toContain("uv publish"); + }); + + it("run executes arbitrary command via uv run", () => { + const p = py.uv(); + expect(p.run("flask db upgrade")._cmd).toContain("uv run flask db upgrade"); + }); + + it("run auto-labels with first word of cmd", () => { + const p = py.uv(); + expect(p.run("flask db upgrade")._label).toBe(":python: flask"); + }); + + it("actions chain from install (sync step)", () => { + const p = py.uv(); + expect(p.test()._parent).toBe(p.install()); + }); + + it("default labels use :python: prefix", () => { + const p = py.uv(); + expect(p.test()._label).toBe(":python: test"); + expect(p.lint()._label).toBe(":python: lint"); + expect(p.fmt()._label).toBe(":python: fmt"); + expect(p.typecheck()._label).toBe(":python: typecheck"); + expect(p.build()._label).toBe(":python: build"); + expect(p.lockCheck()._label).toBe(":python: lock-check"); + expect(p.publish()._label).toBe(":python: publish"); + }); + + it("label override works", () => { + const p = py.uv(); + expect(p.test({ label: "custom" })._label).toBe("custom"); + }); +}); + +describe("py.uv install chain", () => { + it("chain is: scratch -> apt-base -> uv-install -> uv-sync", () => { + const p = py.uv(); + const sync = p.install(); + expect(sync._label).toBe(":python: uv-sync"); + + const uvInstall = sync._parent!; + expect(uvInstall._label).toBe(":python: uv-install"); + + const aptBase = uvInstall._parent!; + expect(aptBase._cmd).toContain("apt-get"); + }); + + it("accepts base step", () => { + const base = sh("custom"); + const p = py.uv({ base }); + const sync = p.install(); + const uvInstall = sync._parent!; + expect(uvInstall._parent).toBe(base); + }); +}); + +describe("py.uv in pipeline", () => { + it("produces valid IR", () => { + const p = py.uv(); + const ir = pipeline(p.test(), p.lint(), { defaultImage: "ubuntu:24.04" }); + expect(ir.graph.nodes.length).toBeGreaterThanOrEqual(4); + expect(ir.version).toBe("0"); + }); +}); From 467a8b44de2c5e742c8c8dc8084745187bc11121 Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Sat, 23 May 2026 23:58:29 -0700 Subject: [PATCH 03/14] =?UTF-8?q?feat:=20add=20.harmont/ci.py=20=E2=80=94?= =?UTF-8?q?=20dogfood=20CI=20pipeline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Uses hm.py.uv() for Python DSL checks and hm.rust() for workspace build/test/clippy/fmt. Demonstrates the new py.uv namespace with run() escape hatch for custom pytest args. --- .harmont/ci.py | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .harmont/ci.py diff --git a/.harmont/ci.py b/.harmont/ci.py new file mode 100644 index 0000000..5044554 --- /dev/null +++ b/.harmont/ci.py @@ -0,0 +1,46 @@ +"""Harmont CI pipeline — dogfood.""" +from __future__ import annotations + +import harmont as hm +from harmont.py.uv import UvProject +from harmont.rust import RustToolchain + + +@hm.target() +def rust_project() -> RustToolchain: + return hm.rust(path=".") + + +@hm.target() +def py_project() -> UvProject: + return hm.py.uv(path="dsls/harmont-py") + + +@hm.pipeline( + "ci", + env={"CI": "true"}, + default_image="ubuntu:24.04", + triggers=[ + hm.push(branch="main"), + hm.pull_request(branches="main"), + ], +) +def ci( + rust_project: hm.Target[RustToolchain], + py_project: hm.Target[UvProject], +) -> tuple[hm.Step, ...]: + return ( + rust_project.build(), + rust_project.test(), + rust_project.clippy(), + rust_project.fmt(), + py_project.lint(), + py_project.fmt(), + py_project.typecheck(), + py_project.run( + "pytest -v" + " --deselect tests/test_gradle.py" + " --deselect tests/test_haskell.py", + label=":python: test", + ), + ) From 75a26dc817a7b8e0d99398adfbb3850ab8b19f9b Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Sun, 24 May 2026 00:16:04 -0700 Subject: [PATCH 04/14] fix: sort __all__ + add .harmont/ci.ts pipeline Fix RUF022 (unsorted __all__) by moving "py" to correct alphabetical position. Add TypeScript variant of the dogfood CI pipeline alongside the existing Python one. --- .harmont/ci.ts | 28 ++++++++++++++++++++++++++++ dsls/harmont-py/harmont/__init__.py | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 .harmont/ci.ts diff --git a/.harmont/ci.ts b/.harmont/ci.ts new file mode 100644 index 0000000..2d510ce --- /dev/null +++ b/.harmont/ci.ts @@ -0,0 +1,28 @@ +import { pipeline, push, pullRequest, type PipelineDefinition } from "harmont"; +import { rust, py } from "harmont/toolchains"; + +const rustProject = rust({ path: "." }); +const pyProject = py.uv({ path: "dsls/harmont-py" }); + +const pipelines: PipelineDefinition[] = [ + { + slug: "ci", + triggers: [push({ branch: "main" }), pullRequest({ branches: ["main"] })], + pipeline: pipeline( + rustProject.build(), + rustProject.test(), + rustProject.clippy(), + rustProject.fmt(), + pyProject.lint(), + pyProject.fmt(), + pyProject.typecheck(), + pyProject.run( + "pytest -v --deselect tests/test_gradle.py --deselect tests/test_haskell.py", + { label: ":python: test" }, + ), + { env: { CI: "true" }, defaultImage: "ubuntu:24.04" }, + ), + }, +]; + +export default pipelines; diff --git a/dsls/harmont-py/harmont/__init__.py b/dsls/harmont-py/harmont/__init__.py index 1b1b07f..829b870 100644 --- a/dsls/harmont-py/harmont/__init__.py +++ b/dsls/harmont-py/harmont/__init__.py @@ -157,10 +157,10 @@ def sh( "on_change", "perl", "pipeline", - "py", "pipeline_to_json", "pull_request", "push", + "py", "python", "ruby", "rust", From 7e59c86b4d034c3677fae96d53d8fc6c71322699 Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Sun, 24 May 2026 00:25:33 -0700 Subject: [PATCH 05/14] ci: add dogfood job that runs hm run ci on own repo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds hm from source, installs harmont-py into system Python, and executes .harmont/ci.py through the hm binary — the repo now CI-tests itself through its own tool. --- .github/workflows/ci.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 695a091..f10ffe5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -91,6 +91,33 @@ jobs: working-directory: dsls/harmont-ts run: npm test + dogfood: + name: dogfood (hm run ci) + runs-on: ubuntu-latest + if: github.event_name == 'push' || (github.event_name == 'pull_request' && !github.event.pull_request.draft) + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-wasip1 + + - uses: Swatinem/rust-cache@v2 + + - name: Build hm + run: cargo build -p harmont-cli + + - name: Install harmont-py into system Python + run: | + sudo /usr/bin/python3 -m pip install --break-system-packages dsls/harmont-py + /usr/bin/python3 -c "import harmont; print('harmont', harmont.__file__)" + + - name: hm run ci + env: + HM_NONINTERACTIVE: '1' + run: ./target/debug/hm run ci + integration: name: docker-gated integration test runs-on: ubuntu-latest From 14314029ba3d68693bda1c4a2fdc71eebddf9b9c Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Sun, 24 May 2026 00:29:17 -0700 Subject: [PATCH 06/14] ci: run dogfood job on all PRs including drafts --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f10ffe5..de93bb1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -94,7 +94,6 @@ jobs: dogfood: name: dogfood (hm run ci) runs-on: ubuntu-latest - if: github.event_name == 'push' || (github.event_name == 'pull_request' && !github.event.pull_request.draft) timeout-minutes: 30 steps: - uses: actions/checkout@v4 From 6226136e8a59f7fd81ce4e3e1f46b749f7a7c56c Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Sun, 24 May 2026 00:32:29 -0700 Subject: [PATCH 07/14] fix: align dogfood pipeline with actual CI commands Drop fmt() (normal CI doesn't check formatting) and scope mypy to package dir only (normal CI runs mypy harmont, not mypy .). --- .harmont/ci.py | 3 +-- .harmont/ci.ts | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.harmont/ci.py b/.harmont/ci.py index 5044554..3a9da1b 100644 --- a/.harmont/ci.py +++ b/.harmont/ci.py @@ -35,8 +35,7 @@ def ci( rust_project.clippy(), rust_project.fmt(), py_project.lint(), - py_project.fmt(), - py_project.typecheck(), + py_project.run("mypy harmont", label=":python: typecheck"), py_project.run( "pytest -v" " --deselect tests/test_gradle.py" diff --git a/.harmont/ci.ts b/.harmont/ci.ts index 2d510ce..5b15430 100644 --- a/.harmont/ci.ts +++ b/.harmont/ci.ts @@ -14,8 +14,7 @@ const pipelines: PipelineDefinition[] = [ rustProject.clippy(), rustProject.fmt(), pyProject.lint(), - pyProject.fmt(), - pyProject.typecheck(), + pyProject.run("mypy harmont", { label: ":python: typecheck" }), pyProject.run( "pytest -v --deselect tests/test_gradle.py --deselect tests/test_haskell.py", { label: ":python: test" }, From 6f826872f13f9c5676757020f080177946c54eb8 Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Sun, 24 May 2026 00:37:32 -0700 Subject: [PATCH 08/14] feat: add paths param to typecheck() + ruff format harmont-py - typecheck(paths="src") or typecheck(paths=["src", "tests"]) to scope mypy to specific directories (defaults to "." as before) - Applied to PythonToolchain and UvProject in both Python and TS DSLs - ruff format across all harmont-py source and test files - Dogfood pipelines now use typecheck(paths="harmont") --- .harmont/ci.py | 2 +- .harmont/ci.ts | 2 +- dsls/harmont-py/harmont/_decorator.py | 2 + dsls/harmont-py/harmont/_deploy.py | 2 + dsls/harmont-py/harmont/_envelope.py | 11 +- dsls/harmont-py/harmont/_registry.py | 4 +- dsls/harmont-py/harmont/_step.py | 6 +- dsls/harmont-py/harmont/_target.py | 4 +- dsls/harmont-py/harmont/_typing.py | 3 +- dsls/harmont-py/harmont/cmake.py | 17 +- dsls/harmont-py/harmont/dev/__init__.py | 1 + dsls/harmont-py/harmont/dev/__main__.py | 1 + dsls/harmont-py/harmont/dev/_deployment.py | 2 + dsls/harmont-py/harmont/dev/_factory.py | 3 +- dsls/harmont-py/harmont/dev/_port.py | 1 + dsls/harmont-py/harmont/dev/_registry_dump.py | 13 +- dsls/harmont-py/harmont/dotnet.py | 16 +- dsls/harmont-py/harmont/elm.py | 24 +- dsls/harmont-py/harmont/go.py | 8 +- dsls/harmont-py/harmont/gradle.py | 17 +- dsls/harmont-py/harmont/haskell.py | 29 +- dsls/harmont-py/harmont/npm.py | 6 +- dsls/harmont-py/harmont/ocaml.py | 5 +- dsls/harmont-py/harmont/perl.py | 8 +- dsls/harmont-py/harmont/pipeline.py | 4 +- dsls/harmont-py/harmont/py/uv.py | 45 +- dsls/harmont-py/harmont/python.py | 30 +- dsls/harmont-py/harmont/ruby.py | 8 +- dsls/harmont-py/harmont/rust.py | 17 +- dsls/harmont-py/harmont/triggers.py | 10 +- dsls/harmont-py/harmont/zig.py | 14 +- dsls/harmont-py/tests/conftest.py | 1 + dsls/harmont-py/tests/dev/conftest.py | 1 + .../tests/dev/test_canonical_example.py | 6 +- dsls/harmont-py/tests/dev/test_decorator.py | 6 +- dsls/harmont-py/tests/dev/test_dep_marker.py | 1 + .../tests/dev/test_deploy_factory.py | 1 + dsls/harmont-py/tests/dev/test_dump_cli.py | 7 +- .../tests/dev/test_local_deployment.py | 34 +- .../tests/dev/test_port_sentinel.py | 3 +- .../tests/dev/test_registry_dump.py | 4 +- dsls/harmont-py/tests/dev/test_topo.py | 4 +- .../tests/examples_render_conftest.py | 1 + dsls/harmont-py/tests/test_cmake.py | 7 +- dsls/harmont-py/tests/test_composer.py | 1 + dsls/harmont-py/tests/test_decorator.py | 5 + dsls/harmont-py/tests/test_deps.py | 1 + dsls/harmont-py/tests/test_dotnet.py | 1 + dsls/harmont-py/tests/test_e2e_fixtures.py | 5 +- dsls/harmont-py/tests/test_elm.py | 6 +- dsls/harmont-py/tests/test_envelope.py | 2 + dsls/harmont-py/tests/test_examples_render.py | 8 +- dsls/harmont-py/tests/test_go.py | 4 +- dsls/harmont-py/tests/test_gradle.py | 1 + dsls/harmont-py/tests/test_har_28_example.py | 1 + dsls/harmont-py/tests/test_haskell.py | 1 + .../tests/test_haskell_cabal_alias.py | 1 + dsls/harmont-py/tests/test_keygen.py | 204 ++++--- dsls/harmont-py/tests/test_npm.py | 6 +- dsls/harmont-py/tests/test_ocaml.py | 1 + dsls/harmont-py/tests/test_perl.py | 1 + .../tests/test_pipeline_fixtures.py | 1 + dsls/harmont-py/tests/test_py_uv.py | 20 +- dsls/harmont-py/tests/test_python.py | 25 +- dsls/harmont-py/tests/test_ruby.py | 1 + dsls/harmont-py/tests/test_rust.py | 16 +- dsls/harmont-py/tests/test_sh_shorthand.py | 1 + dsls/harmont-py/tests/test_step_sh.py | 3 +- .../harmont-py/tests/test_strict_signature.py | 3 + dsls/harmont-py/tests/test_target.py | 1 + .../tests/test_target_cross_module.py | 6 + dsls/harmont-py/tests/test_target_fixtures.py | 3 + dsls/harmont-py/tests/test_target_unwrap.py | 1 + dsls/harmont-py/tests/test_toolchain.py | 1 + .../tests/test_toolchain_compose.py | 8 +- dsls/harmont-py/tests/test_triggers.py | 1 + dsls/harmont-py/tests/test_typing_markers.py | 1 + dsls/harmont-py/tests/test_zig.py | 1 + dsls/harmont-py/tests/test_zig_toolchain.py | 5 +- dsls/harmont-py/uv.lock | 525 ++++++++++++++++++ dsls/harmont-ts/src/toolchains/py/uv.ts | 13 +- dsls/harmont-ts/src/toolchains/python.ts | 13 +- .../harmont-ts/tests/toolchains/py/uv.test.ts | 12 + .../tests/toolchains/python.test.ts | 12 + 84 files changed, 1050 insertions(+), 263 deletions(-) create mode 100644 dsls/harmont-py/uv.lock diff --git a/.harmont/ci.py b/.harmont/ci.py index 3a9da1b..1e3b0e8 100644 --- a/.harmont/ci.py +++ b/.harmont/ci.py @@ -35,7 +35,7 @@ def ci( rust_project.clippy(), rust_project.fmt(), py_project.lint(), - py_project.run("mypy harmont", label=":python: typecheck"), + py_project.typecheck(paths="harmont"), py_project.run( "pytest -v" " --deselect tests/test_gradle.py" diff --git a/.harmont/ci.ts b/.harmont/ci.ts index 5b15430..4052257 100644 --- a/.harmont/ci.ts +++ b/.harmont/ci.ts @@ -14,7 +14,7 @@ const pipelines: PipelineDefinition[] = [ rustProject.clippy(), rustProject.fmt(), pyProject.lint(), - pyProject.run("mypy harmont", { label: ":python: typecheck" }), + pyProject.typecheck({ paths: "harmont" }), pyProject.run( "pytest -v --deselect tests/test_gradle.py --deselect tests/test_haskell.py", { label: ":python: test" }, diff --git a/dsls/harmont-py/harmont/_decorator.py b/dsls/harmont-py/harmont/_decorator.py index 69fe75c..1c46ed0 100644 --- a/dsls/harmont-py/harmont/_decorator.py +++ b/dsls/harmont-py/harmont/_decorator.py @@ -1,4 +1,5 @@ """@hm.pipeline decorator — see docs/superpowers/specs/2026-05-10-har-9-imperfect-dsl-design.md.""" + from __future__ import annotations import re @@ -43,6 +44,7 @@ def pipeline( declare dependencies as parameters (pytest-style); each parameter name is resolved against the global target registry. """ + def decorator(fn: Callable[..., Any]) -> Callable[[], Any]: validate_target_signature(fn) resolved = slug if slug is not None else fn.__name__ diff --git a/dsls/harmont-py/harmont/_deploy.py b/dsls/harmont-py/harmont/_deploy.py index acac143..27eba07 100644 --- a/dsls/harmont-py/harmont/_deploy.py +++ b/dsls/harmont-py/harmont/_deploy.py @@ -6,6 +6,7 @@ The registry stores deployments polymorphically; CLI subcommands filter by ``isinstance`` or by the ``driver`` discriminator. """ + from __future__ import annotations import dataclasses @@ -27,6 +28,7 @@ class Deployment: ``name`` is the slug the user passed to ``@hm.deploy``. ``driver`` is the discriminator string ("local" for ``hm.dev``). """ + name: str driver: str diff --git a/dsls/harmont-py/harmont/_envelope.py b/dsls/harmont-py/harmont/_envelope.py index 6ada1dd..56f183f 100644 --- a/dsls/harmont-py/harmont/_envelope.py +++ b/dsls/harmont-py/harmont/_envelope.py @@ -38,10 +38,7 @@ def _render_one( try: leaves = as_leaves(raw) except TypeError as e: - msg = ( - f"pipeline {reg.slug!r}: invalid return value\n" - f" → {e}" - ) + msg = f"pipeline {reg.slug!r}: invalid return value\n → {e}" raise TypeError(msg) from e ir = _assemble(*leaves, env=reg.env, default_image=reg.default_image) resolve_pipeline_keys( @@ -84,8 +81,10 @@ def dump_registry_json( """ clear_target_memo() env_map: Mapping[str, str] = env if env is not None else os.environ - org = pipeline_org if pipeline_org is not None else env_map.get( - "HARMONT_PIPELINE_ORG", "default" + org = ( + pipeline_org + if pipeline_org is not None + else env_map.get("HARMONT_PIPELINE_ORG", "default") ) render_now = now if now is not None else int(time.time()) bp = base_path if base_path is not None else Path.cwd() diff --git a/dsls/harmont-py/harmont/_registry.py b/dsls/harmont-py/harmont/_registry.py index 72c0917..03fe905 100644 --- a/dsls/harmont-py/harmont/_registry.py +++ b/dsls/harmont-py/harmont/_registry.py @@ -3,6 +3,7 @@ Stage 1 (`dump_registry_json` in `_envelope`) walks REGISTRATIONS to emit the envelope JSON the api/cli consume. """ + from __future__ import annotations from dataclasses import dataclass @@ -32,8 +33,7 @@ def register(reg: PipelineRegistration) -> None: """Append a registration; raise on duplicate slug.""" if any(r.slug == reg.slug for r in REGISTRATIONS): msg = ( - f"duplicate pipeline slug {reg.slug!r}\n" - f" → each @hm.pipeline must have a unique slug" + f"duplicate pipeline slug {reg.slug!r}\n → each @hm.pipeline must have a unique slug" ) raise ValueError(msg) REGISTRATIONS.append(reg) diff --git a/dsls/harmont-py/harmont/_step.py b/dsls/harmont-py/harmont/_step.py index 9889a80..01960c3 100644 --- a/dsls/harmont-py/harmont/_step.py +++ b/dsls/harmont-py/harmont/_step.py @@ -62,7 +62,7 @@ def sh( if cwd == "": msg = ( "hm: cwd must be a non-empty path\n" - ' → omit cwd= to run in the workspace root, ' + " → omit cwd= to run in the workspace root, " 'or pass cwd="some/dir"' ) raise ValueError(msg) @@ -71,8 +71,8 @@ def sh( # passes it down to the first emitted command step. Once the # chain has a real cmd, inheritance stops — keeps wire format # identical for normal chains. - effective_image = image if image is not None else ( - self.image if self.cmd is None else None + effective_image = ( + image if image is not None else (self.image if self.cmd is None else None) ) return Step( cmd=effective_cmd, diff --git a/dsls/harmont-py/harmont/_target.py b/dsls/harmont-py/harmont/_target.py index 18c8cd8..a8093b3 100644 --- a/dsls/harmont-py/harmont/_target.py +++ b/dsls/harmont-py/harmont/_target.py @@ -76,7 +76,8 @@ def clear_target_cache() -> None: def target( - *, name: str | None = None, + *, + name: str | None = None, ) -> Callable[[Callable[..., Any]], Callable[[], Any]]: """Mark a function as a reusable, memoized pipeline building block. @@ -88,6 +89,7 @@ def target( name collides with another target or when a more human-readable registry key is wanted. """ + def decorator(fn: Callable[..., Any]) -> Callable[[], Any]: validate_target_signature(fn) target_name = name if name is not None else fn.__name__ diff --git a/dsls/harmont-py/harmont/_typing.py b/dsls/harmont-py/harmont/_typing.py index 953db49..051f484 100644 --- a/dsls/harmont-py/harmont/_typing.py +++ b/dsls/harmont-py/harmont/_typing.py @@ -90,8 +90,7 @@ def BaseImage(image: str) -> _BaseImageMarker: # noqa: N802 — factory mimicki """ if not isinstance(image, str) or not image: msg = ( - "hm: BaseImage(...) takes a non-empty image string\n" - ' → e.g. BaseImage("ubuntu-24.04")' + 'hm: BaseImage(...) takes a non-empty image string\n → e.g. BaseImage("ubuntu-24.04")' ) raise TypeError(msg) return _BaseImageMarker(image) diff --git a/dsls/harmont-py/harmont/cmake.py b/dsls/harmont-py/harmont/cmake.py index 4264848..12c5308 100644 --- a/dsls/harmont-py/harmont/cmake.py +++ b/dsls/harmont-py/harmont/cmake.py @@ -46,20 +46,23 @@ def _emit(self, cmd: str, default_label: str, **kw: Any) -> Step: def configure(self, **kw: Any) -> Step: return self._emit( f"cd {self.path} && cmake -S . -B build", - f":{self._tag}: configure", **kw, + f":{self._tag}: configure", + **kw, ) def build(self, **kw: Any) -> Step: return self._emit( f"cd {self.path} && cmake -S . -B build && cmake --build build", - f":{self._tag}: build", **kw, + f":{self._tag}: build", + **kw, ) def test(self, **kw: Any) -> Step: return self._emit( f"cd {self.path} && cmake -S . -B build && cmake --build build " "&& ctest --test-dir build --output-on-failure", - f":{self._tag}: test", **kw, + f":{self._tag}: test", + **kw, ) def fmt(self, **kw: Any) -> Step: @@ -67,7 +70,8 @@ def fmt(self, **kw: Any) -> Step: f"cd {self.path} && find src tests -name '*.[ch]' " f"-o -name '*.cpp' -o -name '*.hpp' | " f"xargs clang-format --dry-run --Werror", - f":{self._tag}: fmt", **kw, + f":{self._tag}: fmt", + **kw, ) @@ -79,10 +83,7 @@ def _make_cmake( base: Step | None = None, ) -> CMakeProject: if lang not in ("c", "cpp"): - msg = ( - f"hm.cmake: invalid lang {lang!r}\n" - ' → use "c" or "cpp"' - ) + msg = f'hm.cmake: invalid lang {lang!r}\n → use "c" or "cpp"' raise ValueError(msg) installed = make_install_chain( apt_packages=APT_PACKAGES, diff --git a/dsls/harmont-py/harmont/dev/__init__.py b/dsls/harmont-py/harmont/dev/__init__.py index 060c33e..00d2c01 100644 --- a/dsls/harmont-py/harmont/dev/__init__.py +++ b/dsls/harmont-py/harmont/dev/__init__.py @@ -9,6 +9,7 @@ LocalDeployment (concrete subclass) dump_registry_json(*, worktree_root) -> str """ + from __future__ import annotations from ._deployment import LocalDeployment diff --git a/dsls/harmont-py/harmont/dev/__main__.py b/dsls/harmont-py/harmont/dev/__main__.py index 9366e61..071b467 100644 --- a/dsls/harmont-py/harmont/dev/__main__.py +++ b/dsls/harmont-py/harmont/dev/__main__.py @@ -8,6 +8,7 @@ Errors go to stderr with exit code 1 (DSL error) or 2 (argparse usage error), matching ``harmont``'s convention. """ + from __future__ import annotations import argparse diff --git a/dsls/harmont-py/harmont/dev/_deployment.py b/dsls/harmont-py/harmont/dev/_deployment.py index 561a1cc..3fbdfc3 100644 --- a/dsls/harmont-py/harmont/dev/_deployment.py +++ b/dsls/harmont-py/harmont/dev/_deployment.py @@ -4,6 +4,7 @@ factory does input validation and coerces fields. ``__post_init__`` is the last-line invariant check (driver must be 'local'). """ + from __future__ import annotations from dataclasses import dataclass @@ -29,6 +30,7 @@ class LocalDeployment(Deployment): ``volumes`` maps host paths (relative or absolute) to container paths (with optional ``:ro`` suffix). """ + image: str | None from_step: Step | None cmd: tuple[str, ...] | None diff --git a/dsls/harmont-py/harmont/dev/_factory.py b/dsls/harmont-py/harmont/dev/_factory.py index 8d2e9f8..6b845af 100644 --- a/dsls/harmont-py/harmont/dev/_factory.py +++ b/dsls/harmont-py/harmont/dev/_factory.py @@ -5,6 +5,7 @@ emits LocalDeployment with name="" — the decorator stamps the slug in afterwards via dataclasses.replace. """ + from __future__ import annotations from typing import TYPE_CHECKING @@ -51,7 +52,7 @@ def deploy( workdir_resolved = _validate_workdir(workdir) return LocalDeployment( - name="", # decorator stamps the slug in + name="", # decorator stamps the slug in driver="local", image=image, from_step=from_, diff --git a/dsls/harmont-py/harmont/dev/_port.py b/dsls/harmont-py/harmont/dev/_port.py index 5bef7d5..9571148 100644 --- a/dsls/harmont-py/harmont/dev/_port.py +++ b/dsls/harmont-py/harmont/dev/_port.py @@ -5,6 +5,7 @@ Any other position (env value, cmd arg, …) is rejected at the call site that consumes it, with a fix-directed message per PRINCIPLES § 5. """ + from __future__ import annotations diff --git a/dsls/harmont-py/harmont/dev/_registry_dump.py b/dsls/harmont-py/harmont/dev/_registry_dump.py index 2b40d89..f2bcd1e 100644 --- a/dsls/harmont-py/harmont/dev/_registry_dump.py +++ b/dsls/harmont-py/harmont/dev/_registry_dump.py @@ -11,6 +11,7 @@ keygen path so the Rust executor can use the terminal key as the build-image tag without re-running the algorithm. """ + from __future__ import annotations import json @@ -93,8 +94,10 @@ def dump_registry_json( "expected a Deployment subclass" ) raise TypeError(msg) - return json.dumps({ - "schema_version": "0", - "worktree": str(wt), - "deployments": deployments, - }) + return json.dumps( + { + "schema_version": "0", + "worktree": str(wt), + "deployments": deployments, + } + ) diff --git a/dsls/harmont-py/harmont/dotnet.py b/dsls/harmont-py/harmont/dotnet.py index 43fa617..4295bab 100644 --- a/dsls/harmont-py/harmont/dotnet.py +++ b/dsls/harmont-py/harmont/dotnet.py @@ -49,18 +49,23 @@ def _emit(self, cmd: str, default_label: str, **kw: Any) -> Step: def build(self, **kw: Any) -> Step: return self._emit( - f"cd {self.path} && dotnet build", ":dotnet: build", **kw, + f"cd {self.path} && dotnet build", + ":dotnet: build", + **kw, ) def test(self, **kw: Any) -> Step: return self._emit( - f"cd {self.path} && dotnet test", ":dotnet: test", **kw, + f"cd {self.path} && dotnet test", + ":dotnet: test", + **kw, ) def fmt(self, **kw: Any) -> Step: return self._emit( f"cd {self.path} && dotnet format --verify-no-changes", - ":dotnet: fmt", **kw, + ":dotnet: fmt", + **kw, ) @@ -72,10 +77,7 @@ def _make_dotnet( base: Step | None = None, ) -> DotnetProject: if not _CHANNEL_RE.match(channel): - msg = ( - f"hm.dotnet: invalid channel {channel!r}\n" - ' → use "8.0", "LTS", or "STS"' - ) + msg = f'hm.dotnet: invalid channel {channel!r}\n → use "8.0", "LTS", or "STS"' raise ValueError(msg) installed = make_install_chain( apt_packages=APT_PACKAGES, diff --git a/dsls/harmont-py/harmont/elm.py b/dsls/harmont-py/harmont/elm.py index 5a8f6c2..68b208a 100644 --- a/dsls/harmont-py/harmont/elm.py +++ b/dsls/harmont-py/harmont/elm.py @@ -53,25 +53,29 @@ def make(self, target: str, *, output: str | None = None, **kw: Any) -> Step: suffix = f" --output={output}" if output is not None else "" return self._emit( f"cd {self.path} && elm make {target}{suffix}", - f":elm: make {target}", **kw, + f":elm: make {target}", + **kw, ) def test(self, **kw: Any) -> Step: return self._emit( f"cd {self.path} && npx --yes elm-test", - ":elm: test", **kw, + ":elm: test", + **kw, ) def review(self, **kw: Any) -> Step: return self._emit( f"cd {self.path} && npx --yes elm-review", - ":elm: review", **kw, + ":elm: review", + **kw, ) def fmt(self, **kw: Any) -> Step: return self._emit( f"cd {self.path} && npx --yes elm-format --validate .", - ":elm: fmt", **kw, + ":elm: fmt", + **kw, ) @@ -84,10 +88,7 @@ def _make_elm( base: Step | None = None, ) -> ElmProject: if not _VERSION_RE.match(elm_version): - msg = ( - f"hm.elm: invalid elm_version {elm_version!r}\n" - ' → e.g. elm_version="0.19.1"' - ) + msg = f'hm.elm: invalid elm_version {elm_version!r}\n → e.g. elm_version="0.19.1"' raise ValueError(msg) node_installed = make_install_chain( apt_packages=APT_PACKAGES, @@ -119,8 +120,11 @@ def __call__( base: Step | None = None, ) -> ElmProject: return _make_elm( - path=path, elm_version=elm_version, node_version=node_version, - image=image, base=base, + path=path, + elm_version=elm_version, + node_version=node_version, + image=image, + base=base, ) def make(self, target: str, *, output: str | None = None, **kw: Any) -> Step: diff --git a/dsls/harmont-py/harmont/go.py b/dsls/harmont-py/harmont/go.py index 921e690..c82040d 100644 --- a/dsls/harmont-py/harmont/go.py +++ b/dsls/harmont-py/harmont/go.py @@ -57,7 +57,8 @@ def vet(self, **kw: Any) -> Step: def fmt(self, **kw: Any) -> Step: return self._emit( f'cd {self.path} && test -z "$(gofmt -l .)"', - ":go: fmt", **kw, + ":go: fmt", + **kw, ) @@ -69,10 +70,7 @@ def _make_go( base: Step | None = None, ) -> GoToolchain: if not _VERSION_RE.match(version): - msg = ( - f"hm.go: invalid version {version!r}\n" - ' → use a Go version like "1.23.2"' - ) + msg = f'hm.go: invalid version {version!r}\n → use a Go version like "1.23.2"' raise ValueError(msg) installed = make_install_chain( apt_packages=APT_PACKAGES, diff --git a/dsls/harmont-py/harmont/gradle.py b/dsls/harmont-py/harmont/gradle.py index 29fb714..0ce1d17 100644 --- a/dsls/harmont-py/harmont/gradle.py +++ b/dsls/harmont-py/harmont/gradle.py @@ -68,17 +68,23 @@ def _emit(self, cmd: str, default_label: str, **kw: Any) -> Step: def build(self, **kw: Any) -> Step: return self._emit( - f"cd {self.path} && gradle build", f":{self._tag}: build", **kw, + f"cd {self.path} && gradle build", + f":{self._tag}: build", + **kw, ) def test(self, **kw: Any) -> Step: return self._emit( - f"cd {self.path} && gradle test", f":{self._tag}: test", **kw, + f"cd {self.path} && gradle test", + f":{self._tag}: test", + **kw, ) def lint(self, **kw: Any) -> Step: return self._emit( - f"cd {self.path} && gradle check", f":{self._tag}: lint", **kw, + f"cd {self.path} && gradle check", + f":{self._tag}: lint", + **kw, ) @@ -91,10 +97,7 @@ def _make_gradle( base: Step | None = None, ) -> GradleProject: if not _JDK_RE.match(jdk): - msg = ( - f"hm.gradle: invalid jdk {jdk!r}\n" - ' → use "11", "17", or "21"' - ) + msg = f'hm.gradle: invalid jdk {jdk!r}\n → use "11", "17", or "21"' raise ValueError(msg) tag = "kotlin" if kotlin else "java" installed = make_install_chain( diff --git a/dsls/harmont-py/harmont/haskell.py b/dsls/harmont-py/harmont/haskell.py index 9e51a83..8a56a9b 100644 --- a/dsls/harmont-py/harmont/haskell.py +++ b/dsls/harmont-py/harmont/haskell.py @@ -28,8 +28,13 @@ from ._step import Step APT_PACKAGES = ( - "curl", "ca-certificates", "build-essential", - "libgmp-dev", "libffi-dev", "libncurses-dev", "zlib1g-dev", + "curl", + "ca-certificates", + "build-essential", + "libgmp-dev", + "libffi-dev", + "libncurses-dev", + "zlib1g-dev", ) _ACTION_KWARGS = frozenset(("cache", "env", "timeout_seconds", "label", "key")) @@ -80,31 +85,36 @@ def _emit(self, cmd: str, default_label: str, **kw: Any) -> Step: def build(self, **kw: Any) -> Step: return self._emit( f"cd {self.path} && cabal build all", - f":haskell: {self.path} build", **kw, + f":haskell: {self.path} build", + **kw, ) def test(self, **kw: Any) -> Step: return self._emit( f"cd {self.path} && cabal test all", - f":haskell: {self.path} test", **kw, + f":haskell: {self.path} test", + **kw, ) def lint(self, **kw: Any) -> Step: return self._emit( f"cd {self.path} && cabal build all --flag werror", - f":haskell: {self.path} lint", **kw, + f":haskell: {self.path} lint", + **kw, ) def hlint(self, **kw: Any) -> Step: return self._emit( f"hlint {self.path}", - f":haskell: {self.path} hlint", **kw, + f":haskell: {self.path} hlint", + **kw, ) def fmt(self, **kw: Any) -> Step: return self._emit( f"fourmolu --mode check {self.path}", - f":haskell: {self.path} fmt", **kw, + f":haskell: {self.path} fmt", + **kw, ) @@ -174,10 +184,7 @@ def _validate_ghc(ghc: str | None) -> str: ) raise ValueError(msg) if not _VERSION_RE.match(ghc): - msg = ( - f"hm.haskell: invalid ghc {ghc!r}\n" - ' → use a GHC version like "9.6.7"' - ) + msg = f'hm.haskell: invalid ghc {ghc!r}\n → use a GHC version like "9.6.7"' raise ValueError(msg) return ghc diff --git a/dsls/harmont-py/harmont/npm.py b/dsls/harmont-py/harmont/npm.py index 212dd7e..3e83486 100644 --- a/dsls/harmont-py/harmont/npm.py +++ b/dsls/harmont-py/harmont/npm.py @@ -35,13 +35,15 @@ def install(self) -> Step: def run(self, script: str, **kw: Any) -> Step: return self._emit( f"cd {self.path} && npm run {script}", - f":node: {script}", **kw, + f":node: {script}", + **kw, ) def test(self, **kw: Any) -> Step: return self._emit( f"cd {self.path} && npm test", - ":node: test", **kw, + ":node: test", + **kw, ) def lint(self, **kw: Any) -> Step: diff --git a/dsls/harmont-py/harmont/ocaml.py b/dsls/harmont-py/harmont/ocaml.py index d41ebca..f2fd88c 100644 --- a/dsls/harmont-py/harmont/ocaml.py +++ b/dsls/harmont-py/harmont/ocaml.py @@ -82,10 +82,7 @@ def _make_ocaml( base: Step | None = None, ) -> OCamlProject: if not _VERSION_RE.match(compiler): - msg = ( - f"hm.ocaml: invalid compiler {compiler!r}\n" - ' → use a compiler version like "5.1.1"' - ) + msg = f'hm.ocaml: invalid compiler {compiler!r}\n → use a compiler version like "5.1.1"' raise ValueError(msg) opam = make_install_chain( apt_packages=APT_PACKAGES, diff --git a/dsls/harmont-py/harmont/perl.py b/dsls/harmont-py/harmont/perl.py index 1007a73..216af5f 100644 --- a/dsls/harmont-py/harmont/perl.py +++ b/dsls/harmont-py/harmont/perl.py @@ -32,12 +32,16 @@ def _emit(self, cmd: str, default_label: str, **kw: Any) -> Step: def test(self, **kw: Any) -> Step: return self._emit( - f"cd {self.path} && prove -lv t/", ":perl: test", **kw, + f"cd {self.path} && prove -lv t/", + ":perl: test", + **kw, ) def lint(self, **kw: Any) -> Step: return self._emit( - f"cd {self.path} && perlcritic lib/", ":perl: lint", **kw, + f"cd {self.path} && perlcritic lib/", + ":perl: lint", + **kw, ) diff --git a/dsls/harmont-py/harmont/pipeline.py b/dsls/harmont-py/harmont/pipeline.py index 92f419e..a7ba488 100644 --- a/dsls/harmont-py/harmont/pipeline.py +++ b/dsls/harmont-py/harmont/pipeline.py @@ -48,7 +48,9 @@ def pipeline( if default_image is not None: out["default_image"] = default_image out["graph"] = _lower_to_graph( - list(leaves), env=env, default_image=default_image, + list(leaves), + env=env, + default_image=default_image, ) return out diff --git a/dsls/harmont-py/harmont/py/uv.py b/dsls/harmont-py/harmont/py/uv.py index a528470..1c2cff9 100644 --- a/dsls/harmont-py/harmont/py/uv.py +++ b/dsls/harmont-py/harmont/py/uv.py @@ -32,6 +32,14 @@ _VERSION_RE = re.compile(r"^([0-9]+\.[0-9]+\.[0-9]+|latest)$") +def _resolve_paths(paths: str | list[str] | None) -> str: + if paths is None: + return "." + if isinstance(paths, str): + return paths + return " ".join(paths) + + def _uv_install_cmd(version: str) -> str: pin = "" if version == "latest" else f"UV_VERSION={version} " return ( @@ -52,45 +60,60 @@ def _emit(self, cmd: str, default_label: str, **kw: Any) -> Step: def test(self, **kw: Any) -> Step: return self._emit( - f"cd {self.path} && uv run pytest", ":python: test", **kw, + f"cd {self.path} && uv run pytest", + ":python: test", + **kw, ) def lint(self, **kw: Any) -> Step: return self._emit( - f"cd {self.path} && uv run ruff check .", ":python: lint", **kw, + f"cd {self.path} && uv run ruff check .", + ":python: lint", + **kw, ) def fmt(self, **kw: Any) -> Step: return self._emit( f"cd {self.path} && uv run ruff format --check .", - ":python: fmt", **kw, + ":python: fmt", + **kw, ) - def typecheck(self, **kw: Any) -> Step: + def typecheck(self, *, paths: str | list[str] | None = None, **kw: Any) -> Step: + target = _resolve_paths(paths) return self._emit( - f"cd {self.path} && uv run mypy .", ":python: typecheck", **kw, + f"cd {self.path} && uv run mypy {target}", + ":python: typecheck", + **kw, ) def run(self, cmd: str, **kw: Any) -> Step: first_word = cmd.split()[0] if cmd.split() else "run" return self._emit( f"cd {self.path} && uv run {cmd}", - f":python: {first_word}", **kw, + f":python: {first_word}", + **kw, ) def build(self, **kw: Any) -> Step: return self._emit( - f"cd {self.path} && uv build", ":python: build", **kw, + f"cd {self.path} && uv build", + ":python: build", + **kw, ) def lock_check(self, **kw: Any) -> Step: return self._emit( - f"cd {self.path} && uv lock --check", ":python: lock-check", **kw, + f"cd {self.path} && uv lock --check", + ":python: lock-check", + **kw, ) def publish(self, **kw: Any) -> Step: return self._emit( - f"cd {self.path} && uv publish", ":python: publish", **kw, + f"cd {self.path} && uv publish", + ":python: publish", + **kw, ) @@ -147,9 +170,9 @@ def fmt(self, **kw: Any) -> Step: action_kw = {k: kw.pop(k) for k in list(kw) if k in _ACTION_KWARGS} return self(**kw).fmt(**action_kw) - def typecheck(self, **kw: Any) -> Step: + def typecheck(self, *, paths: str | list[str] | None = None, **kw: Any) -> Step: action_kw = {k: kw.pop(k) for k in list(kw) if k in _ACTION_KWARGS} - return self(**kw).typecheck(**action_kw) + return self(**kw).typecheck(paths=paths, **action_kw) def run(self, cmd: str, **kw: Any) -> Step: action_kw = {k: kw.pop(k) for k in list(kw) if k in _ACTION_KWARGS} diff --git a/dsls/harmont-py/harmont/python.py b/dsls/harmont-py/harmont/python.py index 9c5396d..eb3073a 100644 --- a/dsls/harmont-py/harmont/python.py +++ b/dsls/harmont-py/harmont/python.py @@ -32,6 +32,14 @@ _VERSION_RE = re.compile(r"^([0-9]+\.[0-9]+\.[0-9]+|latest)$") +def _resolve_paths(paths: str | list[str] | None) -> str: + if paths is None: + return "." + if isinstance(paths, str): + return paths + return " ".join(paths) + + def _uv_install_cmd(uv_version: str) -> str: pin = "" if uv_version == "latest" else f"UV_VERSION={uv_version} " return ( @@ -52,23 +60,31 @@ def _emit(self, cmd: str, default_label: str, **kw: Any) -> Step: def test(self, **kw: Any) -> Step: return self._emit( - f"cd {self.path} && uv run pytest", ":python: test", **kw, + f"cd {self.path} && uv run pytest", + ":python: test", + **kw, ) def lint(self, **kw: Any) -> Step: return self._emit( - f"cd {self.path} && uv run ruff check .", ":python: lint", **kw, + f"cd {self.path} && uv run ruff check .", + ":python: lint", + **kw, ) def fmt(self, **kw: Any) -> Step: return self._emit( f"cd {self.path} && uv run ruff format --check .", - ":python: fmt", **kw, + ":python: fmt", + **kw, ) - def typecheck(self, **kw: Any) -> Step: + def typecheck(self, *, paths: str | list[str] | None = None, **kw: Any) -> Step: + target = _resolve_paths(paths) return self._emit( - f"cd {self.path} && uv run mypy .", ":python: typecheck", **kw, + f"cd {self.path} && uv run mypy {target}", + ":python: typecheck", + **kw, ) @@ -133,9 +149,9 @@ def fmt(self, **kw: Any) -> Step: action_kw = {k: kw.pop(k) for k in list(kw) if k in _ACTION_KWARGS} return self(**kw).fmt(**action_kw) - def typecheck(self, **kw: Any) -> Step: + def typecheck(self, *, paths: str | list[str] | None = None, **kw: Any) -> Step: action_kw = {k: kw.pop(k) for k in list(kw) if k in _ACTION_KWARGS} - return self(**kw).typecheck(**action_kw) + return self(**kw).typecheck(paths=paths, **action_kw) python = _PythonEntry() diff --git a/dsls/harmont-py/harmont/ruby.py b/dsls/harmont-py/harmont/ruby.py index 55f05be..17f1af6 100644 --- a/dsls/harmont-py/harmont/ruby.py +++ b/dsls/harmont-py/harmont/ruby.py @@ -39,12 +39,16 @@ def _emit(self, cmd: str, default_label: str, **kw: Any) -> Step: def test(self, **kw: Any) -> Step: return self._emit( - f"cd {self.path} && bundle exec rspec", ":ruby: test", **kw, + f"cd {self.path} && bundle exec rspec", + ":ruby: test", + **kw, ) def lint(self, **kw: Any) -> Step: return self._emit( - f"cd {self.path} && bundle exec rubocop", ":ruby: lint", **kw, + f"cd {self.path} && bundle exec rubocop", + ":ruby: lint", + **kw, ) diff --git a/dsls/harmont-py/harmont/rust.py b/dsls/harmont-py/harmont/rust.py index 910bd57..b718952 100644 --- a/dsls/harmont-py/harmont/rust.py +++ b/dsls/harmont-py/harmont/rust.py @@ -18,7 +18,11 @@ from ._step import Step APT_PACKAGES = ( - "curl", "ca-certificates", "build-essential", "pkg-config", "libssl-dev", + "curl", + "ca-certificates", + "build-essential", + "pkg-config", + "libssl-dev", ) _ACTION_KWARGS = frozenset(("cache", "env", "timeout_seconds", "label", "key")) @@ -61,7 +65,9 @@ def test(self, *, release: bool = False, **kw: Any) -> Step: def clippy(self, **kw: Any) -> Step: return self._emit( - "cargo clippy --all-targets -- -D warnings", ":rust: clippy", **kw, + "cargo clippy --all-targets -- -D warnings", + ":rust: clippy", + **kw, ) def fmt(self, **kw: Any) -> Step: @@ -111,8 +117,11 @@ def __call__( base: Step | None = None, ) -> RustToolchain: return _make_rust( - path=path, version=version, image=image, - components=components, base=base, + path=path, + version=version, + image=image, + components=components, + base=base, ) def build(self, *, release: bool = False, **kw: Any) -> Step: diff --git a/dsls/harmont-py/harmont/triggers.py b/dsls/harmont-py/harmont/triggers.py index 224982b..8615729 100644 --- a/dsls/harmont-py/harmont/triggers.py +++ b/dsls/harmont-py/harmont/triggers.py @@ -5,6 +5,7 @@ wire-format JSON object documented in docs/superpowers/specs/2026-05-10-har-9-imperfect-dsl-design.md. """ + from __future__ import annotations from dataclasses import dataclass @@ -65,9 +66,7 @@ def push( ) -_PR_TYPES = frozenset( - {"opened", "synchronize", "reopened", "closed", "ready_for_review"} -) +_PR_TYPES = frozenset({"opened", "synchronize", "reopened", "closed", "ready_for_review"}) _DEFAULT_PR_TYPES = ("opened", "synchronize", "reopened") @@ -101,10 +100,7 @@ def pull_request( bad = [t for t in resolved_types if t not in _PR_TYPES] if bad: valid = ", ".join(sorted(_PR_TYPES)) - msg = ( - f"unknown pull_request type {bad[0]!r}\n" - f" → valid: {valid}" - ) + msg = f"unknown pull_request type {bad[0]!r}\n → valid: {valid}" raise ValueError(msg) return PullRequestTrigger( branches=_normalise_globs(branches), diff --git a/dsls/harmont-py/harmont/zig.py b/dsls/harmont-py/harmont/zig.py index dd0e9b1..531d578 100644 --- a/dsls/harmont-py/harmont/zig.py +++ b/dsls/harmont-py/harmont/zig.py @@ -58,19 +58,22 @@ def _emit(self, cmd: str, default_label: str, **kw: Any) -> Step: def build(self, **kw: Any) -> Step: return self._emit( f"cd {self.path} && zig build", - f":zig: {self.path} build", **kw, + f":zig: {self.path} build", + **kw, ) def test(self, **kw: Any) -> Step: return self._emit( f"cd {self.path} && zig build test", - f":zig: {self.path} test", **kw, + f":zig: {self.path} test", + **kw, ) def fmt(self, **kw: Any) -> Step: return self._emit( f"cd {self.path} && zig fmt --check .", - f":zig: {self.path} fmt", **kw, + f":zig: {self.path} fmt", + **kw, ) @@ -98,10 +101,7 @@ def _make_toolchain( base: Step | None, ) -> ZigToolchain: if not _VERSION_RE.match(version): - msg = ( - f"hm.zig: invalid version {version!r}\n" - ' → use a Zig version like "0.13.0"' - ) + msg = f'hm.zig: invalid version {version!r}\n → use a Zig version like "0.13.0"' raise ValueError(msg) installed = make_install_chain( apt_packages=APT_PACKAGES, diff --git a/dsls/harmont-py/tests/conftest.py b/dsls/harmont-py/tests/conftest.py index fdd4d07..847e770 100644 --- a/dsls/harmont-py/tests/conftest.py +++ b/dsls/harmont-py/tests/conftest.py @@ -6,6 +6,7 @@ (e.g. :func:`harmont.haskell.HaskellToolchain.package`) resolve real files in ``api/``, ``freestyle/``, ``app/``, etc. """ + from __future__ import annotations from pathlib import Path diff --git a/dsls/harmont-py/tests/dev/conftest.py b/dsls/harmont-py/tests/dev/conftest.py index ba961a6..82c0e4f 100644 --- a/dsls/harmont-py/tests/dev/conftest.py +++ b/dsls/harmont-py/tests/dev/conftest.py @@ -1,4 +1,5 @@ """Per-test reset of every registry the deploy DSL touches.""" + from __future__ import annotations import pytest diff --git a/dsls/harmont-py/tests/dev/test_canonical_example.py b/dsls/harmont-py/tests/dev/test_canonical_example.py index f94ba04..0dc4abd 100644 --- a/dsls/harmont-py/tests/dev/test_canonical_example.py +++ b/dsls/harmont-py/tests/dev/test_canonical_example.py @@ -4,6 +4,7 @@ image dependency), which is the smallest practical "native language facility" demonstration of an HTTP server in a harmont deployment. """ + from __future__ import annotations import json @@ -40,7 +41,10 @@ def greeter(hello: hm.Dep[hm.Deployment]) -> hm.Deployment: assert out["deployments"]["greeter"]["deps"] == ["hello"] assert out["deployments"]["hello"]["image"] == "python:3.12-alpine" assert out["deployments"]["hello"]["cmd"] == [ - "python", "-m", "http.server", "5678", + "python", + "-m", + "http.server", + "5678", ] assert out["deployments"]["greeter"]["env"] == {"HELLO_HOST": "hello"} assert out["deployments"]["hello"]["from"] is None diff --git a/dsls/harmont-py/tests/dev/test_decorator.py b/dsls/harmont-py/tests/dev/test_decorator.py index 1036d21..920c0ae 100644 --- a/dsls/harmont-py/tests/dev/test_decorator.py +++ b/dsls/harmont-py/tests/dev/test_decorator.py @@ -1,4 +1,5 @@ """@hm.deploy decorator: registration, slug derivation, fixture injection.""" + from __future__ import annotations import pytest @@ -16,7 +17,7 @@ def db(): assert "db" in DEPLOYMENTS resolved = DEPLOYMENTS["db"]() assert isinstance(resolved, LocalDeployment) - assert resolved.name == "db" # decorator stamped slug in + assert resolved.name == "db" # decorator stamped slug in assert resolved.image == "postgres:16" @@ -30,6 +31,7 @@ def redis(): def test_deploy_rejects_invalid_slug(): with pytest.raises(ValueError, match="invalid deployment slug"): + @hm.deploy("Bad Slug") def x(): return hm.dev.deploy(image="x", port_mapping={5432: hm.dev.port()}) @@ -41,6 +43,7 @@ def db1(): return hm.dev.deploy(image="postgres:16", port_mapping={5432: hm.dev.port()}) with pytest.raises(ValueError, match="duplicate deployment slug"): + @hm.deploy("db") def db2(): return hm.dev.deploy(image="postgres:15", port_mapping={5432: hm.dev.port()}) @@ -50,6 +53,7 @@ def test_deploy_requires_marker_on_param(): # validate_target_signature (the shared validator used by @hm.target, # @hm.pipeline, and @hm.deploy) raises TypeError for unmarkered params. with pytest.raises(TypeError, match=r"parameter 'db' has no marker"): + @hm.deploy("api") def api(db): # type: ignore[no-untyped-def] return hm.dev.deploy(image="x", port_mapping={8000: hm.dev.port()}) diff --git a/dsls/harmont-py/tests/dev/test_dep_marker.py b/dsls/harmont-py/tests/dev/test_dep_marker.py index 21004ec..22e150d 100644 --- a/dsls/harmont-py/tests/dev/test_dep_marker.py +++ b/dsls/harmont-py/tests/dev/test_dep_marker.py @@ -1,4 +1,5 @@ """hm.Dep[T] marker is detected; call_with_deps resolves it from DEPLOYMENTS.""" + from __future__ import annotations import pytest diff --git a/dsls/harmont-py/tests/dev/test_deploy_factory.py b/dsls/harmont-py/tests/dev/test_deploy_factory.py index 3bc9905..d733828 100644 --- a/dsls/harmont-py/tests/dev/test_deploy_factory.py +++ b/dsls/harmont-py/tests/dev/test_deploy_factory.py @@ -1,4 +1,5 @@ """hm.dev.deploy(...) field validation + LocalDeployment construction.""" + from __future__ import annotations import pytest diff --git a/dsls/harmont-py/tests/dev/test_dump_cli.py b/dsls/harmont-py/tests/dev/test_dump_cli.py index f09386e..0dd0622 100644 --- a/dsls/harmont-py/tests/dev/test_dump_cli.py +++ b/dsls/harmont-py/tests/dev/test_dump_cli.py @@ -1,4 +1,5 @@ """`python -m harmont.dev --dump-registry` integration.""" + from __future__ import annotations import json @@ -14,7 +15,8 @@ def test_dump_cli_walks_harmont_dir_and_prints_registry(tmp_path: Path): pkg = tmp_path / ".harmont" pkg.mkdir() - (pkg / "deploys.py").write_text(textwrap.dedent(""" + (pkg / "deploys.py").write_text( + textwrap.dedent(""" import harmont as hm @hm.deploy("db") @@ -24,7 +26,8 @@ def db(): port_mapping={5432: hm.dev.port()}, env={"POSTGRES_PASSWORD": "dev"}, ) - """)) + """) + ) result = subprocess.run( [sys.executable, "-m", "harmont.dev", "--dump-registry"], cwd=tmp_path, diff --git a/dsls/harmont-py/tests/dev/test_local_deployment.py b/dsls/harmont-py/tests/dev/test_local_deployment.py index 0c24ef0..7076081 100644 --- a/dsls/harmont-py/tests/dev/test_local_deployment.py +++ b/dsls/harmont-py/tests/dev/test_local_deployment.py @@ -1,4 +1,5 @@ """Abstract Deployment + LocalDeployment construction tests.""" + from __future__ import annotations from collections.abc import Mapping @@ -46,20 +47,30 @@ def test_local_deployment_is_a_deployment_with_driver_local(): def test_local_deployment_rejects_non_local_driver(): with pytest.raises(ValueError, match="driver must be 'local'"): LocalDeployment( - name="db", driver="aws", - image="postgres:16", from_step=None, cmd=None, + name="db", + driver="aws", + image="postgres:16", + from_step=None, + cmd=None, port_mapping={5432: port()}, - env={}, volumes={}, workdir=None, + env={}, + volumes={}, + workdir=None, ) def test_local_deployment_holds_step_chain(): s = scratch().sh("echo hi", image="alpine:3.20") d = LocalDeployment( - name="api", driver="local", - image=None, from_step=s, cmd=None, + name="api", + driver="local", + image=None, + from_step=s, + cmd=None, port_mapping={8000: port()}, - env={}, volumes={}, workdir=None, + env={}, + volumes={}, + workdir=None, ) assert d.from_step is s assert d.image is None @@ -67,10 +78,15 @@ def test_local_deployment_holds_step_chain(): def test_port_mapping_is_a_mapping_of_int_to_port_sentinel(): d = LocalDeployment( - name="db", driver="local", - image="postgres:16", from_step=None, cmd=None, + name="db", + driver="local", + image="postgres:16", + from_step=None, + cmd=None, port_mapping={5432: port()}, - env={}, volumes={}, workdir=None, + env={}, + volumes={}, + workdir=None, ) assert isinstance(d.port_mapping, Mapping) [(cport, sentinel)] = d.port_mapping.items() diff --git a/dsls/harmont-py/tests/dev/test_port_sentinel.py b/dsls/harmont-py/tests/dev/test_port_sentinel.py index 6ef994f..b5b76b6 100644 --- a/dsls/harmont-py/tests/dev/test_port_sentinel.py +++ b/dsls/harmont-py/tests/dev/test_port_sentinel.py @@ -1,4 +1,5 @@ """hm.dev.port() sentinel: equality, repr, and structural use.""" + from __future__ import annotations from harmont.dev import port @@ -7,7 +8,7 @@ def test_port_returns_sentinel_singleton(): a = port() b = port() - assert a is b # singleton — equality-by-identity is fine + assert a is b # singleton — equality-by-identity is fine assert a == b diff --git a/dsls/harmont-py/tests/dev/test_registry_dump.py b/dsls/harmont-py/tests/dev/test_registry_dump.py index d2e3906..eda4074 100644 --- a/dsls/harmont-py/tests/dev/test_registry_dump.py +++ b/dsls/harmont-py/tests/dev/test_registry_dump.py @@ -1,4 +1,5 @@ """dump_registry_json — golden JSON shape for canonical examples.""" + from __future__ import annotations import json @@ -60,7 +61,8 @@ def db(): @hm.deploy("api") def api(db: hm.Dep[hm.Deployment]): return hm.dev.deploy( - image="x", port_mapping={8000: hm.dev.port()}, + image="x", + port_mapping={8000: hm.dev.port()}, env={"DB_HOST": db.name}, ) diff --git a/dsls/harmont-py/tests/dev/test_topo.py b/dsls/harmont-py/tests/dev/test_topo.py index 17cd4fa..57d3b03 100644 --- a/dsls/harmont-py/tests/dev/test_topo.py +++ b/dsls/harmont-py/tests/dev/test_topo.py @@ -1,4 +1,5 @@ """dep_graph extraction + topo_order on the deployment registry.""" + from __future__ import annotations import pytest @@ -23,8 +24,7 @@ def db(): @hm.deploy("api") def api(db: hm.Dep[hm.Deployment]): - return hm.dev.deploy(image="x", port_mapping={8000: hm.dev.port()}, - env={"DB": db.name}) + return hm.dev.deploy(image="x", port_mapping={8000: hm.dev.port()}, env={"DB": db.name}) g = dep_graph() assert g == {"db": (), "api": ("db",)} diff --git a/dsls/harmont-py/tests/examples_render_conftest.py b/dsls/harmont-py/tests/examples_render_conftest.py index 23b9978..c555f2a 100644 --- a/dsls/harmont-py/tests/examples_render_conftest.py +++ b/dsls/harmont-py/tests/examples_render_conftest.py @@ -4,6 +4,7 @@ to v0 IR JSON. They are gated behind HARMONT_CLI_PATH so they only run when a sibling harmont-cli checkout is available. """ + from __future__ import annotations import importlib.util diff --git a/dsls/harmont-py/tests/test_cmake.py b/dsls/harmont-py/tests/test_cmake.py index e182cce..c93536a 100644 --- a/dsls/harmont-py/tests/test_cmake.py +++ b/dsls/harmont-py/tests/test_cmake.py @@ -1,4 +1,5 @@ """CMake (C/C++) toolchain tests.""" + from __future__ import annotations import pytest @@ -22,8 +23,7 @@ def test_cmake_object_form_full_chain(): def test_cmake_actions_share_install(): cm = hm.cmake(path="svc") - p = hm.pipeline(cm.configure(), cm.build(), cm.test(), cm.fmt(), - default_image="ubuntu:24.04") + p = hm.pipeline(cm.configure(), cm.build(), cm.test(), cm.fmt(), default_image="ubuntu:24.04") cmds = _cmds(p) assert len([c for c in cmds if "cmake --version" in c]) == 1 assert len([c for c in cmds if "apt-get install" in c]) == 1 @@ -51,8 +51,7 @@ def test_cmake_invalid_lang_rejected(): def test_cmake_bare_form_actions(): - p = hm.pipeline(hm.cmake.configure(), hm.cmake.build(), - hm.cmake.test(), hm.cmake.fmt()) + p = hm.pipeline(hm.cmake.configure(), hm.cmake.build(), hm.cmake.test(), hm.cmake.fmt()) cmds = _cmds(p) assert any("cmake -S . -B build" in c for c in cmds) assert any("cmake --build build" in c for c in cmds) diff --git a/dsls/harmont-py/tests/test_composer.py b/dsls/harmont-py/tests/test_composer.py index 0b9aef4..0da41d6 100644 --- a/dsls/harmont-py/tests/test_composer.py +++ b/dsls/harmont-py/tests/test_composer.py @@ -1,4 +1,5 @@ """Composer (PHP / Laravel) toolchain tests.""" + from __future__ import annotations import harmont as hm diff --git a/dsls/harmont-py/tests/test_decorator.py b/dsls/harmont-py/tests/test_decorator.py index b682296..7d5aa6c 100644 --- a/dsls/harmont-py/tests/test_decorator.py +++ b/dsls/harmont-py/tests/test_decorator.py @@ -1,4 +1,5 @@ """@hm.pipeline decorator surface.""" + import pytest import harmont as hm @@ -72,6 +73,7 @@ def ci() -> hm.Step: def test_invalid_slug_uppercase(): with pytest.raises(ValueError, match="invalid pipeline slug 'CI'"): + @hm.pipeline("CI") def ci() -> hm.Step: return hm.scratch().sh("echo") @@ -79,6 +81,7 @@ def ci() -> hm.Step: def test_invalid_slug_starts_with_digit(): with pytest.raises(ValueError, match="invalid pipeline slug '1ci'"): + @hm.pipeline("1ci") def x() -> hm.Step: return hm.scratch().sh("echo") @@ -87,6 +90,7 @@ def x() -> hm.Step: def test_invalid_slug_too_long(): long = "a" * 65 with pytest.raises(ValueError, match="invalid pipeline slug"): + @hm.pipeline(long) def x() -> hm.Step: return hm.scratch().sh("echo") @@ -98,6 +102,7 @@ def a() -> hm.Step: return hm.scratch().sh("echo") with pytest.raises(ValueError, match="duplicate pipeline slug"): + @hm.pipeline("ci") def b() -> hm.Step: return hm.scratch().sh("echo") diff --git a/dsls/harmont-py/tests/test_deps.py b/dsls/harmont-py/tests/test_deps.py index e84acc6..7ada062 100644 --- a/dsls/harmont-py/tests/test_deps.py +++ b/dsls/harmont-py/tests/test_deps.py @@ -5,6 +5,7 @@ signature-kind rejection (``*args`` / ``**kwargs`` / positional-only), default-value handling on plain params, and cycle detection. """ + from __future__ import annotations import pytest diff --git a/dsls/harmont-py/tests/test_dotnet.py b/dsls/harmont-py/tests/test_dotnet.py index bf26066..1ba2519 100644 --- a/dsls/harmont-py/tests/test_dotnet.py +++ b/dsls/harmont-py/tests/test_dotnet.py @@ -1,4 +1,5 @@ """dotnet (C#) toolchain tests.""" + from __future__ import annotations import pytest diff --git a/dsls/harmont-py/tests/test_e2e_fixtures.py b/dsls/harmont-py/tests/test_e2e_fixtures.py index eb48195..1bae78f 100644 --- a/dsls/harmont-py/tests/test_e2e_fixtures.py +++ b/dsls/harmont-py/tests/test_e2e_fixtures.py @@ -5,6 +5,7 @@ Regenerate: UPDATE_E2E_FIXTURES=1 pytest tests/test_e2e_fixtures.py -v """ + from __future__ import annotations import json @@ -44,9 +45,7 @@ def _assert_fixture(name: str, ir: dict) -> None: ) expected = json.loads(fixture_path.read_text()) actual = json.loads(rendered) - assert actual == expected, ( - f"Fixture drift for {name}. Regenerate with UPDATE_E2E_FIXTURES=1" - ) + assert actual == expected, f"Fixture drift for {name}. Regenerate with UPDATE_E2E_FIXTURES=1" def _build_monorepo_ci() -> dict: diff --git a/dsls/harmont-py/tests/test_elm.py b/dsls/harmont-py/tests/test_elm.py index ef09672..f547c96 100644 --- a/dsls/harmont-py/tests/test_elm.py +++ b/dsls/harmont-py/tests/test_elm.py @@ -1,4 +1,5 @@ """Elm project abstraction tests.""" + from __future__ import annotations import pytest @@ -103,7 +104,10 @@ def test_elm_action_labels(): def test_elm_actions_share_install(): elm = hm.elm(path="app") p = hm.pipeline( - elm.make("src/Main.elm"), elm.test(), elm.review(), elm.fmt(), + elm.make("src/Main.elm"), + elm.test(), + elm.review(), + elm.fmt(), default_image="ubuntu:24.04", ) cmds = _cmds(p) diff --git a/dsls/harmont-py/tests/test_envelope.py b/dsls/harmont-py/tests/test_envelope.py index 451080f..d2901a3 100644 --- a/dsls/harmont-py/tests/test_envelope.py +++ b/dsls/harmont-py/tests/test_envelope.py @@ -190,6 +190,7 @@ def ci() -> tuple[hm.Step, ...]: def test_envelope_clears_target_cache_between_renders(): """Two consecutive dump_registry_json calls must not share target state.""" + @hm.target() def apt_base() -> hm.Step: return hm.sh("apt-get update") @@ -209,6 +210,7 @@ def ci() -> hm.Step: def test_envelope_wraps_typeerror_with_pipeline_slug(): """Bad return from pipeline fn surfaces as TypeError naming the slug.""" + @hm.pipeline("broken") def broken(): return 42 # not a Step / tuple / toolchain wrapper diff --git a/dsls/harmont-py/tests/test_examples_render.py b/dsls/harmont-py/tests/test_examples_render.py index 66b0bc8..92a89d3 100644 --- a/dsls/harmont-py/tests/test_examples_render.py +++ b/dsls/harmont-py/tests/test_examples_render.py @@ -3,6 +3,7 @@ Gated: skipped when HARMONT_CLI_PATH is unset. CI sets it after cloning harmont-cli. """ + from __future__ import annotations import json @@ -31,7 +32,8 @@ def _example_dirs() -> list[pathlib.Path]: if EXAMPLES_ROOT is None: return [] return sorted( - p for p in EXAMPLES_ROOT.iterdir() + p + for p in EXAMPLES_ROOT.iterdir() if p.is_dir() and (p / ".harmont" / "pipeline.py").is_file() ) @@ -54,9 +56,7 @@ def test_example_renders_to_v0_ir( assert envelope["schema_version"] == "1" assert envelope["pipelines"], f"{example_dir.name}: no pipelines registered" - ci_pipeline = next( - (p for p in envelope["pipelines"] if p["slug"] == "ci"), None - ) + ci_pipeline = next((p for p in envelope["pipelines"] if p["slug"] == "ci"), None) assert ci_pipeline is not None, ( f"{example_dir.name}: no 'ci' pipeline registered; " f"got slugs {[p['slug'] for p in envelope['pipelines']]}" diff --git a/dsls/harmont-py/tests/test_go.py b/dsls/harmont-py/tests/test_go.py index 3bf69a1..1b71f52 100644 --- a/dsls/harmont-py/tests/test_go.py +++ b/dsls/harmont-py/tests/test_go.py @@ -1,4 +1,5 @@ """Go toolchain abstraction tests.""" + from __future__ import annotations import pytest @@ -29,8 +30,7 @@ def test_go_object_form_full_chain(): def test_go_actions_share_install_step(): go = hm.go(path="svc") - p = hm.pipeline(go.build(), go.test(), go.vet(), go.fmt(), - default_image="ubuntu:24.04") + p = hm.pipeline(go.build(), go.test(), go.vet(), go.fmt(), default_image="ubuntu:24.04") cmds = _cmds(p) assert len([c for c in cmds if "go.dev/dl/" in c]) == 1 assert any("go build ./..." in c for c in cmds) diff --git a/dsls/harmont-py/tests/test_gradle.py b/dsls/harmont-py/tests/test_gradle.py index 3c9149f..8d0216a 100644 --- a/dsls/harmont-py/tests/test_gradle.py +++ b/dsls/harmont-py/tests/test_gradle.py @@ -1,4 +1,5 @@ """Gradle (Java/Kotlin) toolchain tests.""" + from __future__ import annotations import pytest diff --git a/dsls/harmont-py/tests/test_har_28_example.py b/dsls/harmont-py/tests/test_har_28_example.py index e51181a..94855b7 100644 --- a/dsls/harmont-py/tests/test_har_28_example.py +++ b/dsls/harmont-py/tests/test_har_28_example.py @@ -1,4 +1,5 @@ """End-to-end: HAR-28 issue example renders to a valid envelope.""" + from __future__ import annotations import json diff --git a/dsls/harmont-py/tests/test_haskell.py b/dsls/harmont-py/tests/test_haskell.py index f832f15..c6a45d1 100644 --- a/dsls/harmont-py/tests/test_haskell.py +++ b/dsls/harmont-py/tests/test_haskell.py @@ -1,4 +1,5 @@ """Haskell toolchain + package abstraction tests.""" + from __future__ import annotations import pytest diff --git a/dsls/harmont-py/tests/test_haskell_cabal_alias.py b/dsls/harmont-py/tests/test_haskell_cabal_alias.py index cf994bb..6a4fb13 100644 --- a/dsls/harmont-py/tests/test_haskell_cabal_alias.py +++ b/dsls/harmont-py/tests/test_haskell_cabal_alias.py @@ -1,4 +1,5 @@ """HaskellToolchain.cabal alias for .package (HAR-28).""" + from __future__ import annotations import harmont as hm diff --git a/dsls/harmont-py/tests/test_keygen.py b/dsls/harmont-py/tests/test_keygen.py index c889dde..6675277 100644 --- a/dsls/harmont-py/tests/test_keygen.py +++ b/dsls/harmont-py/tests/test_keygen.py @@ -31,12 +31,14 @@ def _make_graph(nodes, edges=None): def test_none_policy_emits_no_key(): - graph = _make_graph([ - { - "step": {"key": "a", "cmd": "echo", "cache": {"policy": "none"}}, - "env": {}, - }, - ]) + graph = _make_graph( + [ + { + "step": {"key": "a", "cmd": "echo", "cache": {"policy": "none"}}, + "env": {}, + }, + ] + ) out = resolve_pipeline_keys( graph, pipeline_org="default", @@ -49,16 +51,18 @@ def test_none_policy_emits_no_key(): def test_forever_policy_key_matches_scheme_formula(): - graph = _make_graph([ - { - "step": { - "key": "a", - "cmd": "echo hi", - "cache": {"policy": "forever", "env_keys": []}, + graph = _make_graph( + [ + { + "step": { + "key": "a", + "cmd": "echo hi", + "cache": {"policy": "forever", "env_keys": []}, + }, + "env": {}, }, - "env": {}, - }, - ]) + ] + ) out = resolve_pipeline_keys( graph, pipeline_org="default", @@ -76,16 +80,18 @@ def test_forever_policy_key_matches_scheme_formula(): def test_ttl_policy_key_includes_bucket(): - graph = _make_graph([ - { - "step": { - "key": "a", - "cmd": "x", - "cache": {"policy": "ttl", "duration_seconds": 3600, "env_keys": []}, + graph = _make_graph( + [ + { + "step": { + "key": "a", + "cmd": "x", + "cache": {"policy": "ttl", "duration_seconds": 3600, "env_keys": []}, + }, + "env": {}, }, - "env": {}, - }, - ]) + ] + ) out = resolve_pipeline_keys( graph, pipeline_org="default", @@ -106,16 +112,18 @@ def test_on_change_reads_file_contents(): with tempfile.TemporaryDirectory() as d: f = Path(d) / "file.txt" f.write_bytes(b"hello") - graph = _make_graph([ - { - "step": { - "key": "a", - "cmd": "make", - "cache": {"policy": "on_change", "paths": ["file.txt"]}, + graph = _make_graph( + [ + { + "step": { + "key": "a", + "cmd": "make", + "cache": {"policy": "on_change", "paths": ["file.txt"]}, + }, + "env": {}, }, - "env": {}, - }, - ]) + ] + ) out = resolve_pipeline_keys( graph, pipeline_org="default", @@ -145,16 +153,18 @@ def test_on_change_handles_directory_paths(): (sub / "a.txt").write_bytes(b"alpha") (sub / "b.txt").write_bytes(b"beta") - graph = _make_graph([ - { - "step": { - "key": "s", - "cmd": "make", - "cache": {"policy": "on_change", "paths": ["dir/"]}, + graph = _make_graph( + [ + { + "step": { + "key": "s", + "cmd": "make", + "cache": {"policy": "on_change", "paths": ["dir/"]}, + }, + "env": {}, }, - "env": {}, - }, - ]) + ] + ) out1 = resolve_pipeline_keys( graph, pipeline_org="default", @@ -166,16 +176,18 @@ def test_on_change_handles_directory_paths(): key1 = out1["nodes"][0]["step"]["cache"]["key"] # Same tree -> same key. - graph2 = _make_graph([ - { - "step": { - "key": "s", - "cmd": "make", - "cache": {"policy": "on_change", "paths": ["dir/"]}, + graph2 = _make_graph( + [ + { + "step": { + "key": "s", + "cmd": "make", + "cache": {"policy": "on_change", "paths": ["dir/"]}, + }, + "env": {}, }, - "env": {}, - }, - ]) + ] + ) out_again = resolve_pipeline_keys( graph2, pipeline_org="default", @@ -188,16 +200,18 @@ def test_on_change_handles_directory_paths(): # Modify a file -> key changes. (sub / "a.txt").write_bytes(b"alpha2") - graph3 = _make_graph([ - { - "step": { - "key": "s", - "cmd": "make", - "cache": {"policy": "on_change", "paths": ["dir/"]}, + graph3 = _make_graph( + [ + { + "step": { + "key": "s", + "cmd": "make", + "cache": {"policy": "on_change", "paths": ["dir/"]}, + }, + "env": {}, }, - "env": {}, - }, - ]) + ] + ) out2 = resolve_pipeline_keys( graph3, pipeline_org="default", @@ -211,16 +225,18 @@ def test_on_change_handles_directory_paths(): def test_on_change_missing_path_skipped(): with tempfile.TemporaryDirectory() as d: - graph = _make_graph([ - { - "step": { - "key": "s", - "cmd": "make", - "cache": {"policy": "on_change", "paths": ["nope/"]}, + graph = _make_graph( + [ + { + "step": { + "key": "s", + "cmd": "make", + "cache": {"policy": "on_change", "paths": ["nope/"]}, + }, + "env": {}, }, - "env": {}, - }, - ]) + ] + ) resolve_pipeline_keys( graph, pipeline_org="default", @@ -233,16 +249,18 @@ def test_on_change_missing_path_skipped(): def test_env_keys_are_sorted_and_picked_up(): - graph = _make_graph([ - { - "step": { - "key": "a", - "cmd": "echo", - "cache": {"policy": "forever", "env_keys": ["BAR", "FOO"]}, + graph = _make_graph( + [ + { + "step": { + "key": "a", + "cmd": "echo", + "cache": {"policy": "forever", "env_keys": ["BAR", "FOO"]}, + }, + "env": {}, }, - "env": {}, - }, - ]) + ] + ) out = resolve_pipeline_keys( graph, pipeline_org="default", @@ -300,22 +318,24 @@ def test_parent_key_chains_through_resolved_cache_keys(): def test_compose_concatenates_subpolicies(): - graph = _make_graph([ - { - "step": { - "key": "a", - "cmd": "z", - "cache": { - "policy": "compose", - "sub_policies": [ - {"policy": "forever", "env_keys": []}, - {"policy": "none"}, - ], + graph = _make_graph( + [ + { + "step": { + "key": "a", + "cmd": "z", + "cache": { + "policy": "compose", + "sub_policies": [ + {"policy": "forever", "env_keys": []}, + {"policy": "none"}, + ], + }, }, + "env": {}, }, - "env": {}, - }, - ]) + ] + ) out = resolve_pipeline_keys( graph, pipeline_org="default", diff --git a/dsls/harmont-py/tests/test_npm.py b/dsls/harmont-py/tests/test_npm.py index ccf4aed..bf1fbf8 100644 --- a/dsls/harmont-py/tests/test_npm.py +++ b/dsls/harmont-py/tests/test_npm.py @@ -1,4 +1,5 @@ """Npm project abstraction tests.""" + from __future__ import annotations import pytest @@ -30,7 +31,10 @@ def test_npm_full_chain(): def test_npm_actions_share_install(): node = hm.npm(path="app/codegen") p = hm.pipeline( - node.run("build"), node.test(), node.lint(), node.fmt(), + node.run("build"), + node.test(), + node.lint(), + node.fmt(), default_image="ubuntu:24.04", ) cmds = _cmds(p) diff --git a/dsls/harmont-py/tests/test_ocaml.py b/dsls/harmont-py/tests/test_ocaml.py index 63661be..f907d15 100644 --- a/dsls/harmont-py/tests/test_ocaml.py +++ b/dsls/harmont-py/tests/test_ocaml.py @@ -1,4 +1,5 @@ """OCaml toolchain tests.""" + from __future__ import annotations import pytest diff --git a/dsls/harmont-py/tests/test_perl.py b/dsls/harmont-py/tests/test_perl.py index 50f9a5c..6078682 100644 --- a/dsls/harmont-py/tests/test_perl.py +++ b/dsls/harmont-py/tests/test_perl.py @@ -1,4 +1,5 @@ """Perl toolchain tests.""" + from __future__ import annotations import harmont as hm diff --git a/dsls/harmont-py/tests/test_pipeline_fixtures.py b/dsls/harmont-py/tests/test_pipeline_fixtures.py index ac0faf8..037827b 100644 --- a/dsls/harmont-py/tests/test_pipeline_fixtures.py +++ b/dsls/harmont-py/tests/test_pipeline_fixtures.py @@ -1,4 +1,5 @@ """@hm.pipeline fixture-style param resolution (HAR-28 follow-up).""" + from __future__ import annotations import json diff --git a/dsls/harmont-py/tests/test_py_uv.py b/dsls/harmont-py/tests/test_py_uv.py index cf2af6a..2e1ec72 100644 --- a/dsls/harmont-py/tests/test_py_uv.py +++ b/dsls/harmont-py/tests/test_py_uv.py @@ -36,7 +36,10 @@ def test_full_chain(self): def test_shared_install(self): proj = hm.py.uv(path="svc") p = hm.pipeline( - proj.test(), proj.lint(), proj.fmt(), proj.typecheck(), + proj.test(), + proj.lint(), + proj.fmt(), + proj.typecheck(), default_image="ubuntu:24.04", ) cmds = _cmds(p) @@ -80,6 +83,21 @@ def test_label_override(self): proj = hm.py.uv(path=".") assert proj.test(label=":python: smoke").label == ":python: smoke" + def test_typecheck_paths_string(self): + proj = hm.py.uv(path="myapp") + s = proj.typecheck(paths="src") + assert "uv run mypy src" in s.cmd + + def test_typecheck_paths_list(self): + proj = hm.py.uv(path="myapp") + s = proj.typecheck(paths=["src", "tests"]) + assert "uv run mypy src tests" in s.cmd + + def test_typecheck_paths_default(self): + proj = hm.py.uv(path="myapp") + s = proj.typecheck() + assert "uv run mypy ." in s.cmd + def test_cache_forwarded(self): proj = hm.py.uv(path=".") s = proj.test(cache=CacheOnChange(paths=("pyproject.toml",))) diff --git a/dsls/harmont-py/tests/test_python.py b/dsls/harmont-py/tests/test_python.py index 38e9c24..84d6220 100644 --- a/dsls/harmont-py/tests/test_python.py +++ b/dsls/harmont-py/tests/test_python.py @@ -1,4 +1,5 @@ """Python (uv) toolchain abstraction tests.""" + from __future__ import annotations import pytest @@ -31,8 +32,7 @@ def test_python_object_form_full_chain(): def test_python_actions_share_install_step(): py = hm.python(path="svc") - p = hm.pipeline(py.test(), py.lint(), py.fmt(), py.typecheck(), - default_image="ubuntu:24.04") + p = hm.pipeline(py.test(), py.lint(), py.fmt(), py.typecheck(), default_image="ubuntu:24.04") cmds = _cmds(p) assert len([c for c in cmds if "astral.sh/uv/install.sh" in c]) == 1 assert len([c for c in cmds if "apt-get install" in c]) == 1 @@ -65,8 +65,7 @@ def test_python_bare_form_test(): def test_python_bare_form_all_actions(): - p = hm.pipeline(hm.python.test(), hm.python.lint(), - hm.python.fmt(), hm.python.typecheck()) + p = hm.pipeline(hm.python.test(), hm.python.lint(), hm.python.fmt(), hm.python.typecheck()) cmds = _cmds(p) assert any("pytest" in c for c in cmds) assert any("ruff check" in c for c in cmds) @@ -82,6 +81,24 @@ def test_python_action_labels_auto_generated(): assert py.typecheck().label == ":python: typecheck" +def test_python_typecheck_paths_string(): + py = hm.python(path="myapp") + s = py.typecheck(paths="src") + assert "uv run mypy src" in s.cmd + + +def test_python_typecheck_paths_list(): + py = hm.python(path="myapp") + s = py.typecheck(paths=["src", "tests"]) + assert "uv run mypy src tests" in s.cmd + + +def test_python_typecheck_paths_default(): + py = hm.python(path="myapp") + s = py.typecheck() + assert "uv run mypy ." in s.cmd + + def test_python_action_label_override(): py = hm.python(path=".") assert py.test(label=":python: smoke").label == ":python: smoke" diff --git a/dsls/harmont-py/tests/test_ruby.py b/dsls/harmont-py/tests/test_ruby.py index 98700e3..35f64e7 100644 --- a/dsls/harmont-py/tests/test_ruby.py +++ b/dsls/harmont-py/tests/test_ruby.py @@ -1,4 +1,5 @@ """Ruby toolchain tests.""" + from __future__ import annotations import pytest diff --git a/dsls/harmont-py/tests/test_rust.py b/dsls/harmont-py/tests/test_rust.py index b831aaa..99e8334 100644 --- a/dsls/harmont-py/tests/test_rust.py +++ b/dsls/harmont-py/tests/test_rust.py @@ -1,4 +1,5 @@ """Rust toolchain abstraction tests.""" + from __future__ import annotations import pytest @@ -30,8 +31,14 @@ def test_rust_object_form_full_chain(): def test_rust_actions_share_install_step(): rust = hm.rust(path="cli") - p = hm.pipeline(rust.build(), rust.test(), rust.clippy(), rust.fmt(), rust.doc(), - default_image="ubuntu:24.04") + p = hm.pipeline( + rust.build(), + rust.test(), + rust.clippy(), + rust.fmt(), + rust.doc(), + default_image="ubuntu:24.04", + ) cmds = _cmds(p) assert len([c for c in cmds if "sh.rustup.rs" in c]) == 1 assert len([c for c in cmds if "apt-get install" in c]) == 1 @@ -147,8 +154,9 @@ def test_rust_bare_form_build(): def test_rust_bare_form_all_actions(): - p = hm.pipeline(hm.rust.build(), hm.rust.test(), hm.rust.clippy(), - hm.rust.fmt(), hm.rust.doc()) + p = hm.pipeline( + hm.rust.build(), hm.rust.test(), hm.rust.clippy(), hm.rust.fmt(), hm.rust.doc() + ) cmds = _cmds(p) assert any("cargo build" in c for c in cmds) assert any("cargo test" in c for c in cmds) diff --git a/dsls/harmont-py/tests/test_sh_shorthand.py b/dsls/harmont-py/tests/test_sh_shorthand.py index 6526d2f..ee73dd4 100644 --- a/dsls/harmont-py/tests/test_sh_shorthand.py +++ b/dsls/harmont-py/tests/test_sh_shorthand.py @@ -1,4 +1,5 @@ """hm.sh top-level shorthand (HAR-28).""" + from __future__ import annotations import harmont as hm diff --git a/dsls/harmont-py/tests/test_step_sh.py b/dsls/harmont-py/tests/test_step_sh.py index 9b1dd0c..eb96939 100644 --- a/dsls/harmont-py/tests/test_step_sh.py +++ b/dsls/harmont-py/tests/test_step_sh.py @@ -1,4 +1,5 @@ """Step.sh chain method + cwd= kwarg (HAR-28).""" + from __future__ import annotations from harmont._step import scratch @@ -82,5 +83,3 @@ def test_sh_scratch_without_image_remains_none(): s = scratch().sh("echo") assert s.image is None - - diff --git a/dsls/harmont-py/tests/test_strict_signature.py b/dsls/harmont-py/tests/test_strict_signature.py index c5948da..eaa0af8 100644 --- a/dsls/harmont-py/tests/test_strict_signature.py +++ b/dsls/harmont-py/tests/test_strict_signature.py @@ -1,4 +1,5 @@ """Strict signature validation + Annotated-marker dispatch (HAR-28 follow-up).""" + from __future__ import annotations from typing import Annotated @@ -55,6 +56,7 @@ def fn(base: Annotated[Step, hm.BaseImage("ubuntu-24.04")]) -> Step: # type: ig def test_base_image_then_sh_emits_step_with_image(): """End-to-end: BaseImage param → .sh() → first emitted cmd step carries image.""" + def fn(base: Annotated[Step, hm.BaseImage("ubuntu-24.04")]) -> Step: # type: ignore[empty-body] ... @@ -121,6 +123,7 @@ def fn() -> Step: # type: ignore[empty-body] def test_target_marker_strict_no_default_fallback(): """Even with a default, Target marker requires the target to exist.""" + def fn(api: hm.Target[Step] = None) -> Step: # type: ignore[assignment,empty-body] ... diff --git a/dsls/harmont-py/tests/test_target.py b/dsls/harmont-py/tests/test_target.py index 93e30ee..608db33 100644 --- a/dsls/harmont-py/tests/test_target.py +++ b/dsls/harmont-py/tests/test_target.py @@ -1,4 +1,5 @@ """@hm.target() decorator — memoization + composition (HAR-28).""" + from __future__ import annotations import pytest diff --git a/dsls/harmont-py/tests/test_target_cross_module.py b/dsls/harmont-py/tests/test_target_cross_module.py index 96c9230..fa888fd 100644 --- a/dsls/harmont-py/tests/test_target_cross_module.py +++ b/dsls/harmont-py/tests/test_target_cross_module.py @@ -1,4 +1,5 @@ """Cross-module target deps via global registry (HAR-28 follow-up).""" + from __future__ import annotations import json @@ -23,6 +24,7 @@ def test_target_in_module_a_consumed_by_target_in_module_b(): """Simulate two .harmont/*.py files registering targets in one envelope render. Module A defines apt_base; module B's target depends on it by parameter name.""" + # Module A — defines apt_base. @hm.target() def apt_base() -> hm.Step: @@ -48,11 +50,13 @@ def ci(py_test: hm.Target[hm.Step]) -> hm.Step: def test_duplicate_name_across_modules_raises(): """Same target name registered twice (e.g. two modules both define apt_base) raises at decoration time.""" + @hm.target() def apt_base() -> hm.Step: return hm.sh("first") with pytest.raises(ValueError, match="duplicate target name 'apt_base'"): + @hm.target() def apt_base() -> hm.Step: return hm.sh("second") @@ -60,6 +64,7 @@ def apt_base() -> hm.Step: def test_disambiguate_via_explicit_name(): """Two modules with same fn name can coexist via name=.""" + @hm.target(name="apt_base_a") def apt_base() -> hm.Step: return hm.sh("first") @@ -69,5 +74,6 @@ def apt_base() -> hm.Step: # noqa: F811 return hm.sh("second") from harmont._deps import _TARGETS_BY_NAME + assert "apt_base_a" in _TARGETS_BY_NAME assert "apt_base_b" in _TARGETS_BY_NAME diff --git a/dsls/harmont-py/tests/test_target_fixtures.py b/dsls/harmont-py/tests/test_target_fixtures.py index d63b2b6..336024e 100644 --- a/dsls/harmont-py/tests/test_target_fixtures.py +++ b/dsls/harmont-py/tests/test_target_fixtures.py @@ -1,4 +1,5 @@ """@hm.target fixture-style param resolution (HAR-28 follow-up).""" + from __future__ import annotations import pytest @@ -77,6 +78,7 @@ def apt_base() -> hm.Step: return hm.sh("a") with pytest.raises(ValueError, match="duplicate target name 'apt_base'"): + @hm.target() def apt_base() -> hm.Step: return hm.sh("b") @@ -91,6 +93,7 @@ def whatever() -> hm.Step: return hm.sh("apt-get update") from harmont._deps import _TARGETS_BY_NAME + assert "apt-base" in _TARGETS_BY_NAME assert "whatever" not in _TARGETS_BY_NAME diff --git a/dsls/harmont-py/tests/test_target_unwrap.py b/dsls/harmont-py/tests/test_target_unwrap.py index a9e3aad..496afcc 100644 --- a/dsls/harmont-py/tests/test_target_unwrap.py +++ b/dsls/harmont-py/tests/test_target_unwrap.py @@ -1,4 +1,5 @@ """as_leaves unwraps toolchain return values to (Step, ...) (HAR-28).""" + from __future__ import annotations import pytest diff --git a/dsls/harmont-py/tests/test_toolchain.py b/dsls/harmont-py/tests/test_toolchain.py index 25c4195..4af8153 100644 --- a/dsls/harmont-py/tests/test_toolchain.py +++ b/dsls/harmont-py/tests/test_toolchain.py @@ -1,4 +1,5 @@ """Shared toolchain helpers — apt-install template and chain builder.""" + from __future__ import annotations from datetime import timedelta diff --git a/dsls/harmont-py/tests/test_toolchain_compose.py b/dsls/harmont-py/tests/test_toolchain_compose.py index ecc1bdb..14eefa7 100644 --- a/dsls/harmont-py/tests/test_toolchain_compose.py +++ b/dsls/harmont-py/tests/test_toolchain_compose.py @@ -1,4 +1,5 @@ """Cross-cutting toolchain composition tests (HAR-15).""" + from __future__ import annotations import harmont as hm @@ -57,10 +58,10 @@ def test_escape_hatch_consistent_across_toolchains(): def test_deterministic_emission(): """Two identical pipeline constructions emit equal IR dicts.""" + def build() -> dict: rust = hm.rust(path="cli") - return hm.pipeline(rust.build(), rust.test(), - default_image="ubuntu:24.04") + return hm.pipeline(rust.build(), rust.test(), default_image="ubuntu:24.04") assert build() == build() @@ -73,7 +74,8 @@ def test_mixed_pipeline_compiles(): elm = hm.elm(path="app", base=node.installed) p = hm.pipeline( ghc.package("api").test(), - rust.test(), rust.clippy(), + rust.test(), + rust.clippy(), node.install(), elm.make("src/Main.elm"), default_image="ubuntu:24.04", diff --git a/dsls/harmont-py/tests/test_triggers.py b/dsls/harmont-py/tests/test_triggers.py index c9ae58b..774cb72 100644 --- a/dsls/harmont-py/tests/test_triggers.py +++ b/dsls/harmont-py/tests/test_triggers.py @@ -1,4 +1,5 @@ """Trigger constructors — push/pull_request/schedule.""" + import pytest import harmont as hm diff --git a/dsls/harmont-py/tests/test_typing_markers.py b/dsls/harmont-py/tests/test_typing_markers.py index 336dc0a..f8bdf34 100644 --- a/dsls/harmont-py/tests/test_typing_markers.py +++ b/dsls/harmont-py/tests/test_typing_markers.py @@ -1,4 +1,5 @@ """Target[T] and BaseImage(X) annotation markers (HAR-28 follow-up).""" + from __future__ import annotations import typing diff --git a/dsls/harmont-py/tests/test_zig.py b/dsls/harmont-py/tests/test_zig.py index c944654..71a66a2 100644 --- a/dsls/harmont-py/tests/test_zig.py +++ b/dsls/harmont-py/tests/test_zig.py @@ -1,4 +1,5 @@ """Zig toolchain tests.""" + from __future__ import annotations import pytest diff --git a/dsls/harmont-py/tests/test_zig_toolchain.py b/dsls/harmont-py/tests/test_zig_toolchain.py index 313e419..b4927ab 100644 --- a/dsls/harmont-py/tests/test_zig_toolchain.py +++ b/dsls/harmont-py/tests/test_zig_toolchain.py @@ -1,4 +1,5 @@ """Tests for ZigToolchain (the multi-project entry point for hm.zig).""" + from __future__ import annotations import json @@ -41,6 +42,7 @@ def test_pipeline_with_shared_toolchain_emits_one_install() -> None: ZigToolchain must emit exactly one :zig: install node in the IR.""" import harmont._registry as reg import harmont._target as targets + reg.clear_registry() targets.clear_target_cache() @@ -70,8 +72,7 @@ def ci( zig_installs = [n for n in nodes if n["step"].get("label") == ":zig: install"] assert len(zig_installs) == 1, ( - f"expected exactly one :zig: install node, got " - f"{[n['step']['key'] for n in zig_installs]}" + f"expected exactly one :zig: install node, got {[n['step']['key'] for n in zig_installs]}" ) install_key = zig_installs[0]["step"]["key"] diff --git a/dsls/harmont-py/uv.lock b/dsls/harmont-py/uv.lock new file mode 100644 index 0000000..fe1eaf7 --- /dev/null +++ b/dsls/harmont-py/uv.lock @@ -0,0 +1,525 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version < '3.15'", +] + +[[package]] +name = "ast-serialize" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/9d/09e27731bd5864a9ce04e3244074e674bb8936bf62b45e0357248717adac/ast_serialize-0.5.0.tar.gz", hash = "sha256:5880091bfe6f4f986f22866375c2e884843e7a0b6343ae41aeea659613d879b6", size = 61157, upload-time = "2026-05-17T17:48:29.429Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/9a/13dde51ba9e15f8b97957ab7cb0120d0e381524d651c6bd630b9c359227f/ast_serialize-0.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8f5c14f169eb0972c0c21bada5358b23d6047c76583b005234f865b11f1fa00a", size = 1183520, upload-time = "2026-05-17T17:47:30.831Z" }, + { url = "https://files.pythonhosted.org/packages/37/de/5a7f0a9fe68944f536632a5af84676739c7d2582be42deb082634bf3a754/ast_serialize-0.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7d1a2de9de5be04652f0ed60738356ef94f66db37924a9499fffe98dc491aa0b", size = 1175779, upload-time = "2026-05-17T17:47:32.551Z" }, + { url = "https://files.pythonhosted.org/packages/9c/81/0bb853e76e4f6e9a1855d569003c59e19ffac45f7079d91505d1bb212f92/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be5173fb66f9b49026d9d5a2ff0fc7c7009077107c0eb285b2d60fdf1fe10bd1", size = 1233750, upload-time = "2026-05-17T17:47:34.731Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d3/4cf705beeccc08754d0bbda99aefff26110e209b9a07ac8a6b60eec48531/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8015cd071ac1339924ee2b8098c93e00e155f30a16f40ec9816fcf84f4753f6", size = 1235942, upload-time = "2026-05-17T17:47:36.287Z" }, + { url = "https://files.pythonhosted.org/packages/26/c8/ee097e437ea27dd2b8b227865c875492b585650a5802a22d82b304c8201b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5499e8797edff2a9186aa313ed382c6b422e798e9332d9953badcee6e69a88f2", size = 1442517, upload-time = "2026-05-17T17:47:38.17Z" }, + { url = "https://files.pythonhosted.org/packages/ff/bd/68063442838f1ba68ec72b5436430bc75b3bb17a1a3c3063f09b0c05ae2b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6848f2a093fb5548751a9a09bff8fcd229e2bbeb0e3331f391b6ae6d26cd9903", size = 1254081, upload-time = "2026-05-17T17:47:39.826Z" }, + { url = "https://files.pythonhosted.org/packages/50/e2/1e520793bc6a4e4524a6ab022391e827825eaa0c3811828bfdc6852eca26/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:832d4c998e0b091fd60a6d6bceee535483c4d490de9ba85003af835225719261", size = 1259910, upload-time = "2026-05-17T17:47:41.369Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e1/49b60f467979979cfe6913b43948ff25bca971ad0591d181812f163a988e/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:16db7c62ec0b8efe1d7afd283a388d8f74f2605d56032e5a37747d2de8dba027", size = 1250678, upload-time = "2026-05-17T17:47:43.702Z" }, + { url = "https://files.pythonhosted.org/packages/74/ba/66ab9555de6275677566f6574e5ef6c29cb185ea866f643bc06f8280a8ee/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf5eb061eb5bccade4128ad42da33787d72f6013809cd1b590376ece8b3c937", size = 1301603, upload-time = "2026-05-17T17:47:46.256Z" }, + { url = "https://files.pythonhosted.org/packages/66/42/6aca9b9abc710014b2be9059689e5dd1679339e78f567ffb4d255a9e2050/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:104e4a35bd7c124173c41760ef9aaea17ddb3f86c65cb643671d59afbe3ee94c", size = 1410332, upload-time = "2026-05-17T17:47:47.899Z" }, + { url = "https://files.pythonhosted.org/packages/47/68/2f76594432a22581ecf878b5e75a9b8601c24b2241cf0bbeb1e21fcf370c/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:36be371028fc1675acb38a331bde160dbab7ff907fdf00b67eb6911aa106951b", size = 1509979, upload-time = "2026-05-17T17:47:50.942Z" }, + { url = "https://files.pythonhosted.org/packages/40/ac/a93c9b58292653f6c595752f677a08e608f903b710594909e9231a389b3b/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:061ee58bdb52341c8201a6df41182a977736bae3b7ded87ca7176ca25a8a47ab", size = 1505002, upload-time = "2026-05-17T17:47:54.093Z" }, + { url = "https://files.pythonhosted.org/packages/14/2e/b278f68c497ee2f1d1576cbbef8db5281cd4a5f2db040537592ac9c8862e/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b15219e9cdc9f53f6f4cb51c009203507228226148c05c5e8fe451c28b435eb3", size = 1456231, upload-time = "2026-05-17T17:47:56.311Z" }, + { url = "https://files.pythonhosted.org/packages/0b/43/419be1c566a4c504cd8fd60ce2f84e790f295495c0f327cfaeadf3d51012/ast_serialize-0.5.0-cp314-cp314t-win32.whl", hash = "sha256:842d1c004bb466c7df036f95fabef789570541922b10976b12f5592a69cf0b38", size = 1058668, upload-time = "2026-05-17T17:47:58.305Z" }, + { url = "https://files.pythonhosted.org/packages/03/6f/c9d4d549295ed05111aeb8853232d1afd9d0a179fddb01eeffbb3a4a6842/ast_serialize-0.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b0c06d760909b095cc466356dfccd05a1c7233a6ca191c020dca2c6a6f16c24c", size = 1101075, upload-time = "2026-05-17T17:48:00.35Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8e/d00c5ab30c58222e07d62956fca86c59d91b9ad32997e633c38b526623a3/ast_serialize-0.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:787baedb0262cc49e8ce37cc15c00ae818e46a165a3b36f5e21ed174998104cb", size = 1075347, upload-time = "2026-05-17T17:48:01.753Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9e/dc2530acb3a60dc6e46d65abf27d1d9f86721694757906a148d90a6860de/ast_serialize-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0668aa9459cfa8c9c49ddd2163ebcf43088ba045ef7492af6fe22e0098303101", size = 1191380, upload-time = "2026-05-17T17:48:03.738Z" }, + { url = "https://files.pythonhosted.org/packages/26/0a/bd3d18a582f273d6c843d16bb9e22e9e16365ff7991e92f18f798e9f1224/ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:bf683d6363edf2b39eed6b6d4fe22d34b6203867a67e27134d9e2a2680c4bc4a", size = 1183879, upload-time = "2026-05-17T17:48:05.463Z" }, + { url = "https://files.pythonhosted.org/packages/40/ae/1f919100f8620887af58fcc381c61a1f218cdf89c6e155f87b213e61010a/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc22cf0c9be65e71cf88fda130af60d61eb4a79370ad4cfe7900d48a4aa2211", size = 1244529, upload-time = "2026-05-17T17:48:07.008Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ca/6376559dcce707cdbc1d0d9a13c8d3baaaa501e949ce0ebdc4230cd881aa/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f66173891548c9f2726bf27957b41cabce12fa679dc6da505ddbde4d4b3b31cf", size = 1240560, upload-time = "2026-05-17T17:48:08.46Z" }, + { url = "https://files.pythonhosted.org/packages/35/b2/a620e206b5aeb7efbf2710336df57d457cffbb3991076bbcc1147ef9abd4/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e42d729ef2be96a14efbad355093284739e3670ece3e534f82cc8832790911d9", size = 1451172, upload-time = "2026-05-17T17:48:09.922Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e0/4ad5c04c24a40481b2935ce9a0ccdb6023dc8b667167d06ae530cc3512f2/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b725026bafa801dbd7310eb13a75f0a2e370e7e51b2cb225f9d21fcfadf919ee", size = 1265072, upload-time = "2026-05-17T17:48:11.469Z" }, + { url = "https://files.pythonhosted.org/packages/b2/71/4d1d479aa56d0101c40e17720c3d6ac2af7269ea0487a80b18e7bfd1a5b7/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b54f60c1d78767a53b67eaa663f0dfac3afe606aa07f1301572f588b73d64809", size = 1270488, upload-time = "2026-05-17T17:48:13.575Z" }, + { url = "https://files.pythonhosted.org/packages/6d/4f/0de1bbe06f6edef9fde4ed12ca8e7b3ec7e6e2bd4e672c5af487f7957665/ast_serialize-0.5.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:27d51654fc240a1e87e742d353d98eb45b75f62f129086b3596ab53df2ac2a43", size = 1260702, upload-time = "2026-05-17T17:48:15.141Z" }, + { url = "https://files.pythonhosted.org/packages/75/61/e00872439cfdddcc3c1b6cdaa6e5d904ba8e26a18807c67c4e14409d0ca8/ast_serialize-0.5.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c36237c46dd1674542f2109740ea5ea485a169bf1431939ada0434e17934", size = 1311182, upload-time = "2026-05-17T17:48:16.779Z" }, + { url = "https://files.pythonhosted.org/packages/76/8e/699a5b955f7926956c95e9e1d74132acad73c2fe7a426f94da89123c20aa/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1943db345233cc7194a470f13afa9c59772c0b123dea0c9414c4d4ca54369759", size = 1421410, upload-time = "2026-05-17T17:48:18.527Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ae/d5b7626874478997adc7a29ab28accf21e596fb590c944290401dfd0b29e/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:df1c00022cbbcb064bfaa505aa9c9295362443ce5dacb459d1331d3da353f887", size = 1516587, upload-time = "2026-05-17T17:48:20.133Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ce/b59e02a82d9c4244d64cde502e0b00e83e38816abe19155ceb5437402c7f/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cae65289fc456fde04af979a2be09302ef5d8ab92ef23e596d6746dc267ada27", size = 1515171, upload-time = "2026-05-17T17:48:21.921Z" }, + { url = "https://files.pythonhosted.org/packages/8b/38/d8d90042747d05aa08d4efcf1c99035a5f670a6bf4c214d31644392afbca/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:239a4c354e8d676e9d94631d1d4a64edc6b266f86ff3a5a80aedd344f342c01d", size = 1464668, upload-time = "2026-05-17T17:48:23.544Z" }, + { url = "https://files.pythonhosted.org/packages/dd/51/5b840c4df7334104cecffa28f23904fe81ca89ca223d2450e288de39fd3c/ast_serialize-0.5.0-cp39-abi3-win32.whl", hash = "sha256:143a4ef63285a075871908fda3672dc21864b83a8ec3ee12304aa3e4c5387b9a", size = 1068311, upload-time = "2026-05-17T17:48:25.027Z" }, + { url = "https://files.pythonhosted.org/packages/41/11/ca5672c7d491825bc4cd6702dea106a6b60d928707712ec257c7833ae476/ast_serialize-0.5.0-cp39-abi3-win_amd64.whl", hash = "sha256:cf25572c526add400f26a4750dc6ce0c3bb93fc1f75e7ae0cad4ce4f2cd5c590", size = 1108931, upload-time = "2026-05-17T17:48:26.591Z" }, + { url = "https://files.pythonhosted.org/packages/45/19/cc8bd127d28a43da249aa955cfd164cf8fd534e79e42cea96c4854d72fd0/ast_serialize-0.5.0-cp39-abi3-win_arm64.whl", hash = "sha256:92a31c9c20d25a076edaeec76b128a3535d74a24f340b9a8a7e96c9b86dc9642", size = 1081181, upload-time = "2026-05-17T17:48:28.122Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/7f/d0720730a397a999ffc0fd3f5bebef347338e3a47b727da66fbb228e2ff2/coverage-7.14.0.tar.gz", hash = "sha256:057a6af2f160a85384cde4ab36f0d2777bae1057bae255f95413cdd382aa5c74", size = 919489, upload-time = "2026-05-10T18:02:31.397Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/e4/649c8d4f7f1709b6dbfc474358aa1bba02f67bcd52e2fec291a5014006cd/coverage-7.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a78e2a9d9c5e3b8d4ab9b9d28c985ea66fced0a7d7c2aec1f216e03a2011480", size = 219795, upload-time = "2026-05-10T17:59:48.198Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8d/46692d24b3f395d4cbf17bfcc57136b4f2f9c0c0df864b0bddfc1d71a014/coverage-7.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1816c505187592dcd1c5a5f226601a549f70365fbd00930ac88b0c225b76bb4", size = 220299, upload-time = "2026-05-10T17:59:49.683Z" }, + { url = "https://files.pythonhosted.org/packages/12/c2/a40f5cb295bbcbb697a76947a56081c494c61950366294ee426ffe261099/coverage-7.14.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d8e1762f0e9cbc26ec315471e7b47855218e833cd5a032d706fbf43845d878c7", size = 250721, upload-time = "2026-05-10T17:59:51.494Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/202235eb5c3c14c212462cd91d61b7386bf8fc44bc7a77f4742d2a69174b/coverage-7.14.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9336e23e8bb3a3925398261385e2a1533957d3e760e91070dcb0e98bfa514eed", size = 252633, upload-time = "2026-05-10T17:59:53.244Z" }, + { url = "https://files.pythonhosted.org/packages/bb/80/5f596e8995785124ee191c42535664c5e62c65995b66f4ca21e28ae04c81/coverage-7.14.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd1169b2230f9cbe9c638ba38022ed7a2b1e641cc07f7cea0365e4be2a74980", size = 254743, upload-time = "2026-05-10T17:59:55.021Z" }, + { url = "https://files.pythonhosted.org/packages/1e/6d/0d178825be2350f0adb27984d0aa7cf84bbdab201f6fb926b535d23a8f5f/coverage-7.14.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d1bb3543b58fea74d2cd1abc4054cc927e4724687cb4560cd2ed88d2c7d820c0", size = 256700, upload-time = "2026-05-10T17:59:56.511Z" }, + { url = "https://files.pythonhosted.org/packages/19/5b/9e549c2f6e9dfea472adadba06c294e64735dabc2dd19015fac082095013/coverage-7.14.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a93bac2cb577ef60074999ed56d8a1535894398e2ed920d4185c3ec0c8864742", size = 250854, upload-time = "2026-05-10T17:59:57.94Z" }, + { url = "https://files.pythonhosted.org/packages/3d/1c/b94f9f5f36396021ee2f62c5834b12e6a3d31f0bed5d6fc6d1c3caec087c/coverage-7.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5904abf7e18cddc463219b17552229650c6b79e061d31a1059283051169cf7d5", size = 252433, upload-time = "2026-05-10T17:59:59.688Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cb/d192cd8e1345eccabc32016f2d39072ecd10cb4f4b983ed8d0ebdeaf00dc/coverage-7.14.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:741f57cddc9004a8c81b084660215f33a6b597dbe62c31386b983ee26310e327", size = 250494, upload-time = "2026-05-10T18:00:01.953Z" }, + { url = "https://files.pythonhosted.org/packages/53/c5/aac9f460a41d835dbddef1d377f105f6ac2311d0f3c1588e9f51046d8813/coverage-7.14.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:664123feb0929d7affc135717dbd70d61d98688a08ab1e5ba464739620c6252d", size = 254261, upload-time = "2026-05-10T18:00:03.779Z" }, + { url = "https://files.pythonhosted.org/packages/23/aa/7af7c0081980a9cb3d289c5a435a4b7657dcecbd128e25c580e6a50389b5/coverage-7.14.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c83d2399a51bbec8429266905d33616f04bc5726b1138c35844d5fcd896b2e20", size = 250216, upload-time = "2026-05-10T18:00:05.262Z" }, + { url = "https://files.pythonhosted.org/packages/35/60/a4257538ce2f6b978aeb51870d6c4208c510928a03db7e0339bb625dccb7/coverage-7.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb2e855b87321259a037429288ae85216d191c74de3e79bf57cd2bc0761992c", size = 251125, upload-time = "2026-05-10T18:00:06.858Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ab/f91af47642ec1aa53490e835a95847168d9c77fc39aa58527604c051e145/coverage-7.14.0-cp311-cp311-win32.whl", hash = "sha256:731dc15b385ac52289743d476245b61e1a2927e803bef655b52bc3b2a75a21f3", size = 222300, upload-time = "2026-05-10T18:00:08.608Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f0/a71ddbd874431e7a7cd96071f0c331cfbbad07704833c765d24ffbab8a67/coverage-7.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:bfb0ed8ec5d25e93face268115d7964db9df8b9aae8edcde9ec6b16c726a7cc1", size = 223241, upload-time = "2026-05-10T18:00:10.746Z" }, + { url = "https://files.pythonhosted.org/packages/d8/6e/d9d312a5151a96cd110efee32efc3fc97b01ebd86203fe618ccb29cf4c92/coverage-7.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:7ebb1c6df9f78046a1b1e0a89674cd4bf73b7c648914eebcf976a57fd99a5627", size = 221908, upload-time = "2026-05-10T18:00:12.242Z" }, + { url = "https://files.pythonhosted.org/packages/09/1e/2f996b2c8415cbb6f54b0f5ec1ee850c96d7911961afb4fc05f4a89d8c58/coverage-7.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7ffd19fc8aed057fd686a17a4935eef5f9859d69208f96310e893e64b9b6ccf5", size = 219967, upload-time = "2026-05-10T18:00:13.756Z" }, + { url = "https://files.pythonhosted.org/packages/34/23/35c7aea1274aef7525bdd2dc92f710bdde6d11652239d71d1ec450067939/coverage-7.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:829994cfe1aeb773ca27bf246d4badc1e764893e3bfb98fff820fcecd1ca4662", size = 220329, upload-time = "2026-05-10T18:00:15.264Z" }, + { url = "https://files.pythonhosted.org/packages/75/cf/a8f4b43a16e194b0261257ad28ded5853ec052570afef4a84e1d81189f3b/coverage-7.14.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b4f07cf7edcb7ec39431a5074d7ea83b29a9f71fcfc494f0f40af4e65180420f", size = 251839, upload-time = "2026-05-10T18:00:17.16Z" }, + { url = "https://files.pythonhosted.org/packages/69/ff/6699e7b71e60d3049eb2bdcbc95ee3f35707b2b0e48f32e9e63d3ce30c08/coverage-7.14.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca3d9cf2c32b521bd9518385608787fa86f38daf993695307531822c3430ed67", size = 254576, upload-time = "2026-05-10T18:00:18.829Z" }, + { url = "https://files.pythonhosted.org/packages/22/ec/c936d495fcd67f48f03a9c4ad3297ff80d1f222a5df3980f15b34c186c21/coverage-7.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92af52828e7f29d827346b0294e5a0853fa206db77db0395b282918d41e28db9", size = 255690, upload-time = "2026-05-10T18:00:20.648Z" }, + { url = "https://files.pythonhosted.org/packages/5c/42/5af63f636cc62a4a2b1b3ba9146f6ee6f53a35a50d5cefc54d5670f60999/coverage-7.14.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7b2bb6c9d7e769360d0f20a0f219603fd64f0c8f97de17ab25853261602be0fb", size = 257949, upload-time = "2026-05-10T18:00:22.28Z" }, + { url = "https://files.pythonhosted.org/packages/26/d3/a225317bd2012132a27e1176d51660b826f99bb975876463c44ea0d7ee5a/coverage-7.14.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1c9ed6ef99f88fb8c14aa8e2bf8eb0fe55fa2edfea68f8675d78741df1a5ac0e", size = 252242, upload-time = "2026-05-10T18:00:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7f/9e65495298c3ea414742998539c37d048b5e81cc818fb1828cc6b51d10bf/coverage-7.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8231ade007f37959fbf58acc677f26b922c02eda6f0428ea307da0fd39681bf3", size = 253608, upload-time = "2026-05-10T18:00:25.588Z" }, + { url = "https://files.pythonhosted.org/packages/94/46/1522b524a35bdad22b2b8c4f9d32d0a104b524726ec380b2db68db1746f5/coverage-7.14.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d8b013632cc1ce1d09dbe4f32667b4d320ec2f54fc326ebeffcd0b0bcc2bb6c4", size = 251753, upload-time = "2026-05-10T18:00:27.104Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e9/cdf00d38817742c541ade405e115a3f7bf36e6f2a8b99d4f209861b85a2d/coverage-7.14.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1733198802d71ec4c524f322e2867ee05c62e9e75df86bdca545407a221827d1", size = 255823, upload-time = "2026-05-10T18:00:29.038Z" }, + { url = "https://files.pythonhosted.org/packages/38/fc/5e7877cf5f902d08a17ff1c532511476d87e1bea355bd5028cb97f902e79/coverage-7.14.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:72a305291fa8ee01332f1aaf38b348ca34097f6aa0b0ef627eef2837e57bbba5", size = 251323, upload-time = "2026-05-10T18:00:30.647Z" }, + { url = "https://files.pythonhosted.org/packages/18/9d/50f05a72dff8487464fdd4178dda5daed642a060e60afb644e3d45123559/coverage-7.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcaba850dd317c65423a9d63d88f9573c53b00354d6dd95724576cc98a131595", size = 253197, upload-time = "2026-05-10T18:00:32.211Z" }, + { url = "https://files.pythonhosted.org/packages/00/3f/6f61ffe6439df266c3cf60f5c99cfaa21103d0210d706a42fc6c30683ff8/coverage-7.14.0-cp312-cp312-win32.whl", hash = "sha256:5ac83957a80d0701310e96d8bec68cdcf4f90a7674b7d13f15a344315b41ab27", size = 222515, upload-time = "2026-05-10T18:00:33.717Z" }, + { url = "https://files.pythonhosted.org/packages/85/19/93853133df2cb371083285ef6a93982a0173e7a233b0f61373ba9fd30eb2/coverage-7.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:70390b0da32cb90b501953716302906e8bcce087cb283e70d8c97729f22e92b2", size = 223324, upload-time = "2026-05-10T18:00:35.172Z" }, + { url = "https://files.pythonhosted.org/packages/74/18/9f7fe62f659f24b7a82a0be56bf94c1bd0a89e0ae7ab4c668f6e82404294/coverage-7.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:91b993743d959b8be85b4abf9d5478216a69329c321efe5be0433c1a841d691d", size = 221944, upload-time = "2026-05-10T18:00:37.014Z" }, + { url = "https://files.pythonhosted.org/packages/6b/76/b7c66ee3c66e1b0f9d894c8125983aa0c03fb2336f2fd16559f9c966157f/coverage-7.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f2bbb8254370eb4c628ff3d6fa8a7f74ddc40565394d4f7ab791d1fe568e37ef", size = 219990, upload-time = "2026-05-10T18:00:38.887Z" }, + { url = "https://files.pythonhosted.org/packages/b3/af/e567cbad5ba69c013a50146dfa886dc7193361fda77521f51274ff620e1b/coverage-7.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23b81107f46d3f21d0cbce30664fcec0f5d9f585638a67081750f99738f6bf66", size = 220365, upload-time = "2026-05-10T18:00:40.864Z" }, + { url = "https://files.pythonhosted.org/packages/44/6f/9ad575d505b4d805b254febc8a5b338a2efe278f8786e56ff1cb8413f9c3/coverage-7.14.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:22a7e06a5f11a757cdfe79018e9095f9f69ae283c5cd8123774c788deec8717b", size = 251363, upload-time = "2026-05-10T18:00:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5f/b5370068b2f57787454592ed7dcd1002f0f1703b7db1fa30f6a325a4ca6e/coverage-7.14.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9d1aa57a1dc8e05bdc42e81c5d671d849577aeedf279f4c449d6d286f9ed88ca", size = 253961, upload-time = "2026-05-10T18:00:44.079Z" }, + { url = "https://files.pythonhosted.org/packages/29/1e/51adf17738976e8f2b85ddef7b7aa12a0838b056c92f175941d8862767c1/coverage-7.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90c1a51bcfddf645b3bb7ec333d9e94393a8e94f55642380fa8a9a5a9e636cb7", size = 255193, upload-time = "2026-05-10T18:00:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/9e/7b/5bfd7ac1df3b881c2ac7a5cbc99c7609e6296c402f5ef587cd81c6f355b3/coverage-7.14.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a841fae2fadcae4f438d43b6ccc4aac2ad609f47cdb6cfdce60cbb3fe5ca7bc2", size = 257326, upload-time = "2026-05-10T18:00:47.173Z" }, + { url = "https://files.pythonhosted.org/packages/7d/38/1d37d316b174fad3843a1d76dbdfe4398771c9ecd0515935dd9ece9cd627/coverage-7.14.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c79d2319cabef1fe8e86df73371126931550804738f78ad7d31e3aad85a67367", size = 251582, upload-time = "2026-05-10T18:00:49.152Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/746704f95980ba220214e1a41e18cec5aea80a898eaa53c51bf2d645ff36/coverage-7.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b23b0c6f0b1db6ad769b7050c8b641c0bf215ded26c1816955b17b7f26edfa9", size = 253325, upload-time = "2026-05-10T18:00:51.252Z" }, + { url = "https://files.pythonhosted.org/packages/e1/b9/bbe87206d9687b192352f893797825b5f5b15ecd3aa9c68fbff0c074d77b/coverage-7.14.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:55d3089079ce181a4566b1065ab28d2575eb76d8ac8f81f4fcda2bf037fee087", size = 251291, upload-time = "2026-05-10T18:00:52.816Z" }, + { url = "https://files.pythonhosted.org/packages/46/57/b8cdb12ac0d73ef0243218bd5e22c9df8f92edab8018213a86aec67c5324/coverage-7.14.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:49c005cba1e2f9677fb2845dcdf9a2e72a52a17d63e8231aaaae35d9f50215ef", size = 255448, upload-time = "2026-05-10T18:00:54.548Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d4/5002019538b2036ce3c84340f54d2fd5100d55b0a6b0894eee56128d03c7/coverage-7.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9117377b823daa28aa8635fbb08cda1cd6be3d7143257345459559aeef852d52", size = 251110, upload-time = "2026-05-10T18:00:56.122Z" }, + { url = "https://files.pythonhosted.org/packages/37/53/20c5009477660f084e6ed60bc02a91894b8e234e617e86ecfd9aaf78e27b/coverage-7.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7b79d646cf46d5cf9a9f40281d4441df5849e445726e369006d2b117710b33fe", size = 252885, upload-time = "2026-05-10T18:00:57.967Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ab/3cf6427ac9c1f1db747dbb1ce71dde47984876d4c2cfd018a3fef0a78d4d/coverage-7.14.0-cp313-cp313-win32.whl", hash = "sha256:fb609b3658479e33f9516d46f1a89dbb9b6c261366e3a11844a96ec487533dae", size = 222539, upload-time = "2026-05-10T18:00:59.581Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b8/9228523e80321c2cb4880d1f589bc0171f2f71432c35118ad04dc01decce/coverage-7.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0773d8329cf32b6fd222e4b52622c61fe8d503eb966cfc8d3c3c10c96266d50e", size = 223344, upload-time = "2026-05-10T18:01:01.531Z" }, + { url = "https://files.pythonhosted.org/packages/a3/99/118daa192f95e3a6cb2740100fbf8797cda1734b4134ef0b5d501a7fa8f3/coverage-7.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:b4e26a0f1b696faf283bffe5b8569e44e336c582439df5d53281ab89ee0cba96", size = 221966, upload-time = "2026-05-10T18:01:03.16Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f1/a46cc0c013be170216253184a32366d7cbdb9252feaec866b05c2d12a894/coverage-7.14.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:953f521ca9445300397e65fda3dca58b2dbd68fee983777420b57ac3c77e9f90", size = 220679, upload-time = "2026-05-10T18:01:05.058Z" }, + { url = "https://files.pythonhosted.org/packages/64/8c/9c30a3d311a34177fa432995be7fbfc64477d8bac5630bd38055b1c9b424/coverage-7.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:98af83fd65ae24b1fdd03aaead967a9f523bcd2f1aab2d4f3ffda65bb568a6f1", size = 221033, upload-time = "2026-05-10T18:01:07.002Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cd/3fb5e06c3badefd0c1b47e2044fdca67f8220a4ec2e7fcfb476aa0a67c6c/coverage-7.14.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:668b92e6958c4db7cf92e81caac328dfbbdbb215db2850ad28f0cbe1eea0bfbd", size = 262333, upload-time = "2026-05-10T18:01:08.903Z" }, + { url = "https://files.pythonhosted.org/packages/a8/e6/fbc322325c7294d3e22c1ad6b79e45d0806b25228c8e5842aed6d8169aa7/coverage-7.14.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9fbd898551762dea00d3fef2b1c4f99afd2c6a3ff952ea07d60a9bd5ed4f34bc", size = 264410, upload-time = "2026-05-10T18:01:10.531Z" }, + { url = "https://files.pythonhosted.org/packages/08/92/c497b264bec1673c47cc77e26f760fcda4654cabf1f39546d1a23a3b8c35/coverage-7.14.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68af363c07ecd8d4b7d4043d85cb376d7d227eceb54e5323ee45da73dbd3e426", size = 266836, upload-time = "2026-05-10T18:01:12.19Z" }, + { url = "https://files.pythonhosted.org/packages/78/fc/045da320987f401af5d2815d351e8aa799aec859f60e29f445e3089eeedb/coverage-7.14.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e57054a583da8ac55edf24117ea4c9133032cfc4cf72aa2d48c1e5d4b52f899", size = 267974, upload-time = "2026-05-10T18:01:13.926Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ae/227b1e379497fb7a4fc3286e620f80c8a1e7cec66d45695a01639eb1af65/coverage-7.14.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3499459bbcdd51a65b64c35ab7ed2764eaf3cba826e0df3f1d7fe2e102b70b", size = 261578, upload-time = "2026-05-10T18:01:15.564Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f5/3570342900f2acea31d33ff1590c5d8bac1a8e1a2e1c6d34a5d5e61de681/coverage-7.14.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:45899ec2138a4346ed34d601dedf5076fb74edf2d1dd9dc76a78e82397edee90", size = 264394, upload-time = "2026-05-10T18:01:17.607Z" }, + { url = "https://files.pythonhosted.org/packages/16/29/de1bbc01c935b28f89b1dc3db85b011c055e843a8e5e3b83141c3f80af7f/coverage-7.14.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8767486808c436f05b23ab98eb963fb29185e32a9357a166971685cb3459900f", size = 262022, upload-time = "2026-05-10T18:01:19.304Z" }, + { url = "https://files.pythonhosted.org/packages/35/95/f53890b0bf2fc10ab168e05d38869215e73ca24c4cb521c3bb0eb62fe16b/coverage-7.14.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a3b5ddfd6aa7ddad53ee3edb231e88a2151507a43229b7d71b953916deca127d", size = 265732, upload-time = "2026-05-10T18:01:21.494Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ea/c919e259081dd2bdf0e43b87209709ba7ec2e4117c2a7f5185379c43463c/coverage-7.14.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:63df0fe568e698e1045792399f8ab6da3a6c2dce3182813fb92afa2641087b47", size = 260921, upload-time = "2026-05-10T18:01:23.533Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2c/c2831889705a81dc5d1c6ca12e4d8e9b95dfc146d153488a6c0ea685d28e/coverage-7.14.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:827d6397dbd95144939b18f89edf31f63e1f99633e8d5f32f22ba8bdda567477", size = 263109, upload-time = "2026-05-10T18:01:25.165Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a9/2fcae5003cac3d63fe344d2166243c2756935f48420863c5272b240d550b/coverage-7.14.0-cp313-cp313t-win32.whl", hash = "sha256:7bf43e000d24012599b879791cff41589af90674722421ef11b11a5431920bab", size = 223212, upload-time = "2026-05-10T18:01:27.157Z" }, + { url = "https://files.pythonhosted.org/packages/3f/bb/18e94d7b14b9b398164197114a587a04ab7c9fdbe1d237eef57311c5e883/coverage-7.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3f5549365af25d770e06b1f8f5682d9a5637d06eb494db91c6fa75d3950cc917", size = 224272, upload-time = "2026-05-10T18:01:29.107Z" }, + { url = "https://files.pythonhosted.org/packages/db/56/4f14fad782b035c81c4ffd09159e7103d42bb1d93ac8496d04b90a11b7da/coverage-7.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6d160217ec6fe890f16ad3a9531761589443749e448f91986c972714fad361c8", size = 222530, upload-time = "2026-05-10T18:01:31.151Z" }, + { url = "https://files.pythonhosted.org/packages/1c/18/b9a6586d73992807c26f9a5f274131be3d76b56b18a82b9392e2a25d2e45/coverage-7.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9aed9fa983514ca032790f3fe0d1c0e42ca7e16b42432af1706b50a9a46bef5d", size = 220036, upload-time = "2026-05-10T18:01:33.057Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9b/4165a1d56ddc302a0e2d518fd9d412a4fd0b57562618c78c5f21c57194f5/coverage-7.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ba3b8390db29296dbbf49e91b6fe08f990743a90c8f447ba4c2ffc29670dfa63", size = 220368, upload-time = "2026-05-10T18:01:34.705Z" }, + { url = "https://files.pythonhosted.org/packages/69/aa/c12e52a5ba148d9995229d557e3be6e554fe469addc0e9241b2f0956d8ea/coverage-7.14.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3a5d8e876dfa2f102e970b183863d6dedd023d3c0eeca1fe7a9787bc5f28b212", size = 251417, upload-time = "2026-05-10T18:01:36.949Z" }, + { url = "https://files.pythonhosted.org/packages/d7/51/ec641c26e6dca1b25a7d2035ba6ecb7c884ef1a100a9e42fbe4ce4405139/coverage-7.14.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ebb8f4614a3787d567e610bbfdf96a4798dd69a1afb1bd8ad228d4111fe6ff3", size = 253924, upload-time = "2026-05-10T18:01:38.985Z" }, + { url = "https://files.pythonhosted.org/packages/33/c4/59c3de0bd1b538824173fd518fed51c1ce740ca5ed68e74545983f4053a9/coverage-7.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b9bf47223dd8db3d4c4b2e443b02bace480d428f0822c3f991600448a176c97", size = 255269, upload-time = "2026-05-10T18:01:40.957Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a9/36dfa153a62040296f6e7febfdb20a5720622f6ef5a81a41e8237b9a5344/coverage-7.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3485a836550b303d006d57cc06e3d5afaabc642c77050b7c985a97b13e3776b8", size = 257583, upload-time = "2026-05-10T18:01:42.607Z" }, + { url = "https://files.pythonhosted.org/packages/26/7b/cc2c048d4114d9ab1c2409e9ee365e5ae10736df6dffcfc9444effa6c708/coverage-7.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e7e88110bae996d199d1693ca8ec3fd52441d426401ae963437598667b4c5eb", size = 251434, upload-time = "2026-05-10T18:01:44.537Z" }, + { url = "https://files.pythonhosted.org/packages/ee/df/6770eaa576e604575e9a78055313250faef5faa84bd6f71a39fece519c43/coverage-7.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15228a6800ce7bdf1b74800595e56db7138cecb338fdbf044806e10dcf182dfe", size = 253280, upload-time = "2026-05-10T18:01:46.175Z" }, + { url = "https://files.pythonhosted.org/packages/ad/9e/1c0264514a3f98259a6d64765a397b2c8373e3ba59ee722a4802d3ec0c61/coverage-7.14.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d26ac7f5398bafc5b57421ad994e8a4749e8a7a0e62d05ec7d53014d5963bfa", size = 251241, upload-time = "2026-05-10T18:01:48.732Z" }, + { url = "https://files.pythonhosted.org/packages/64/16/4efdf3e3c4079cdbf0ece56a2fea872df9e8a3e15a13a0af4400e1075944/coverage-7.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb73254ff43c911c967a899e1359bc5049b4b115d6e8fbdde4937d0a2246cd5", size = 255516, upload-time = "2026-05-10T18:01:50.819Z" }, + { url = "https://files.pythonhosted.org/packages/93/69/b1de96346603881b3d1bc8d6447c83200e1c9700ffbaff926ba01ff5724c/coverage-7.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:454a380af72c6adada298ed270d38c7a391288198dbfb8467f786f588751a90c", size = 251059, upload-time = "2026-05-10T18:01:52.773Z" }, + { url = "https://files.pythonhosted.org/packages/a4/66/2881853e0363a5e0a724d1103e53650795367471b6afb234f8b49e713bc6/coverage-7.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:65c86fb646d2bd2972e96bd1a8b45817ed907cee68655d6295fe7ec031d04cca", size = 252716, upload-time = "2026-05-10T18:01:54.506Z" }, + { url = "https://files.pythonhosted.org/packages/55/5c/0d3305d002c41dcde873dbe456491e663dc55152ca526b630b5c47efd62f/coverage-7.14.0-cp314-cp314-win32.whl", hash = "sha256:6a6516b02a6101398e19a3f44820f69bab2590697f7def4331f668b14adaf828", size = 222788, upload-time = "2026-05-10T18:01:56.487Z" }, + { url = "https://files.pythonhosted.org/packages/f9/58/6e1b8f52fdc3184b47dc5037f5070d83a3d11042db1594b02d2a44d786c8/coverage-7.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:45e0f79d8351fa76e256716df91eab12890d32678b9590df7ae1042e4bd4cf5d", size = 223600, upload-time = "2026-05-10T18:01:58.497Z" }, + { url = "https://files.pythonhosted.org/packages/00/70/a18c408e674bc26281cadaedc7351f929bd2094e191e4b15271c30b084cc/coverage-7.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:4b899594a8b2d81e5cc064a0d7f9cac2081fed91049456cae7676787e41549c9", size = 222168, upload-time = "2026-05-10T18:02:00.411Z" }, + { url = "https://files.pythonhosted.org/packages/3d/89/2681f071d238b62aff8dfc2ab44fc24cfdb38d1c01f391a80522ff5d3a16/coverage-7.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f580f8c80acd94ac72e863efe2cab791d8c38d153e0b463b92dfa000d5c84cd1", size = 220766, upload-time = "2026-05-10T18:02:02.313Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c7/c987babafd9207ffa1995e1ef1f9b26762cf4963aa768a66b6f0501e4616/coverage-7.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a2bd259c442cd43c49b30fbafc51776eb19ea396faf159d26a83e6a0a5f13b0c", size = 221035, upload-time = "2026-05-10T18:02:04.017Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e9/d6a5ac3b333088143d6fc877d398a9a674dc03124a2f776e131f03864823/coverage-7.14.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a706b908dfa85538863504c624b237a3cc34232bf403c057414ebfdb3b4d9f84", size = 262405, upload-time = "2026-05-10T18:02:05.915Z" }, + { url = "https://files.pythonhosted.org/packages/38/b1/e70838d29a7c08e22d44398a46db90815bbcbf28de06992bd9210d1a8d8e/coverage-7.14.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7333cd944ee4393b9b3d3c1b598c936d4fc8d70573a4c7dacfec5590dd50e436", size = 264530, upload-time = "2026-05-10T18:02:07.582Z" }, + { url = "https://files.pythonhosted.org/packages/6b/73/5c31ef97763288d03d9995152b96d5475b527c63d91c84b01caea894b83a/coverage-7.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f162bc9a15b82d947b02651b0c7e1609d6f7a8735ca330cfadec8481dd97d5a", size = 266932, upload-time = "2026-05-10T18:02:09.401Z" }, + { url = "https://files.pythonhosted.org/packages/e1/76/dd56d80f29c5f05b4d76f7e7c6d47cafacae017189c75c5759d24f9ff0cc/coverage-7.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:362cb78e01a5dc82009d88004cf60f2e6b6d6fcbfdec05b05af73b0abf40118f", size = 268062, upload-time = "2026-05-10T18:02:11.399Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c7/27ba85cd5b95614f159ff93ebff1901584a8d192e2e5e24c4943a7453f59/coverage-7.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:acebd068fca5512c3a6fde9c045f901613478781a73f0e82b307b214daef23fb", size = 261504, upload-time = "2026-05-10T18:02:13.257Z" }, + { url = "https://files.pythonhosted.org/packages/13/2e/e8149f60ab5d5684c6eee881bdf34b127115cddbb958b196768dd9d63473/coverage-7.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:29fe3da551dface75deb2ccbf87b6b66e2e7ef38f6d89050b428be94afff3490", size = 264398, upload-time = "2026-05-10T18:02:15.063Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7f/1261b025285323225f4b4abffa5a643649dfd67e25ddca7ebcbdea3b7cb3/coverage-7.14.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b4cc4fce8672fffcb09b0eafc167b396b3ba53c4a7230f54b7aaffbf6c835fa9", size = 262000, upload-time = "2026-05-10T18:02:16.756Z" }, + { url = "https://files.pythonhosted.org/packages/d3/dc/829c54f60b9d08389439c00f813c752781c496fc5788c78d8006db4b4f2b/coverage-7.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5d4a51aad8ba8bdcd2b8bd8f03d4aca19693fa2327a3470e4718a25b03481020", size = 265732, upload-time = "2026-05-10T18:02:18.817Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b0/70bd1419941652fa062689cba9c3eeafb8f5e6fbb890bce41c3bdda5dbd6/coverage-7.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9f323af3e1e4f68b60b7b247e37b8515563a61375518fa59de1af48ba28a3db6", size = 260847, upload-time = "2026-05-10T18:02:20.528Z" }, + { url = "https://files.pythonhosted.org/packages/f2/73/be40b2390656c654d35ea0015ea7ba3d945769cf80790ad5e0bb2d56d2ba/coverage-7.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1a0abc7342ea9711c469dd8b821c6c311e6bc6aac1442e5fbd6b27fae0a8f3db", size = 263166, upload-time = "2026-05-10T18:02:22.337Z" }, + { url = "https://files.pythonhosted.org/packages/29/55/4a643f712fcf7cf2881f8ec1e0ccb7b164aff3108f69b51801246c8799f2/coverage-7.14.0-cp314-cp314t-win32.whl", hash = "sha256:a9f864ef57b7172e2db87a096642dd51e179e085ab6b2c371c29e885f65c8fb2", size = 223573, upload-time = "2026-05-10T18:02:24.11Z" }, + { url = "https://files.pythonhosted.org/packages/27/96/3acae5da0953be042c0b4dea6d6789d2f080701c77b88e44d5bd41b9219b/coverage-7.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29943e552fdc08e082eb51400fb2f58e118a83b5542bd06531214e084399b644", size = 224680, upload-time = "2026-05-10T18:02:25.896Z" }, + { url = "https://files.pythonhosted.org/packages/93/3d/6ab5d2dd8325d838737c6f8d83d62eb6230e0d70b87b51b57bbfd08fa767/coverage-7.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:742a73ea621953b012f2c4c2219b512180dd84489acf5b1596b0aafc55b9100b", size = 222703, upload-time = "2026-05-10T18:02:27.822Z" }, + { url = "https://files.pythonhosted.org/packages/61/e8/cb8e80d6f9f55b99588625062822bf946cf03ed06315df4bd8397f5632a1/coverage-7.14.0-py3-none-any.whl", hash = "sha256:8de5b61163aee3d05c8a2beab6f47913df7981dad1baf82c414d99158c286ab1", size = 211764, upload-time = "2026-05-10T18:02:29.538Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "croniter" +version = "2.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "pytz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/70/240ea4efb2e03b513d820e8da48b784258cbc88770b4b061931cf0269cf8/croniter-2.0.7.tar.gz", hash = "sha256:1041b912b4b1e03751a0993531becf77851ae6e8b334c9c76ffeffb8f055f53f", size = 49655, upload-time = "2024-07-16T15:21:23.156Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/1b/62e5ef33bb8cd599004b85f1ae648aaaf329ec396a8e210906e3f04df66a/croniter-2.0.7-py2.py3-none-any.whl", hash = "sha256:f15e80828d23920c4bb7f4d9340b932c9dcabecafc7775703c8b36d1253ed526", size = 21201, upload-time = "2024-07-16T15:21:20.433Z" }, +] + +[[package]] +name = "harmont" +version = "0.0.0.dev0" +source = { editable = "." } +dependencies = [ + { name = "croniter" }, +] + +[package.optional-dependencies] +dev = [ + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "croniter", specifier = ">=1.4,<3" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.8" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.4" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.2" }, +] +provides-extras = ["dev"] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "librt" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/08/9e7f6b5d2b5bed6ad055cdd5925f192bb403a51280f86b56554d9d0699a2/librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1", size = 200139, upload-time = "2026-05-10T18:17:25.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/87/2bf31fe17587b29e3f93ec31421e2b1e1c3e349b8bf6c7c313dbad1d5340/librt-0.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:93d95bd45b7d58343d8b90d904450a545144eec19a002511163426f8ab1fae29", size = 141092, upload-time = "2026-05-10T18:15:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/cf/08/5c5bf772920b7ebac6e32bc91a643e0ab3870199c0b542356d3baa83970a/librt-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ee278c769a713638cdacd4c0436d72156e75df3ebc0166ab2b9dc43acc386c9", size = 142035, upload-time = "2026-05-10T18:15:36.242Z" }, + { url = "https://files.pythonhosted.org/packages/06/20/662a03d254e5b000d838e8b345d83303ddb768c080fd488e40634c0fa66b/librt-0.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f230cb1cbc9faaa616f9a678f530ebcf186e414b6bcbd88b960e4ba1b92428d5", size = 475022, upload-time = "2026-05-10T18:15:37.56Z" }, + { url = "https://files.pythonhosted.org/packages/de/f3/aa81523e45184c6ec23dc7f63263362ec55f80a09d424c012359ecbe7e35/librt-0.11.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5d63c855d86938d9de93e265c9bd8c705b51ec494de5738340ee93767a686e4b", size = 467273, upload-time = "2026-05-10T18:15:39.182Z" }, + { url = "https://files.pythonhosted.org/packages/6b/6f/59c74b560ca8853834d5501d589c8a2519f4184f273a085ffd0f37a1cc47/librt-0.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f028be9e96a08d31df3479ac80d99be374d17f3b78e4796b3fd3c913d4e89", size = 497083, upload-time = "2026-05-10T18:15:40.634Z" }, + { url = "https://files.pythonhosted.org/packages/fe/7b/5aa4d2c9600a719401160bf7055417df0b2a47439b9d88286ce45e56b65f/librt-0.11.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:258d73a0aa66a055e65b2e4d1b8cdb23b9d132c5bb915d9547d804fcaed116cc", size = 489139, upload-time = "2026-05-10T18:15:41.934Z" }, + { url = "https://files.pythonhosted.org/packages/d6/31/9143803d7da6856a69153785768c4936864430eec0fd9461c3ea527d9922/librt-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0827efe7854718f04aaddf6496e96960a956e676fe1d0f04eb41511fd8ad06d5", size = 508442, upload-time = "2026-05-10T18:15:43.206Z" }, + { url = "https://files.pythonhosted.org/packages/2f/5a/bce08184488426bda4ccc2c4964ac048c8f68ae89bd7120082eef4233cfd/librt-0.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7753e57d6e12d019c0d8786f1c09c709f4c3fcc57c3887b24e36e6c06ec938b7", size = 514230, upload-time = "2026-05-10T18:15:44.761Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/bb5e213d254b7505a0e658da199d8ab719086632ce09eef311ab27976523/librt-0.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11bd19822431cc21af9f27374e7ae2e58103c7d98bda823536a6c47f6bb2bb3d", size = 494231, upload-time = "2026-05-10T18:15:46.308Z" }, + { url = "https://files.pythonhosted.org/packages/9d/fb/541cdad5b1ab1300398c74c4c9a497b88e5074c21b1244c8f49731d3a284/librt-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:22bdf239b219d3993761a148ffa134b19e52e9989c84f845d5d7b71d70a17412", size = 537585, upload-time = "2026-05-10T18:15:47.629Z" }, + { url = "https://files.pythonhosted.org/packages/8f/f2/464bb69295c320cb06bddb4f14a4ec67934ee14b2bffb12b19fb7ab287ba/librt-0.11.0-cp311-cp311-win32.whl", hash = "sha256:46c60b61e308eb535fbd6fa622b1ee1bb2815691c1ad9c98bf7b84952ec3bc8d", size = 100509, upload-time = "2026-05-10T18:15:49.157Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e7/a17ee1788f9e4fbf548c19f4afa07c92089b9e24fef6cb2410863781ef4c/librt-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:902e546ff044f579ff1c953ff5fce97b636fe9e3943996b2177710c6ef076f73", size = 118628, upload-time = "2026-05-10T18:15:50.345Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c7/6c766214f9f9903bcfcfbef97d807af8d8f5aa3502d247858ab17582d212/librt-0.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:65ac3bc20f78aa0ee5ae84baa68917f89fef4af63e941084dd019a0d0e749f0c", size = 103122, upload-time = "2026-05-10T18:15:52.068Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d0/07c77e067f0838949b43bd89232c29d72efebb9d2801a9750184eb706b71/librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46", size = 144147, upload-time = "2026-05-10T18:15:53.227Z" }, + { url = "https://files.pythonhosted.org/packages/7a/24/8493538fa4f62f982686398a5b8f68008138a75086abdea19ade64bf4255/librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3", size = 143614, upload-time = "2026-05-10T18:15:54.657Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1e/f8bad050810d9171f34a1648ed910e56814c2ba61639f2bd53c6377ae24b/librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67", size = 485538, upload-time = "2026-05-10T18:15:56.117Z" }, + { url = "https://files.pythonhosted.org/packages/c0/fe/3594ebfbaf03084ba4b120c9ba5c3183fd938a48725e9bbe6ff0a5159ad8/librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a", size = 479623, upload-time = "2026-05-10T18:15:57.544Z" }, + { url = "https://files.pythonhosted.org/packages/b0/da/5d1876984b3746c85dbd219dbfcb73c85f54ee263fd32e5b2a632ec14571/librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a", size = 513082, upload-time = "2026-05-10T18:15:58.805Z" }, + { url = "https://files.pythonhosted.org/packages/19/6e/55bdf5d5ca00c3e18430690bf2c953d8d3ffd3c337418173d33dec985dc9/librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f", size = 508105, upload-time = "2026-05-10T18:16:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/f1f23a7c595ee90ece4d35c851e5d104b1311a887ed1b4ac4c35bbd13da8/librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b", size = 522268, upload-time = "2026-05-10T18:16:01.708Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/5720f5697a7f54b78b3aefbe20df3a48cedcff1276618c4aa481177942ed/librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766", size = 527348, upload-time = "2026-05-10T18:16:03.496Z" }, + { url = "https://files.pythonhosted.org/packages/50/db/b4a47c6f91db4ff76348a0b3dd0cc65e090a078b765a810a62ff9434c3d3/librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d", size = 516294, upload-time = "2026-05-10T18:16:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/9e/58/9384b2f4eb1ed1d273d40948a7c5c4b2360213b402ef3be4641c06299f9c/librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8", size = 553608, upload-time = "2026-05-10T18:16:06.839Z" }, + { url = "https://files.pythonhosted.org/packages/21/7b/5aa8848a7c6a9278c79375146da1812e695754ceec5f005e6043461a7315/librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a", size = 101879, upload-time = "2026-05-10T18:16:08.103Z" }, + { url = "https://files.pythonhosted.org/packages/37/33/8a745436944947575b584231750a41417de1a38cf6a2e9251d1065651c09/librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9", size = 119831, upload-time = "2026-05-10T18:16:09.174Z" }, + { url = "https://files.pythonhosted.org/packages/59/67/a6739ac96e28b7855808bdb0370e250606104a859750d209e5a0716fe7ab/librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c", size = 103470, upload-time = "2026-05-10T18:16:10.369Z" }, + { url = "https://files.pythonhosted.org/packages/82/61/e59168d4d0bf2bf90f4f0caf7a001bfc60254c3af4586013b04dc3ef517b/librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894", size = 144119, upload-time = "2026-05-10T18:16:11.771Z" }, + { url = "https://files.pythonhosted.org/packages/61/fd/caa1d60b12f7dd79ccea23054e06eeaebe266a5f52c40a6b651069200ce5/librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c", size = 143565, upload-time = "2026-05-10T18:16:13.334Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a9/dc744f5c2b4978d48db970be29f22716d3413d28b14ad99740817315cf2c/librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea", size = 485395, upload-time = "2026-05-10T18:16:14.729Z" }, + { url = "https://files.pythonhosted.org/packages/8f/21/7f8e97a1e4dae952a5a95948f6f8507a173bc1e669f54340bba6ca1ca31b/librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230", size = 479383, upload-time = "2026-05-10T18:16:16.321Z" }, + { url = "https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2", size = 513010, upload-time = "2026-05-10T18:16:17.647Z" }, + { url = "https://files.pythonhosted.org/packages/f0/43/0b5708af2bd30a46400e72ba6bdaa8f066f15fb9a688527e34220e8d6c06/librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3", size = 508433, upload-time = "2026-05-10T18:16:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/4a/50/356187247d09013490481033183b3532b58acf8028bcb34b2b56a375c9b2/librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21", size = 522595, upload-time = "2026-05-10T18:16:20.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/c6ac4240899c7f3248079d5a9900debe0dadb3fdeaf856684c987105ba47/librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930", size = 527255, upload-time = "2026-05-10T18:16:22.352Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b5/a81322dbeedeeaf9c1ee6f001734d28a09d8383ac9e6779bc24bbd0743c6/librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be", size = 516847, upload-time = "2026-05-10T18:16:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/ae/66/6e6323787d592b55204a42595ff1102da5115601b53a7e9ddebc889a6da5/librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e", size = 553920, upload-time = "2026-05-10T18:16:25.025Z" }, + { url = "https://files.pythonhosted.org/packages/9c/21/623f8ca230857102066d9ca8c6c1734995908c4d0d1bee7bb2ef0021cb33/librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e", size = 101898, upload-time = "2026-05-10T18:16:26.649Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47", size = 119812, upload-time = "2026-05-10T18:16:27.859Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e4/b2f4ca7965ca373b491cdb4bc25cdb30c1649ca81a8782056a83850292a9/librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44", size = 103448, upload-time = "2026-05-10T18:16:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/29/eb/dbce197da4e227779e56b5735f2decc3eb36e55a1cdbf1bd65d6639d76c1/librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd", size = 143345, upload-time = "2026-05-10T18:16:30.674Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/254bebd0c11c8ba684018efb8006ff22e466abce445215cca6c778e7d9de/librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4", size = 143131, upload-time = "2026-05-10T18:16:32.037Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3f/f77d6122d21ac7bf6ae8a7dfced1bd2a7ac545d3273ebdcaf8042f6d619f/librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8", size = 477024, upload-time = "2026-05-10T18:16:33.493Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0a/2c996dadebaa7d9bbbd43ef2d4f3e66b6da545f838a41694ef6172cebec8/librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b", size = 474221, upload-time = "2026-05-10T18:16:34.864Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7e/f5d92af8486b8272c23b3e686b46ff72d89c8169585eb61eef01a2ac7147/librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175", size = 505174, upload-time = "2026-05-10T18:16:36.705Z" }, + { url = "https://files.pythonhosted.org/packages/af/1a/cb0734fe86398eb33193ab753b7326255c74cac5eb09e76b9b16536e7adb/librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03", size = 497216, upload-time = "2026-05-10T18:16:38.418Z" }, + { url = "https://files.pythonhosted.org/packages/18/06/094820f91558b66e29943c0ec41c9914f460f48dd51fc503c3101e10842d/librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c", size = 513921, upload-time = "2026-05-10T18:16:39.848Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c2/00de9018871a282f530cacb457d5ec0428f6ac7e6fedde9aff7468d9fb04/librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3", size = 520850, upload-time = "2026-05-10T18:16:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/51/9d/64631832348fd1834fb3a61b996434edddaaf25a31d03b0a76273159d2cf/librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96", size = 504237, upload-time = "2026-05-10T18:16:43.15Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ec/ae5525eb16edc827a044e7bb8777a455ff95d4bca9379e7e6bddd7383647/librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe", size = 546261, upload-time = "2026-05-10T18:16:44.408Z" }, + { url = "https://files.pythonhosted.org/packages/5a/09/adce371f27ca039411da9659f7430fcc2ba6cd0c7b3e4467a0f091be7fa9/librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f", size = 96965, upload-time = "2026-05-10T18:16:46.039Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ee/8ac720d98548f173c7ce2e632a7ca94673f74cacd5c8162a84af5b35958a/librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7", size = 115151, upload-time = "2026-05-10T18:16:47.133Z" }, + { url = "https://files.pythonhosted.org/packages/94/20/c900cf14efeb09b6bef2b2dff20779f73464b97fd58d1c6bccc379588ae3/librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1", size = 98850, upload-time = "2026-05-10T18:16:48.597Z" }, + { url = "https://files.pythonhosted.org/packages/0c/71/944bfe4b64e12abffcd3c15e1cce07f72f3d55655083786285f4dedeb532/librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72", size = 151138, upload-time = "2026-05-10T18:16:49.839Z" }, + { url = "https://files.pythonhosted.org/packages/b6/10/99e64a5c86989357fda078c8143c533389585f6473b7439172dd8f3b3b2d/librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa", size = 151976, upload-time = "2026-05-10T18:16:51.062Z" }, + { url = "https://files.pythonhosted.org/packages/21/31/5072ad880946d83e5ea4147d6d018c78eefce85b77819b19bdd0ee229435/librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548", size = 557927, upload-time = "2026-05-10T18:16:52.632Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8d/70b5fb7cfbab60edbe7381614ab985da58e144fbf465c86d44c95f43cdca/librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2", size = 539698, upload-time = "2026-05-10T18:16:53.934Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a3/ba3495a0b3edbd24a4cae0d1d3c64f39a9fc45d06e812101289b50c1a619/librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f", size = 577162, upload-time = "2026-05-10T18:16:55.589Z" }, + { url = "https://files.pythonhosted.org/packages/f7/db/36e25fb81f99937ff1b96612a1dc9fd66f039cb9cc3aee12c01fac31aab9/librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51", size = 566494, upload-time = "2026-05-10T18:16:56.975Z" }, + { url = "https://files.pythonhosted.org/packages/33/0d/3f622b47f0b013eeb9cf4cc07ae9bfe378d832a4eec998b2b209fe84244d/librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2", size = 596858, upload-time = "2026-05-10T18:16:58.374Z" }, + { url = "https://files.pythonhosted.org/packages/a9/02/71b90bc93039c46a2000651f6ad60122b114c8f54c4ad306e0e96f5b75ad/librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085", size = 590318, upload-time = "2026-05-10T18:16:59.676Z" }, + { url = "https://files.pythonhosted.org/packages/04/04/418cb3f75621e2b761fb1ab0f017f4d70a1a72a6e7c74ee4f7e8d198c2f3/librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3", size = 575115, upload-time = "2026-05-10T18:17:01.007Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2c/5a2183ac58dd911f26b5d7e7d7d8f1d87fcecdddd99d6c12169a258ff62c/librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd", size = 617918, upload-time = "2026-05-10T18:17:02.682Z" }, + { url = "https://files.pythonhosted.org/packages/15/1f/dc6771a52592a4451be6effa200cbfc9cec61e4393d3033d81a9d307961d/librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8", size = 103562, upload-time = "2026-05-10T18:17:03.99Z" }, + { url = "https://files.pythonhosted.org/packages/62/4a/7d1415567027286a75ba1093ec4aca11f073e0f559c530cf3e0a757ad55c/librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c", size = 124327, upload-time = "2026-05-10T18:17:05.465Z" }, + { url = "https://files.pythonhosted.org/packages/ce/62/b40b382fa0c66fee1478073eb8db352a4a6beda4a1adccf1df911d8c289c/librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253", size = 102572, upload-time = "2026-05-10T18:17:06.809Z" }, +] + +[[package]] +name = "mypy" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ast-serialize" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/15/cca9d88503549ed6fedeaa1d448cdddd542ee8a490232d732e278036fbf2/mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633", size = 3898359, upload-time = "2026-05-11T18:37:36.237Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/a1/639f3024794a2a15899cb90707fe02e044c4412794c39c5769fd3df2e2ef/mypy-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a683016b16fe2f572dc04c72be7ee0504ac1605a265d0200f5cea695fb788f41", size = 14691685, upload-time = "2026-05-11T18:33:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/3b/08/9a585dea4325f20d8b80dc78623fa50d1fd2173b710f6237afd6ba6ab39b/mypy-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a293c534adb55271fef24a26da04b855540a8c13cc07bc5917b9fd2c394f2ca", size = 13555165, upload-time = "2026-05-11T18:32:16.107Z" }, + { url = "https://files.pythonhosted.org/packages/81/dc/7c42cc9c6cb01e8eb09961f1f738741d3e9c7e9d5c5b30ec69222625cd5f/mypy-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7406f4d048e71e576f5356d317e5b0a9e666dfd966bd99f9d14ca06e1a341538", size = 13994376, upload-time = "2026-05-11T18:32:39.256Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/285946c33bce716e082c11dfeee9ee196eaf1f5042efb3581a31f9f205e4/mypy-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0210d626fc8b31ccc90233754c7bc90e1f43205e85d96387f7db1285b55c398", size = 14864618, upload-time = "2026-05-11T18:34:49.765Z" }, + { url = "https://files.pythonhosted.org/packages/2b/83/82397f48af6c27e295d57979ded8490c9829040152cf7571b2f026aeb9a0/mypy-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3712c20deed54e814eaaa825603bada8ea1c390670a397c95b98405347acc563", size = 15102063, upload-time = "2026-05-11T18:34:05.855Z" }, + { url = "https://files.pythonhosted.org/packages/40/68/b02dec39057b88eb03dc0aa854732e26e8361f34f9d0e20c7614967d1eba/mypy-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fcaa0e479066e31f7cceb6a3bea39cb22b2ff51a6b2f24f193d19179ba17c389", size = 11060564, upload-time = "2026-05-11T18:35:36.494Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a8/ea3dcbef31f99b634f2ee23bb0321cbc8c1b388b76a861eb849f13c347dc/mypy-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:0b1a5260c95aa443083f9ed3592662941951bca3d4ca224a5dc517c38b7cf666", size = 9966983, upload-time = "2026-05-11T18:37:14.139Z" }, + { url = "https://files.pythonhosted.org/packages/95/b1/55861beb5c339b44f9a2ba92df9e2cb1eeb4ae1eee674cdf7772c797778b/mypy-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:244358bf1c0da7722230bce60683d52e8e9fd030554926f15b747a84efb5b3af", size = 14874381, upload-time = "2026-05-11T18:37:31.784Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b3/b7f770114b7d0ac92d0f76e8d93c2780844a70488a90e91821927850da86/mypy-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ec7c57657493c7a75534df2751c8ae2cda383c16ecc55d2106c54476b1b16f6", size = 13665501, upload-time = "2026-05-11T18:34:23.063Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f3/8ae2037967e2126689a0c11d99e2b707134a565191e92c60ca2572aec60a/mypy-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8161b6ff4392410023224f0969d17db93e1e154bc3e4ba62598e720723ae211", size = 14045750, upload-time = "2026-05-11T18:31:48.151Z" }, + { url = "https://files.pythonhosted.org/packages/a0/32/615eb5911859e43d054941b0d0a7d06cfa2870eba86529cf385b052b111c/mypy-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf03e12003084a67395184d3eb8cbd6a489dc3655b5664b28c210a9e2403ab0b", size = 15061630, upload-time = "2026-05-11T18:37:06.898Z" }, + { url = "https://files.pythonhosted.org/packages/d4/03/4eafbfff8bfab1b87082741eae6e6a624028c984e6708b73bce2a8570c9d/mypy-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:20509760fd791c51579d573153407d226385ec1f8bcce55d730b354f3336bc22", size = 15288831, upload-time = "2026-05-11T18:31:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/919661478e5891a3c96e549c036e467e64563ab85995b10c53c8358e16a3/mypy-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:6753d0c1fdd6b1a23b9e4f283ce80b2153b724adcb2653b20b85a8a28ac6436b", size = 11135228, upload-time = "2026-05-11T18:34:31.23Z" }, + { url = "https://files.pythonhosted.org/packages/24/0a/6a12b9782ca0831a553192f351679f4548abc9d19a7cc93bb7feb02084c7/mypy-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:98ebb6589bb3b6d0c6f0c459d53ca55b8091fbc13d277c4041c885392e8195e8", size = 10040684, upload-time = "2026-05-11T18:36:48.199Z" }, + { url = "https://files.pythonhosted.org/packages/6e/dd/c7191469c777f07689c032a8f7326e393ea34c92d6d76eb7ce5ba57ea66d/mypy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35aac3bb114e03888f535d5eb51b8bafbb3266586b599da1940f9b1be3ec5bd5", size = 14852174, upload-time = "2026-05-11T18:31:38.929Z" }, + { url = "https://files.pythonhosted.org/packages/55/8c/aed55408879043d72bb9135f4d0d19a02b886dd569631e113e3d2706cb8d/mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de55a8c861f2a49331f807be98d90caeceeef520bde13d43a160207f8af613e", size = 13651542, upload-time = "2026-05-11T18:36:04.636Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8e/f371a824b1f1fa8ea6e3dbb8703d232977d572be2329554a3bc4d960302f/mypy-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fdf2941a07434af755837d9880f7d7d25f1dacb1af9dcd4b9b66f2220a3024e", size = 14033929, upload-time = "2026-05-11T18:35:55.742Z" }, + { url = "https://files.pythonhosted.org/packages/94/21/f54be870d6dd53a82c674407e0f8eed7174b05ec78d42e5abd7b42e84fd5/mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e195b817c13f02352a9c124301f9f30f078405444679b6753c1b96b6eed37285", size = 15039200, upload-time = "2026-05-11T18:33:10.281Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/bf21748626a40ce59fd29a39386ab46afec88b7bd2f0fa6c3a97c995523f/mypy-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5431d42af987ebd92ba2f71d45c85ed41d8e6ca9f5fd209a69f68f707d2469e5", size = 15272690, upload-time = "2026-05-11T18:32:07.205Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d7/9e90d2cf47100bea550ed2bc7b0d4de3a62181d84d5e37da0003e8462637/mypy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:767fe8c66dc3e01e19e1737d4c38ebefead16125e1b8e58ad421903b376f5c65", size = 11147435, upload-time = "2026-05-11T18:33:56.477Z" }, + { url = "https://files.pythonhosted.org/packages/ec/46/e5c449e858798e35ffc90946282a27c62a77be743fe17480e4977374eb91/mypy-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:ecfe70d43775ab99562ab128ce49854a362044c9f894961f68f898c23cb7429d", size = 10035052, upload-time = "2026-05-11T18:32:30.049Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ca/b279a672e874aedd5498ae25f722dacc8aa86bbffb939b3f97cbb1cf6686/mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2", size = 14848422, upload-time = "2026-05-11T18:35:45.984Z" }, + { url = "https://files.pythonhosted.org/packages/27/e6/3efe56c631d959b9b4454e208b0ac4b7f4f58b404c89f8bec7b49efdfc21/mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f", size = 13677374, upload-time = "2026-05-11T18:36:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/84/7f/8107ea87a44fd1f1b59882442f033c9c3488c127201b1d1d15f1cbd6022e/mypy-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:761be68e023ef5d94678772396a8af1220030f80837a3afd8d0aef3b419666f4", size = 14055743, upload-time = "2026-05-11T18:35:18.361Z" }, + { url = "https://files.pythonhosted.org/packages/51/4d/b6d34db183133b83761b9199a82d31557cdbb70a380d8c3b3438e11882a3/mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef", size = 15020937, upload-time = "2026-05-11T18:34:59.618Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d7/f08360c691d758acb02f45022c34d98b92892f4ea756644e1000d4b9f3d8/mypy-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b84802e7b5a6daf1f5e15bc9fcd7ddae77be13981ffab037f1c67bb84d67d135", size = 15253371, upload-time = "2026-05-11T18:36:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/67/1b/09460a13719530a19bce27bd3bc8449e83569dd2ba7faf51c9c3c30c0b61/mypy-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:022c771234936ceac541ebaf836fe9e2abeb3f5e09aff21588fe543ff006fe21", size = 11326429, upload-time = "2026-05-11T18:34:13.526Z" }, + { url = "https://files.pythonhosted.org/packages/40/62/75dbf0f82f7b6680340efc614af29dd0b3c17b8a4f1cd09b8bd2fd6bc814/mypy-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:498207db725cec88829a6a5c2fc771205fd043719ef98bc49aba8fb9fc4e6d57", size = 10218799, upload-time = "2026-05-11T18:32:23.491Z" }, + { url = "https://files.pythonhosted.org/packages/b2/66/caca04ed7d972fb6eb6dd1ccd6df1de5c38fae8c5b3dc1c4e8e0d85ee6b9/mypy-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d5e5cad0efeba72b93cd17490cc0d69c5ac9ca132994fe3fb0314808aeeb83e", size = 15923458, upload-time = "2026-05-11T18:35:28.64Z" }, + { url = "https://files.pythonhosted.org/packages/ed/52/2d90cbe49d014b13ed7ff337930c30bad35893fe38a1e4641e756bb62191/mypy-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff715050c127d724fd260a2e666e7747fdd83511c0c47d449d98238970aef780", size = 14757697, upload-time = "2026-05-11T18:36:14.208Z" }, + { url = "https://files.pythonhosted.org/packages/ac/37/d98f4a14e081b238992d0ed96b6d39c7cc0148c9699eb71eaa68629665ea/mypy-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82208da9e09414d520e912d3e462d454854bed0810b71540bb016dcbca7308fd", size = 15405638, upload-time = "2026-05-11T18:33:48.249Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c2/15c46613b24a84fad2aea1248bf9619b99c2767ae9071fe224c179a0b7d4/mypy-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e79ebc1b904b84f0310dff7469655a9c36c7a68bddb37bdd42b67a332df61d08", size = 16215852, upload-time = "2026-05-11T18:32:50.296Z" }, + { url = "https://files.pythonhosted.org/packages/5c/90/9c16a57f482c76d25f6379762b56bbf65c711d8158cf271fb2802cfb0640/mypy-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e583edc957cfb0deb142079162ae826f58449b116c1d442f2d91c69d9fced081", size = 16452695, upload-time = "2026-05-11T18:33:38.182Z" }, + { url = "https://files.pythonhosted.org/packages/0f/4c/215a4eeb63cacc5f17f516691ea7285d11e249802b942476bff15922a314/mypy-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b33b6cd332695bba180d55e717a79d3038e479a2c49cc5eb3d53603409b9a5d7", size = 12866622, upload-time = "2026-05-11T18:34:39.945Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/1043e1db5f455ffe4c9ab22747cd8ca2bc492b1e4f4e21b130a44ee2b217/mypy-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4f910fe825376a7b66ef7ca8c98e5a149e8cd64c19ae71d84047a74ee060d4e6", size = 10610798, upload-time = "2026-05-11T18:36:31.444Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2a/13ca1f292f6db1b98ff495ef3467736b331621c5917cad984b7043e7348d/mypy-2.1.0-py3-none-any.whl", hash = "sha256:a663814603a5c563fb87a4f96fb473eeb30d1f5a4885afcf44f9db000a366289", size = 2693302, upload-time = "2026-05-11T18:31:29.246Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pytz" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/46/dd499ec9038423421951e4fad73051febaa13d2df82b4064f87af8b8c0c3/pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a", size = 320861, upload-time = "2026-05-04T01:35:29.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126", size = 510141, upload-time = "2026-05-04T01:35:27.408Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/8a/8bce2894573e9dae6ff4d77fe34ad727d79b9e6238ad288c5638990d90f6/ruff-0.15.14.tar.gz", hash = "sha256:48e866b165be4a9bdbf310f7d3c9a07edef2fe8cd63ffeb4e00bb590506ebf9f", size = 4700910, upload-time = "2026-05-21T14:34:55.177Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/c8/74a92c6ff9fcfb4f1f947126d3ebee8389276e161ecc85de5bda7cda51bd/ruff-0.15.14-py3-none-linux_armv6l.whl", hash = "sha256:8dd2db9416e487c8d4b01fa7056bb02c4d05969d4f8d17a08c229c2f4ff3c108", size = 10739177, upload-time = "2026-05-21T14:34:37.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/254a35c20acc38a7223c9d2d594af12e794432464f2cdeb52af1dc4a892d/ruff-0.15.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:be4ff55af755bd71a00ab3dc6bd7ffc467bd76e0df6881e286c2e3d23e8fb43b", size = 11144969, upload-time = "2026-05-21T14:34:43.978Z" }, + { url = "https://files.pythonhosted.org/packages/56/9e/d13e40f83b8d0a94430e6778ce1d94a43b38cf2efe63278bdd2b4c65abbf/ruff-0.15.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:48d5909d7d06276ce7dde6d32bfa4b0d4cb2651145cd8ee4b440722cbc77832f", size = 10478207, upload-time = "2026-05-21T14:34:48.378Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f1/b15a7839fa4f332f8acec78e20564f26bb2d866e3d21710b877fd0263000/ruff-0.15.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca8cbfa94c4f90984a67561978602746d4cd27103568f745fa90eee3f0d4107d", size = 10818459, upload-time = "2026-05-21T14:34:22.318Z" }, + { url = "https://files.pythonhosted.org/packages/45/33/53d651177f84f94b400a0e27f8824eeada3dddc9d5ee8aeb048f4352a520/ruff-0.15.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a6bbc0333f1ab053423bcbf6226477d266ca7cec7738c4c8e3f55647803f3c4", size = 10541800, upload-time = "2026-05-21T14:34:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/868f87e0bf9786ed24b5d0d0ad8676b8a94fd1912f42cddf9cfc7857818a/ruff-0.15.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a24a4f7605d7003a6674d4387651effd939dead3fddd0f36561eb77a9a2e542", size = 11342149, upload-time = "2026-05-21T14:34:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/a7/8b/38cd5c19faffdcc05a408d2b78edccc69492ab9720eadb49ea15ef80d768/ruff-0.15.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:049b5326e53ed80978f2fc041a280603f69dd6b0c95464342a2bb4572d9d9e2f", size = 12212563, upload-time = "2026-05-21T14:34:28.579Z" }, + { url = "https://files.pythonhosted.org/packages/3e/4d/a3c5b874a556d5731e3e657aaf04311bb76f0a5c3ec220ed43051be6b64b/ruff-0.15.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4ed42e6696c8dfa5f06728e6441993901f548eb92d73bc472cb5a38d1395fbf", size = 11493299, upload-time = "2026-05-21T14:34:41.836Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c0/56472c251d09858a53e51efbd485b09e1995d8731668b76d52e5dd6ee0f1/ruff-0.15.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:715c543cf450c4888251f91c52f1942a800541d9bddd7ac060aa4e6b77ae7cba", size = 11455931, upload-time = "2026-05-21T14:34:57.276Z" }, + { url = "https://files.pythonhosted.org/packages/2c/4a/e2e7b4d8dbf233d4eace59c75bc3435fa6d8bd3bae82d351d4e4300c0fd1/ruff-0.15.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ebab6013ec887d439d8b7593737a0a4ffb06d45d209d4e4bf2e92813082d3f", size = 11400794, upload-time = "2026-05-21T14:34:39.773Z" }, + { url = "https://files.pythonhosted.org/packages/97/c7/83c0539fe34c3e09136204d1e75d6052492364e0b3cb05e9465423f567d7/ruff-0.15.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:49072d36abdbe97a8dd7f480afe9c675699c0c495d4c84076e2c1203c4550581", size = 10804759, upload-time = "2026-05-21T14:34:31.045Z" }, + { url = "https://files.pythonhosted.org/packages/86/a6/18f2bfc095a2ab4a78745644e428205532ce6653a5d0fa8501572891534d/ruff-0.15.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:958522aee105068640c2c2ceae08f413ae44d922f52a1374ac13d6a96032fc93", size = 10539517, upload-time = "2026-05-21T14:34:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/54/3a/5a8b3b69c654d4e4bf1d246ac5b49cbcdac6eaab6905925f8915f31e3b80/ruff-0.15.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f3707da619a143a2e8830e2abab8224478d69ace2d28cb6c20543ae97c36bf61", size = 11065169, upload-time = "2026-05-21T14:34:24.484Z" }, + { url = "https://files.pythonhosted.org/packages/ed/c5/8864e4e7925b836ea354b31d57641ec03830564e281a8b6f061f8c3e0ec1/ruff-0.15.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bb01d645694e3ec0102105d07ef2d53703970407d59c04e59d3ba0b7a1d53553", size = 11560214, upload-time = "2026-05-21T14:34:50.975Z" }, + { url = "https://files.pythonhosted.org/packages/36/38/012bf76752e1f89ed50b77b99532d90f3a3e287bc7918e1fc0948ac866ac/ruff-0.15.14-py3-none-win32.whl", hash = "sha256:6d0c1ad2a0ab718d39b6d8fd2217981ce4d625cd96a720095f798fb47d8b13e6", size = 10805548, upload-time = "2026-05-21T14:34:33.453Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/4ea2c170f10ad760fff2a5250beb18897719dc8b52b53a24cddbb9dd3f19/ruff-0.15.14-py3-none-win_amd64.whl", hash = "sha256:802342981e056db3851a7836e5b070f8f15f67d4a685ae2a6160939d364b2902", size = 11939523, upload-time = "2026-05-21T14:34:18.077Z" }, + { url = "https://files.pythonhosted.org/packages/62/d5/bc97ff895ec35cf3925d4bd60f3b39d822f377a446906ec9bcc87405e59b/ruff-0.15.14-py3-none-win_arm64.whl", hash = "sha256:ff47b90a9ef6a40c9e2f3b479c1fb78531adf055b94c1eba0a7ba04b31951826", size = 11208607, upload-time = "2026-05-21T14:34:26.525Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] diff --git a/dsls/harmont-ts/src/toolchains/py/uv.ts b/dsls/harmont-ts/src/toolchains/py/uv.ts index fe72496..1ff803e 100644 --- a/dsls/harmont-ts/src/toolchains/py/uv.ts +++ b/dsls/harmont-ts/src/toolchains/py/uv.ts @@ -10,6 +10,11 @@ const APT_PACKAGES = [ ] as const; const VERSION_RE = /^([0-9]+\.[0-9]+\.[0-9]+|latest)$/; +function resolvePaths(paths?: string | string[]): string { + if (paths == null) return "."; + return Array.isArray(paths) ? paths.join(" ") : paths; +} + export interface UvOptions { readonly path?: string; readonly version?: string; @@ -53,10 +58,12 @@ export class UvProject { ); } - typecheck(opts?: ActionOptions): Step { - return this._installed.sh(`cd ${this.path} && uv run mypy .`, { + typecheck(opts?: ActionOptions & { paths?: string | string[] }): Step { + const target = resolvePaths(opts?.paths); + const { paths: _, ...rest } = opts ?? {}; + return this._installed.sh(`cd ${this.path} && uv run mypy ${target}`, { label: ":python: typecheck", - ...opts, + ...rest, }); } diff --git a/dsls/harmont-ts/src/toolchains/python.ts b/dsls/harmont-ts/src/toolchains/python.ts index 78fc144..ea7001e 100644 --- a/dsls/harmont-ts/src/toolchains/python.ts +++ b/dsls/harmont-ts/src/toolchains/python.ts @@ -10,6 +10,11 @@ const APT_PACKAGES = [ ] as const; const VERSION_RE = /^([0-9]+\.[0-9]+\.[0-9]+|latest)$/; +function resolvePaths(paths?: string | string[]): string { + if (paths == null) return "."; + return Array.isArray(paths) ? paths.join(" ") : paths; +} + export interface PythonOptions { readonly path?: string; readonly uvVersion?: string; @@ -53,10 +58,12 @@ export class PythonToolchain { ); } - typecheck(opts?: ActionOptions): Step { - return this._installed.sh(`cd ${this.path} && uv run mypy .`, { + typecheck(opts?: ActionOptions & { paths?: string | string[] }): Step { + const target = resolvePaths(opts?.paths); + const { paths: _, ...rest } = opts ?? {}; + return this._installed.sh(`cd ${this.path} && uv run mypy ${target}`, { label: ":python: typecheck", - ...opts, + ...rest, }); } } diff --git a/dsls/harmont-ts/tests/toolchains/py/uv.test.ts b/dsls/harmont-ts/tests/toolchains/py/uv.test.ts index ec26c1d..1756974 100644 --- a/dsls/harmont-ts/tests/toolchains/py/uv.test.ts +++ b/dsls/harmont-ts/tests/toolchains/py/uv.test.ts @@ -47,6 +47,18 @@ describe("py.uv actions", () => { expect(p.typecheck()._cmd).toContain("uv run mypy ."); }); + it("typecheck with paths string", () => { + const p = py.uv({ path: "myapp" }); + expect(p.typecheck({ paths: "src" })._cmd).toContain("uv run mypy src"); + }); + + it("typecheck with paths array", () => { + const p = py.uv({ path: "myapp" }); + expect(p.typecheck({ paths: ["src", "tests"] })._cmd).toContain( + "uv run mypy src tests", + ); + }); + it("build runs uv build", () => { const p = py.uv(); expect(p.build()._cmd).toContain("uv build"); diff --git a/dsls/harmont-ts/tests/toolchains/python.test.ts b/dsls/harmont-ts/tests/toolchains/python.test.ts index 7dfbfd4..ff21dd3 100644 --- a/dsls/harmont-ts/tests/toolchains/python.test.ts +++ b/dsls/harmont-ts/tests/toolchains/python.test.ts @@ -47,6 +47,18 @@ describe("python actions", () => { expect(p.typecheck()._cmd).toContain("uv run mypy ."); }); + it("typecheck with paths string", () => { + const p = python({ path: "myapp" }); + expect(p.typecheck({ paths: "src" })._cmd).toContain("uv run mypy src"); + }); + + it("typecheck with paths array", () => { + const p = python({ path: "myapp" }); + expect(p.typecheck({ paths: ["src", "tests"] })._cmd).toContain( + "uv run mypy src tests", + ); + }); + it("actions chain from install (sync step)", () => { const p = python(); expect(p.test()._parent).toBe(p.install()); From d95d290f4a4cceebaade882a2b1c5e19b88f6df5 Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Sun, 24 May 2026 00:38:08 -0700 Subject: [PATCH 09/14] ci: restore fmt() in dogfood pipelines now that formatting is fixed --- .harmont/ci.py | 1 + .harmont/ci.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/.harmont/ci.py b/.harmont/ci.py index 1e3b0e8..309732c 100644 --- a/.harmont/ci.py +++ b/.harmont/ci.py @@ -35,6 +35,7 @@ def ci( rust_project.clippy(), rust_project.fmt(), py_project.lint(), + py_project.fmt(), py_project.typecheck(paths="harmont"), py_project.run( "pytest -v" diff --git a/.harmont/ci.ts b/.harmont/ci.ts index 4052257..8fbd7fa 100644 --- a/.harmont/ci.ts +++ b/.harmont/ci.ts @@ -14,6 +14,7 @@ const pipelines: PipelineDefinition[] = [ rustProject.clippy(), rustProject.fmt(), pyProject.lint(), + pyProject.fmt(), pyProject.typecheck({ paths: "harmont" }), pyProject.run( "pytest -v --deselect tests/test_gradle.py --deselect tests/test_haskell.py", From a2ef5d9de8cdee8d49c2da35d98ec46806a48a65 Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Sun, 24 May 2026 00:41:06 -0700 Subject: [PATCH 10/14] style: cargo fmt across entire workspace --- crates/hm-pipeline-ir/tests/e2e_fixtures.rs | 50 ++++++++------ crates/hm-pipeline-ir/tests/graph_build.rs | 32 ++++++--- crates/hm-pipeline-ir/tests/graph_serde.rs | 33 ++++++++-- crates/hm-plugin-cloud/src/auth/login.rs | 4 +- crates/hm-plugin-cloud/src/auth/whoami.rs | 5 +- crates/hm-plugin-cloud/src/cli.rs | 10 +-- crates/hm-plugin-cloud/src/http.rs | 5 +- crates/hm-plugin-cloud/src/verbs/billing.rs | 13 ++-- crates/hm-plugin-cloud/src/verbs/build.rs | 14 ++-- crates/hm-plugin-cloud/src/verbs/job.rs | 27 ++------ crates/hm-plugin-cloud/src/verbs/org.rs | 8 ++- crates/hm-plugin-cloud/src/verbs/pipeline.rs | 10 +-- crates/hm-plugin-protocol/src/events.rs | 16 ++++- crates/hm-plugin-protocol/src/executor.rs | 31 +++++++-- crates/hm-util/src/lib.rs | 2 +- crates/hm-util/src/os/fs.rs | 17 ++--- crates/hm/src/commands/dev/down.rs | 17 +++-- crates/hm/src/commands/dev/exec.rs | 4 +- crates/hm/src/commands/dev/logmux.rs | 31 ++++++--- crates/hm/src/commands/dev/logs.rs | 4 +- crates/hm/src/commands/dev/ls.rs | 8 ++- crates/hm/src/commands/dev/naming.rs | 9 ++- crates/hm/src/commands/dev/network.rs | 10 +-- crates/hm/src/commands/dev/port_of.rs | 21 ++++-- crates/hm/src/commands/dev/registry.rs | 15 ++--- crates/hm/src/commands/dev/service_spec.rs | 40 +++++------ crates/hm/src/commands/dev/topo.rs | 32 ++++++--- crates/hm/src/commands/dev/up.rs | 32 +++++---- crates/hm/src/commands/run/local.rs | 3 +- crates/hm/src/config.rs | 12 ++-- crates/hm/src/creds_store.rs | 15 ++--- crates/hm/src/orchestrator/docker_client.rs | 23 +++---- crates/hm/src/orchestrator/mod.rs | 6 +- .../hm/src/orchestrator/output_subscriber.rs | 5 +- crates/hm/src/orchestrator/scheduler.rs | 18 +++-- crates/hm/src/runner/docker.rs | 16 +++-- crates/hm/tests/default_image_inheritance.rs | 22 +++++-- crates/hm/tests/dev_integration.rs | 66 ++++++++++++++----- 38 files changed, 411 insertions(+), 275 deletions(-) diff --git a/crates/hm-pipeline-ir/tests/e2e_fixtures.rs b/crates/hm-pipeline-ir/tests/e2e_fixtures.rs index 3258f78..2872c0f 100644 --- a/crates/hm-pipeline-ir/tests/e2e_fixtures.rs +++ b/crates/hm-pipeline-ir/tests/e2e_fixtures.rs @@ -26,10 +26,8 @@ fn fixtures_dir() -> PathBuf { fn load_fixture(dsl: &str, scenario: &str) -> PipelineGraph { let path = fixtures_dir().join(dsl).join(format!("{scenario}.json")); - let bytes = - fs::read(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display())); - serde_json::from_slice(&bytes) - .unwrap_or_else(|e| panic!("parse {dsl}/{scenario}: {e}")) + let bytes = fs::read(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display())); + serde_json::from_slice(&bytes).unwrap_or_else(|e| panic!("parse {dsl}/{scenario}: {e}")) } fn step_labels(g: &PipelineGraph) -> BTreeSet { @@ -61,8 +59,16 @@ fn python_monorepo_ci() { assert!(g.node_count() >= 15, "nodes: {}", g.node_count()); let labels = step_labels(&g); assert!(labels.iter().any(|l| l.contains("go"))); - assert!(labels.iter().any(|l| l.contains("python") || l.contains("uv"))); - assert!(labels.iter().any(|l| l.contains("node") || l.contains("npm"))); + assert!( + labels + .iter() + .any(|l| l.contains("python") || l.contains("uv")) + ); + assert!( + labels + .iter() + .any(|l| l.contains("node") || l.contains("npm")) + ); } #[test] @@ -81,7 +87,11 @@ fn python_zig_node_polyglot() { assert!(g.node_count() >= 10, "nodes: {}", g.node_count()); let labels = step_labels(&g); assert!(labels.iter().any(|l| l.contains("zig"))); - assert!(labels.iter().any(|l| l.contains("node") || l.contains("npm"))); + assert!( + labels + .iter() + .any(|l| l.contains("node") || l.contains("npm")) + ); } #[test] @@ -91,9 +101,17 @@ fn python_kitchen_sink() { assert!(g.node_count() >= 12, "nodes: {}", g.node_count()); let labels = step_labels(&g); assert!(labels.iter().any(|l| l.contains("haskell"))); - assert!(labels.iter().any(|l| l.contains("cmake") || l.contains(":c:"))); + assert!( + labels + .iter() + .any(|l| l.contains("cmake") || l.contains(":c:")) + ); for (_, t) in g.dag().graph().node_references() { - assert!(t.env.contains_key("CI"), "node {} missing CI env", t.step.key); + assert!( + t.env.contains_key("CI"), + "node {} missing CI env", + t.step.key + ); } } @@ -148,11 +166,7 @@ fn all_fixtures_have_valid_structure() { assert!(bi + dep > 0, "{dsl}/{scenario}: no edges"); for e in g.dag().graph().edge_references() { - assert_ne!( - e.source(), - e.target(), - "{dsl}/{scenario}: self-loop", - ); + assert_ne!(e.source(), e.target(), "{dsl}/{scenario}: self-loop",); } } } @@ -197,7 +211,8 @@ fn parity_step_labels() { let py_labels = step_labels(&py); let ts_labels = step_labels(&ts); assert_eq!( - py_labels, ts_labels, + py_labels, + ts_labels, "parity/{scenario}: labels\npy-only: {:?}\nts-only: {:?}", py_labels.difference(&ts_labels).collect::>(), ts_labels.difference(&py_labels).collect::>(), @@ -241,10 +256,7 @@ fn parity_env_keys() { .find(|(_, t)| t.step.label.as_deref() == Some(label)) .map(|(_, t)| t.env.keys().cloned().collect()) .unwrap(); - assert_eq!( - py_env, ts_env, - "parity/{scenario}/{label}: env keys", - ); + assert_eq!(py_env, ts_env, "parity/{scenario}/{label}: env keys",); } } } diff --git a/crates/hm-pipeline-ir/tests/graph_build.rs b/crates/hm-pipeline-ir/tests/graph_build.rs index 3a1ba0f..4a77793 100644 --- a/crates/hm-pipeline-ir/tests/graph_build.rs +++ b/crates/hm-pipeline-ir/tests/graph_build.rs @@ -26,7 +26,8 @@ fn find_by_key<'a>(g: &'a PipelineGraph, key: &str) -> &'a hm_pipeline_ir::Trans #[test] fn builds_simple_chain() { - let g = graph(br#"{ + let g = graph( + br#"{ "version": "0", "default_image": "ubuntu:24.04", "graph": { @@ -41,14 +42,16 @@ fn builds_simple_chain() { [1, 2, "builds_in"] ] } - }"#); + }"#, + ); assert_eq!(g.node_count(), 3); assert_eq!(g.default_image(), Some("ubuntu:24.04")); } #[test] fn root_inherits_default_image() { - let g = graph(br#"{ + let g = graph( + br#"{ "version": "0", "default_image": "ubuntu:24.04", "graph": { @@ -58,14 +61,16 @@ fn root_inherits_default_image() { "edge_property": "directed", "edges": [] } - }"#); + }"#, + ); let t = find_by_key(&g, "a"); assert_eq!(t.step.image.as_deref(), Some("ubuntu:24.04")); } #[test] fn child_does_not_inherit_default_image() { - let g = graph(br#"{ + let g = graph( + br#"{ "version": "0", "default_image": "ubuntu:24.04", "graph": { @@ -78,14 +83,16 @@ fn child_does_not_inherit_default_image() { [0, 1, "builds_in"] ] } - }"#); + }"#, + ); let b = find_by_key(&g, "b"); assert!(b.step.image.is_none()); } #[test] fn wait_inserts_implicit_deps() { - let g = graph(br#"{ + let g = graph( + br#"{ "version": "0", "graph": { "nodes": [ @@ -99,13 +106,18 @@ fn wait_inserts_implicit_deps() { [1, 2, "depends_on"] ] } - }"#); + }"#, + ); let dag = g.dag(); - let c_idx = dag.graph().node_references() + let c_idx = dag + .graph() + .node_references() .find(|(_, t)| t.step.key == "c") .map(|(idx, _)| idx) .unwrap(); - let parent_keys: Vec = dag.parents(c_idx).iter(dag) + let parent_keys: Vec = dag + .parents(c_idx) + .iter(dag) .map(|(_, p)| dag[p].step.key.clone()) .collect(); assert!(parent_keys.contains(&"a".to_string())); diff --git a/crates/hm-pipeline-ir/tests/graph_serde.rs b/crates/hm-pipeline-ir/tests/graph_serde.rs index 52ab54a..06a4f15 100644 --- a/crates/hm-pipeline-ir/tests/graph_serde.rs +++ b/crates/hm-pipeline-ir/tests/graph_serde.rs @@ -34,8 +34,14 @@ fn transition_round_trips() { #[test] fn edge_kind_serializes_as_snake_case() { - assert_eq!(serde_json::to_string(&EdgeKind::BuildsIn).unwrap(), "\"builds_in\""); - assert_eq!(serde_json::to_string(&EdgeKind::DependsOn).unwrap(), "\"depends_on\""); + assert_eq!( + serde_json::to_string(&EdgeKind::BuildsIn).unwrap(), + "\"builds_in\"" + ); + assert_eq!( + serde_json::to_string(&EdgeKind::DependsOn).unwrap(), + "\"depends_on\"" + ); } #[test] @@ -64,7 +70,8 @@ fn build_test_graph() -> PipelineGraph { [0, 1, "builds_in"] ] } - })).unwrap() + })) + .unwrap() } #[test] @@ -77,17 +84,29 @@ fn pipeline_graph_round_trips_through_json() { use daggy::Walker; use daggy::petgraph::visit::IntoNodeReferences; - let a_idx = back.dag().graph().node_references() + let a_idx = back + .dag() + .graph() + .node_references() .find(|(_, t)| t.step.key == "a") .map(|(idx, _)| idx) .unwrap(); - assert_eq!(back.dag()[a_idx].step.image.as_deref(), Some("ubuntu:24.04")); + assert_eq!( + back.dag()[a_idx].step.image.as_deref(), + Some("ubuntu:24.04") + ); - let b_idx = back.dag().graph().node_references() + let b_idx = back + .dag() + .graph() + .node_references() .find(|(_, t)| t.step.key == "b") .map(|(idx, _)| idx) .unwrap(); - let has_builds_in_parent = back.dag().parents(b_idx).iter(back.dag()) + let has_builds_in_parent = back + .dag() + .parents(b_idx) + .iter(back.dag()) .any(|(e, _)| *back.dag().edge_weight(e).unwrap() == EdgeKind::BuildsIn); assert!(has_builds_in_parent); } diff --git a/crates/hm-plugin-cloud/src/auth/login.rs b/crates/hm-plugin-cloud/src/auth/login.rs index 5b6ca81..df77973 100644 --- a/crates/hm-plugin-cloud/src/auth/login.rs +++ b/crates/hm-plugin-cloud/src/auth/login.rs @@ -41,9 +41,7 @@ async fn login_loopback( tracing::info!("opening browser to {auth_url}"); if webbrowser::open(&auth_url).is_err() { - eprintln!( - "couldn't auto-open the browser. Open this URL manually:\n {auth_url}" - ); + eprintln!("couldn't auto-open the browser. Open this URL manually:\n {auth_url}"); } // Wait for a single connection with a 180-second timeout. diff --git a/crates/hm-plugin-cloud/src/auth/whoami.rs b/crates/hm-plugin-cloud/src/auth/whoami.rs index 8845f7e..47080e8 100644 --- a/crates/hm-plugin-cloud/src/auth/whoami.rs +++ b/crates/hm-plugin-cloud/src/auth/whoami.rs @@ -12,10 +12,7 @@ use crate::http::Client; pub(crate) async fn run(env: &BTreeMap) -> Result<()> { let cfg = Config::from_env(env); let token = creds::load_token(&cfg.api_base, env).ok_or_else(|| { - anyhow::anyhow!( - "not logged in to {}\n fix: `hm cloud login`", - cfg.api_base - ) + anyhow::anyhow!("not logged in to {}\n fix: `hm cloud login`", cfg.api_base) })?; let client = Client::new(&cfg, Some(token)); let me: User = client.get("/auth/me").await?; diff --git a/crates/hm-plugin-cloud/src/cli.rs b/crates/hm-plugin-cloud/src/cli.rs index 9cfbe3d..9c7185a 100644 --- a/crates/hm-plugin-cloud/src/cli.rs +++ b/crates/hm-plugin-cloud/src/cli.rs @@ -148,10 +148,7 @@ pub enum BillingCommand { /// Dispatch from raw argv (used if calling from an external-subcommand /// pattern). Returns an exit code. -pub async fn dispatch( - argv: Vec, - env: BTreeMap, -) -> Result { +pub async fn dispatch(argv: Vec, env: BTreeMap) -> Result { let mut full: Vec = vec!["hm cloud".to_string()]; full.extend(argv.into_iter().skip(1)); let parsed = match CloudCli::try_parse_from(&full) { @@ -175,10 +172,7 @@ pub async fn dispatch( } /// Dispatch from a pre-parsed `CloudCommand`. Returns an exit code. -pub async fn dispatch_command( - command: CloudCommand, - env: BTreeMap, -) -> Result { +pub async fn dispatch_command(command: CloudCommand, env: BTreeMap) -> Result { let result = match command { CloudCommand::Login { paste } => auth::login::run(&env, paste).await, CloudCommand::Logout => auth::logout::run(&env).await, diff --git a/crates/hm-plugin-cloud/src/http.rs b/crates/hm-plugin-cloud/src/http.rs index 31421e3..6a70055 100644 --- a/crates/hm-plugin-cloud/src/http.rs +++ b/crates/hm-plugin-cloud/src/http.rs @@ -58,7 +58,10 @@ impl Client { if let Some(b) = body { req = req.header("Content-Type", "application/json").json(b); } - let resp = req.send().await.with_context(|| format!("{method} {url}"))?; + let resp = req + .send() + .await + .with_context(|| format!("{method} {url}"))?; let status = resp.status().as_u16(); if !(200..300).contains(&status) { let text = resp.text().await.unwrap_or_default(); diff --git a/crates/hm-plugin-cloud/src/verbs/billing.rs b/crates/hm-plugin-cloud/src/verbs/billing.rs index faba1e1..d6e4fb9 100644 --- a/crates/hm-plugin-cloud/src/verbs/billing.rs +++ b/crates/hm-plugin-cloud/src/verbs/billing.rs @@ -62,12 +62,7 @@ async fn transactions(client: &Client, org: &str, limit: u32) -> Result<()> { Ok(()) } -async fn usage( - client: &Client, - org: &str, - from: Option<&str>, - to: Option<&str>, -) -> Result<()> { +async fn usage(client: &Client, org: &str, from: Option<&str>, to: Option<&str>) -> Result<()> { let mut q = vec![]; if let Some(f) = from { q.push(format!("from={f}")); @@ -128,7 +123,7 @@ async fn redeem(client: &Client, org: &str, code: &str) -> Result<()> { } fn active_org() -> Result { - CloudState::load().active_org.ok_or_else(|| { - anyhow::anyhow!("no active organization; run `hm cloud org switch `") - }) + CloudState::load() + .active_org + .ok_or_else(|| anyhow::anyhow!("no active organization; run `hm cloud org switch `")) } diff --git a/crates/hm-plugin-cloud/src/verbs/build.rs b/crates/hm-plugin-cloud/src/verbs/build.rs index 8797b69..fade59d 100644 --- a/crates/hm-plugin-cloud/src/verbs/build.rs +++ b/crates/hm-plugin-cloud/src/verbs/build.rs @@ -21,12 +21,8 @@ pub(crate) async fn run(env: &BTreeMap, cmd: BuildCommand) -> Re match cmd { BuildCommand::List { pipeline } => list(&client, &org, &pipeline).await, BuildCommand::Show { pipeline, number } => show(&client, &org, &pipeline, number).await, - BuildCommand::Cancel { pipeline, number } => { - cancel(&client, &org, &pipeline, number).await - } - BuildCommand::Watch { pipeline, number } => { - watch(&client, &org, &pipeline, number).await - } + BuildCommand::Cancel { pipeline, number } => cancel(&client, &org, &pipeline, number).await, + BuildCommand::Watch { pipeline, number } => watch(&client, &org, &pipeline, number).await, } } @@ -91,7 +87,7 @@ async fn watch(client: &Client, org: &str, pipe: &str, number: i64) -> Result<() } fn active_org() -> Result { - CloudState::load().active_org.ok_or_else(|| { - anyhow::anyhow!("no active organization; run `hm cloud org switch `") - }) + CloudState::load() + .active_org + .ok_or_else(|| anyhow::anyhow!("no active organization; run `hm cloud org switch `")) } diff --git a/crates/hm-plugin-cloud/src/verbs/job.rs b/crates/hm-plugin-cloud/src/verbs/job.rs index c1d33d6..044a07e 100644 --- a/crates/hm-plugin-cloud/src/verbs/job.rs +++ b/crates/hm-plugin-cloud/src/verbs/job.rs @@ -50,32 +50,17 @@ async fn list(client: &Client, org: &str, pipe: &str, build: i64) -> Result<()> Ok(()) } -async fn show( - client: &Client, - org: &str, - pipe: &str, - build: i64, - jid: &str, -) -> Result<()> { +async fn show(client: &Client, org: &str, pipe: &str, build: i64, jid: &str) -> Result<()> { let j: Job = client .get(&format!( "/organizations/{org}/pipelines/{pipe}/builds/{build}/jobs/{jid}" )) .await?; - println!( - "{}", - serde_json::to_string_pretty(&j).unwrap_or_default() - ); + println!("{}", serde_json::to_string_pretty(&j).unwrap_or_default()); Ok(()) } -async fn log_cmd( - client: &Client, - org: &str, - pipe: &str, - build: i64, - jid: &str, -) -> Result<()> { +async fn log_cmd(client: &Client, org: &str, pipe: &str, build: i64, jid: &str) -> Result<()> { let log: JobLog = client .get(&format!( "/organizations/{org}/pipelines/{pipe}/builds/{build}/jobs/{jid}/log" @@ -88,7 +73,7 @@ async fn log_cmd( } fn active_org() -> Result { - CloudState::load().active_org.ok_or_else(|| { - anyhow::anyhow!("no active organization; run `hm cloud org switch `") - }) + CloudState::load() + .active_org + .ok_or_else(|| anyhow::anyhow!("no active organization; run `hm cloud org switch `")) } diff --git a/crates/hm-plugin-cloud/src/verbs/org.rs b/crates/hm-plugin-cloud/src/verbs/org.rs index b3e70da..45ad26c 100644 --- a/crates/hm-plugin-cloud/src/verbs/org.rs +++ b/crates/hm-plugin-cloud/src/verbs/org.rs @@ -24,9 +24,11 @@ pub(crate) async fn run(env: &BTreeMap, cmd: OrgCommand) -> Resu async fn switch(client: &Client, slug: &str) -> Result<()> { let orgs: OrganizationList = client.get("/organizations").await?; - let found = orgs.data.iter().find(|o| o.slug == slug).ok_or_else(|| { - anyhow::anyhow!("no organization with slug '{slug}'") - })?; + let found = orgs + .data + .iter() + .find(|o| o.slug == slug) + .ok_or_else(|| anyhow::anyhow!("no organization with slug '{slug}'"))?; let mut state = CloudState::load(); state.active_org = Some(found.slug.clone()); state.save(); diff --git a/crates/hm-plugin-cloud/src/verbs/pipeline.rs b/crates/hm-plugin-cloud/src/verbs/pipeline.rs index d152737..233a93c 100644 --- a/crates/hm-plugin-cloud/src/verbs/pipeline.rs +++ b/crates/hm-plugin-cloud/src/verbs/pipeline.rs @@ -25,7 +25,9 @@ pub(crate) async fn run(env: &BTreeMap, cmd: PipelineCommand) -> } async fn list(client: &Client, org: &str) -> Result<()> { - let pipes: PipelineList = client.get(&format!("/organizations/{org}/pipelines")).await?; + let pipes: PipelineList = client + .get(&format!("/organizations/{org}/pipelines")) + .await?; for p in &pipes.data { println!( "{:<24} {}", @@ -52,7 +54,7 @@ async fn show(client: &Client, org: &str, slug: &str) -> Result<()> { } fn active_org() -> Result { - CloudState::load().active_org.ok_or_else(|| { - anyhow::anyhow!("no active organization; run `hm cloud org switch `") - }) + CloudState::load() + .active_org + .ok_or_else(|| anyhow::anyhow!("no active organization; run `hm cloud org switch `")) } diff --git a/crates/hm-plugin-protocol/src/events.rs b/crates/hm-plugin-protocol/src/events.rs index cd5d935..1a2764c 100644 --- a/crates/hm-plugin-protocol/src/events.rs +++ b/crates/hm-plugin-protocol/src/events.rs @@ -9,14 +9,26 @@ use uuid::Uuid; use crate::executor::SnapshotRef; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, DeriveJsonSchema, derive_more::IsVariant)] +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize, + Deserialize, + DeriveJsonSchema, + derive_more::IsVariant, +)] #[serde(rename_all = "snake_case")] pub enum StdStream { Stdout, Stderr, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DeriveJsonSchema, derive_more::IsVariant)] +#[derive( + Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DeriveJsonSchema, derive_more::IsVariant, +)] #[serde(tag = "kind", rename_all = "snake_case")] pub enum BuildEvent { BuildStart { diff --git a/crates/hm-plugin-protocol/src/executor.rs b/crates/hm-plugin-protocol/src/executor.rs index 4b03d02..7532319 100644 --- a/crates/hm-plugin-protocol/src/executor.rs +++ b/crates/hm-plugin-protocol/src/executor.rs @@ -11,8 +11,18 @@ use crate::ir::CommandStep; /// Opaque archive handle. The plugin streams bytes via /// `hm_archive_read(id, offset, max)`. #[derive( - Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, DeriveJsonSchema, - derive_more::From, derive_more::Deref, derive_more::Display, + Debug, + Clone, + Copy, + PartialEq, + Eq, + Hash, + Serialize, + Deserialize, + DeriveJsonSchema, + derive_more::From, + derive_more::Deref, + derive_more::Display, )] #[serde(transparent)] pub struct ArchiveId(pub Uuid); @@ -21,8 +31,17 @@ pub struct ArchiveId(pub Uuid); /// tag; other plugins are free to encode their own format. The host /// never inspects the contents. #[derive( - Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, DeriveJsonSchema, - derive_more::From, derive_more::Deref, derive_more::Display, + Debug, + Clone, + PartialEq, + Eq, + Hash, + Serialize, + Deserialize, + DeriveJsonSchema, + derive_more::From, + derive_more::Deref, + derive_more::Display, )] #[serde(transparent)] pub struct SnapshotRef(pub String); @@ -36,7 +55,9 @@ pub struct ArtifactRef { /// Host-decided cache outcome. The executor honours this; it does /// not re-decide. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DeriveJsonSchema, derive_more::IsVariant)] +#[derive( + Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DeriveJsonSchema, derive_more::IsVariant, +)] #[serde(tag = "kind", rename_all = "snake_case")] pub enum CacheDecision { /// Boot from `tag`; skip running `cmd`. diff --git a/crates/hm-util/src/lib.rs b/crates/hm-util/src/lib.rs index 56ea3f3..c5284c5 100644 --- a/crates/hm-util/src/lib.rs +++ b/crates/hm-util/src/lib.rs @@ -1,2 +1,2 @@ -pub mod os; pub mod dirs; +pub mod os; diff --git a/crates/hm-util/src/os/fs.rs b/crates/hm-util/src/os/fs.rs index fec7a30..cdb06b8 100644 --- a/crates/hm-util/src/os/fs.rs +++ b/crates/hm-util/src/os/fs.rs @@ -75,10 +75,7 @@ pub async fn write_atomic_restricted( /// /// Returns an error if the rename fails (permission denied, cross-device, /// source missing, etc.). -pub async fn atomic_rename_over( - from: impl AsRef, - to: impl AsRef, -) -> io::Result<()> { +pub async fn atomic_rename_over(from: impl AsRef, to: impl AsRef) -> io::Result<()> { #[cfg(unix)] { tokio::fs::rename(from.as_ref(), to.as_ref()).await @@ -86,12 +83,11 @@ pub async fn atomic_rename_over( #[cfg(windows)] { fn atomic_rename_over_impl(from: &Path, to: &Path) -> io::Result<()> { - use windows::core::HSTRING; use windows::Win32::Storage::FileSystem::{ - MoveFileExW, ReplaceFileW, - MOVEFILE_REPLACE_EXISTING, MOVEFILE_WRITE_THROUGH, - REPLACEFILE_IGNORE_MERGE_ERRORS, + MOVEFILE_REPLACE_EXISTING, MOVEFILE_WRITE_THROUGH, MoveFileExW, + REPLACEFILE_IGNORE_MERGE_ERRORS, ReplaceFileW, }; + use windows::core::HSTRING; let from_w = HSTRING::from(from.as_os_str()); let to_w = HSTRING::from(to.as_os_str()); @@ -189,7 +185,6 @@ async fn write_file_with_mode(path: &Path, contents: &[u8], mode: u32) -> io::Re Ok(()) } - /// Synchronous wrappers that shell out to the async API via /// `tokio::task::block_in_place`. Safe to call from sync contexts /// that run inside a tokio runtime (e.g. extism `host_fn` callbacks). @@ -217,7 +212,9 @@ pub mod blocking { file_mode: u32, dir_mode: u32, ) -> io::Result<()> { - block_on(super::write_atomic_restricted(path, contents, file_mode, dir_mode)) + block_on(super::write_atomic_restricted( + path, contents, file_mode, dir_mode, + )) } /// Blocking counterpart of [`super::remove_file_if_exists`]. diff --git a/crates/hm/src/commands/dev/down.rs b/crates/hm/src/commands/dev/down.rs index cbbc769..5e076f4 100644 --- a/crates/hm/src/commands/dev/down.rs +++ b/crates/hm/src/commands/dev/down.rs @@ -9,23 +9,30 @@ use crate::context::RunContext; use crate::orchestrator::docker_client::DockerClient; use super::naming::{ - DRIVER_LOCAL, LABEL_DRIVER, LABEL_SESSION, LABEL_SLUG, LABEL_WORKTREE, - network_name, resolve_worktree_root, worktree_hash, + DRIVER_LOCAL, LABEL_DRIVER, LABEL_SESSION, LABEL_SLUG, LABEL_WORKTREE, network_name, + resolve_worktree_root, worktree_hash, }; /// # Errors /// /// Returns Docker errors on list / stop / remove failures. -#[allow(clippy::print_stderr, reason = "status messages to stderr are intentional for a foreground CLI")] +#[allow( + clippy::print_stderr, + reason = "status messages to stderr are intentional for a foreground CLI" +)] pub async fn handle(args: DevDownArgs, _ctx: RunContext) -> Result { let docker = DockerClient::connect()?; let worktree_root = resolve_worktree_root()?; let wt_hash = worktree_hash(&worktree_root); let containers = if args.all { - docker.list_containers_by_label(LABEL_DRIVER, DRIVER_LOCAL).await? + docker + .list_containers_by_label(LABEL_DRIVER, DRIVER_LOCAL) + .await? } else { - docker.list_containers_by_label(LABEL_WORKTREE, &wt_hash).await? + docker + .list_containers_by_label(LABEL_WORKTREE, &wt_hash) + .await? }; // (id, slug, session, name) diff --git a/crates/hm/src/commands/dev/exec.rs b/crates/hm/src/commands/dev/exec.rs index f8dab5e..621845e 100644 --- a/crates/hm/src/commands/dev/exec.rs +++ b/crates/hm/src/commands/dev/exec.rs @@ -24,7 +24,9 @@ pub async fn handle(args: DevExecArgs, _ctx: RunContext) -> Result { let docker = DockerClient::connect()?; let worktree_root = resolve_worktree_root()?; let wt_hash = worktree_hash(&worktree_root); - let containers = docker.list_containers_by_label(LABEL_WORKTREE, &wt_hash).await?; + let containers = docker + .list_containers_by_label(LABEL_WORKTREE, &wt_hash) + .await?; let mut matches: Vec = Vec::new(); for c in &containers { let labels = c.labels.clone().unwrap_or_default(); diff --git a/crates/hm/src/commands/dev/logmux.rs b/crates/hm/src/commands/dev/logmux.rs index 167fbc0..2ff9432 100644 --- a/crates/hm/src/commands/dev/logmux.rs +++ b/crates/hm/src/commands/dev/logmux.rs @@ -19,9 +19,14 @@ struct PerSlug { } impl PerSlug { - fn ingest(&mut self, slug: &str, width: usize, color: bool, bytes: &[u8], w: &mut W) - -> std::io::Result<()> - { + fn ingest( + &mut self, + slug: &str, + width: usize, + color: bool, + bytes: &[u8], + w: &mut W, + ) -> std::io::Result<()> { self.buf.extend_from_slice(bytes); while let Some(idx) = self.buf.iter().position(|&b| b == b'\n') { // line = bytes up to (excluding) the newline @@ -32,9 +37,13 @@ impl PerSlug { Ok(()) } - fn flush(&mut self, slug: &str, width: usize, color: bool, w: &mut W) - -> std::io::Result<()> - { + fn flush( + &mut self, + slug: &str, + width: usize, + color: bool, + w: &mut W, + ) -> std::io::Result<()> { if !self.buf.is_empty() { let line = std::mem::take(&mut self.buf); write_line(slug, width, color, &line, w)?; @@ -60,9 +69,13 @@ fn slug_color(slug: &str) -> AnsiColors { PALETTE[(h as usize) % PALETTE.len()] } -fn write_line(slug: &str, width: usize, color: bool, line: &[u8], w: &mut W) - -> std::io::Result<()> -{ +fn write_line( + slug: &str, + width: usize, + color: bool, + line: &[u8], + w: &mut W, +) -> std::io::Result<()> { let prefix = format!("[{slug: Result { let docker = DockerClient::connect()?; let worktree_root = resolve_worktree_root()?; let wt_hash = worktree_hash(&worktree_root); - let containers = docker.list_containers_by_label(LABEL_WORKTREE, &wt_hash).await?; + let containers = docker + .list_containers_by_label(LABEL_WORKTREE, &wt_hash) + .await?; let mut matches: Vec<(String, String, String)> = Vec::new(); for c in &containers { let labels = c.labels.clone().unwrap_or_default(); diff --git a/crates/hm/src/commands/dev/ls.rs b/crates/hm/src/commands/dev/ls.rs index 6ccbda4..ac11b0c 100644 --- a/crates/hm/src/commands/dev/ls.rs +++ b/crates/hm/src/commands/dev/ls.rs @@ -18,7 +18,10 @@ use super::registry::{RegEntry, dump}; /// /// Returns an error if the worktree root cannot be resolved or the /// registry subprocess fails. -#[allow(clippy::print_stdout, reason = "`hm dev ls` is a table-printing command")] +#[allow( + clippy::print_stdout, + reason = "`hm dev ls` is a table-printing command" +)] pub async fn handle(_ctx: RunContext) -> Result { let worktree_root = resolve_worktree_root()?; let wt_hash = worktree_hash(&worktree_root); @@ -31,8 +34,7 @@ pub async fn handle(_ctx: RunContext) -> Result { ); // Pre-load running containers by (slug, session) key. - let mut running: HashMap<(String, String), (String, HashMap)> = - HashMap::new(); + let mut running: HashMap<(String, String), (String, HashMap)> = HashMap::new(); if let Some(d) = &docker { let containers = d .list_containers_by_label(LABEL_WORKTREE, &wt_hash) diff --git a/crates/hm/src/commands/dev/naming.rs b/crates/hm/src/commands/dev/naming.rs index 10b54fc..c69d4a2 100644 --- a/crates/hm/src/commands/dev/naming.rs +++ b/crates/hm/src/commands/dev/naming.rs @@ -59,7 +59,9 @@ pub fn network_name(worktree_hash: &str, session: &str) -> String { /// Returns an error if the cwd is unreadable. pub fn resolve_worktree_root() -> Result { use std::process::Command; - let try_git = Command::new("git").args(["rev-parse", "--show-toplevel"]).output(); + let try_git = Command::new("git") + .args(["rev-parse", "--show-toplevel"]) + .output(); if let Ok(out) = try_git && out.status.success() { @@ -108,9 +110,6 @@ mod tests { #[test] fn network_name_format() { - assert_eq!( - network_name("a1b2c3d4e5", "7a2f91"), - "hm-a1b2c3d4e5-7a2f91", - ); + assert_eq!(network_name("a1b2c3d4e5", "7a2f91"), "hm-a1b2c3d4e5-7a2f91",); } } diff --git a/crates/hm/src/commands/dev/network.rs b/crates/hm/src/commands/dev/network.rs index 96ee559..cc11c5a 100644 --- a/crates/hm/src/commands/dev/network.rs +++ b/crates/hm/src/commands/dev/network.rs @@ -6,9 +6,7 @@ use anyhow::Result; use crate::orchestrator::docker_client::DockerClient; -use super::naming::{ - DRIVER_LOCAL, LABEL_DRIVER, LABEL_SESSION, LABEL_WORKTREE, network_name, -}; +use super::naming::{DRIVER_LOCAL, LABEL_DRIVER, LABEL_SESSION, LABEL_WORKTREE, network_name}; #[derive(Debug, Clone)] pub struct Network { @@ -20,11 +18,7 @@ pub struct Network { /// # Errors /// /// Returns the docker error if the daemon rejects creation. -pub async fn create( - docker: &DockerClient, - worktree_hash: &str, - session: &str, -) -> Result { +pub async fn create(docker: &DockerClient, worktree_hash: &str, session: &str) -> Result { let name = network_name(worktree_hash, session); let mut labels = HashMap::new(); labels.insert(LABEL_WORKTREE.to_string(), worktree_hash.to_string()); diff --git a/crates/hm/src/commands/dev/port_of.rs b/crates/hm/src/commands/dev/port_of.rs index 9e203c6..38c2824 100644 --- a/crates/hm/src/commands/dev/port_of.rs +++ b/crates/hm/src/commands/dev/port_of.rs @@ -20,18 +20,25 @@ use super::naming::{ clippy::print_stderr, reason = "user-facing error messages for a foreground CLI" )] -#[allow(clippy::print_stdout, reason = "`hm dev port-of` prints the port to stdout for $() use")] +#[allow( + clippy::print_stdout, + reason = "`hm dev port-of` prints the port to stdout for $() use" +)] pub async fn handle(args: DevPortOfArgs, _ctx: RunContext) -> Result { let docker = DockerClient::connect()?; let worktree_root = resolve_worktree_root()?; let wt_hash = worktree_hash(&worktree_root); - let containers = docker.list_containers_by_label(LABEL_WORKTREE, &wt_hash).await?; + let containers = docker + .list_containers_by_label(LABEL_WORKTREE, &wt_hash) + .await?; let mut matches: Vec<(String, String, std::collections::HashMap)> = Vec::new(); for c in &containers { let labels = c.labels.clone().unwrap_or_default(); let slug = labels.get(LABEL_SLUG).cloned().unwrap_or_default(); let session = labels.get(LABEL_SESSION).cloned().unwrap_or_default(); - if slug != args.slug { continue; } + if slug != args.slug { + continue; + } if let Some(s) = &args.session && &session != s { @@ -63,7 +70,10 @@ pub async fn handle(args: DevPortOfArgs, _ctx: RunContext) -> Result { } } if matches.len() > 1 { - eprintln!("hm: slug `{}` matches multiple live sessions in this worktree:", args.slug); + eprintln!( + "hm: slug `{}` matches multiple live sessions in this worktree:", + args.slug + ); for (_, sess, ports) in &matches { let p = format_ports(ports); eprintln!(" {sess} {p}"); @@ -87,7 +97,8 @@ pub async fn handle(args: DevPortOfArgs, _ctx: RunContext) -> Result { fn format_ports(ports: &std::collections::HashMap) -> String { let mut entries: Vec<(u16, u16)> = ports.iter().map(|(c, h)| (*c, *h)).collect(); entries.sort_unstable(); - entries.iter() + entries + .iter() .map(|(c, h)| format!("localhost:{h} → :{c}")) .collect::>() .join(", ") diff --git a/crates/hm/src/commands/dev/registry.rs b/crates/hm/src/commands/dev/registry.rs index 827b2a8..908748b 100644 --- a/crates/hm/src/commands/dev/registry.rs +++ b/crates/hm/src/commands/dev/registry.rs @@ -64,12 +64,7 @@ pub const PORT_SENTINEL: &str = "__hm_dev_port__"; pub async fn dump(worktree_root: &Path) -> Result { let py = std::env::var("HARMONT_PYTHON").unwrap_or_else(|_| "python3".to_string()); let output = Command::new(&py) - .args([ - "-m", - "harmont.dev", - "--dump-registry", - "--worktree-root", - ]) + .args(["-m", "harmont.dev", "--dump-registry", "--worktree-root"]) .arg(worktree_root) .stdout(Stdio::piped()) .stderr(Stdio::piped()) @@ -113,7 +108,9 @@ mod tests { }"#; let reg: DevRegistry = serde_json::from_str(raw).unwrap(); assert_eq!(reg.schema_version, "0"); - let RegEntry::Local(spec) = ®.deployments["db"] else { panic!("local expected") }; + let RegEntry::Local(spec) = ®.deployments["db"] else { + panic!("local expected") + }; assert_eq!(spec.image.as_deref(), Some("postgres:16")); assert_eq!(spec.port_mapping["5432"], PORT_SENTINEL); } @@ -138,7 +135,9 @@ mod tests { } }"#; let reg: DevRegistry = serde_json::from_str(raw).unwrap(); - let RegEntry::Local(spec) = ®.deployments["api"] else { panic!() }; + let RegEntry::Local(spec) = ®.deployments["api"] else { + panic!() + }; assert!(matches!(spec.from, Some(FromSource::StepChain { .. }))); assert_eq!(spec.deps, vec!["db"]); } diff --git a/crates/hm/src/commands/dev/service_spec.rs b/crates/hm/src/commands/dev/service_spec.rs index 3f51e86..0b1462d 100644 --- a/crates/hm/src/commands/dev/service_spec.rs +++ b/crates/hm/src/commands/dev/service_spec.rs @@ -8,8 +8,7 @@ use anyhow::{Context, Result, anyhow}; use crate::orchestrator::docker_client::{ServiceSpec, ServiceSpecBuilder}; use super::naming::{ - DRIVER_LOCAL, LABEL_DRIVER, LABEL_SESSION, LABEL_SLUG, LABEL_WORKTREE, - container_name, + DRIVER_LOCAL, LABEL_DRIVER, LABEL_SESSION, LABEL_SLUG, LABEL_WORKTREE, container_name, }; use super::registry::{LocalSpec, PORT_SENTINEL}; @@ -69,9 +68,11 @@ pub fn build( .port_mapping .iter() .filter(|(_, sentinel)| sentinel.as_str() == PORT_SENTINEL) - .map(|(cport, _)| cport.parse::().context(format!( - "port_mapping key `{cport}` is not a valid u16 — registry-dump bug?" - ))) + .map(|(cport, _)| { + cport.parse::().context(format!( + "port_mapping key `{cport}` is not a valid u16 — registry-dump bug?" + )) + }) .collect::>>()?; let mut labels = HashMap::new(); labels.insert(LABEL_WORKTREE.to_string(), worktree_hash.to_string()); @@ -103,9 +104,9 @@ fn resolve_binds( } else { worktree_root.join(host) }; - let host_str = host_abs.to_str().ok_or_else(|| { - anyhow!("bind host path is not valid UTF-8: {}", host_abs.display()) - })?; + let host_str = host_abs + .to_str() + .ok_or_else(|| anyhow!("bind host path is not valid UTF-8: {}", host_abs.display()))?; // container may carry a `:ro` suffix; split + reconstruct so we // emit "host:container[:ro]". let (cpath, mode) = match container.rsplit_once(':') { @@ -135,9 +136,7 @@ mod tests { from: None, cmd: None, port_mapping, - env: BTreeMap::from([ - ("POSTGRES_PASSWORD".to_string(), "dev".to_string()), - ]), + env: BTreeMap::from([("POSTGRES_PASSWORD".to_string(), "dev".to_string())]), volumes: BTreeMap::new(), workdir: None, deps: vec![], @@ -154,7 +153,8 @@ mod tests { "a1b2c3d4e5", "7a2f91", "hm-a1b2c3d4e5-7a2f91", - ).unwrap(); + ) + .unwrap(); assert_eq!(rs.container_name, "hm-a1b2c3d4e5-db-7a2f91"); assert_eq!(rs.publish, vec![5432]); assert!(rs.env.contains(&"POSTGRES_PASSWORD=dev".to_string())); @@ -164,22 +164,18 @@ mod tests { #[test] fn resolves_relative_volume_against_worktree_root() { let mut spec = local_spec(); - spec.volumes.insert(".".to_string(), "/workspace".to_string()); - let rs = build( - "web", &spec, "node:20", - Path::new("/tmp/wt"), "a", "b", "n", - ).unwrap(); + spec.volumes + .insert(".".to_string(), "/workspace".to_string()); + let rs = build("web", &spec, "node:20", Path::new("/tmp/wt"), "a", "b", "n").unwrap(); assert_eq!(rs.binds, vec!["/tmp/wt/.:/workspace".to_string()]); } #[test] fn preserves_ro_suffix_on_container_path() { let mut spec = local_spec(); - spec.volumes.insert(".".to_string(), "/workspace:ro".to_string()); - let rs = build( - "web", &spec, "node:20", - Path::new("/tmp/wt"), "a", "b", "n", - ).unwrap(); + spec.volumes + .insert(".".to_string(), "/workspace:ro".to_string()); + let rs = build("web", &spec, "node:20", Path::new("/tmp/wt"), "a", "b", "n").unwrap(); assert_eq!(rs.binds, vec!["/tmp/wt/.:/workspace:ro".to_string()]); } } diff --git a/crates/hm/src/commands/dev/topo.rs b/crates/hm/src/commands/dev/topo.rs index 269a131..ab4099f 100644 --- a/crates/hm/src/commands/dev/topo.rs +++ b/crates/hm/src/commands/dev/topo.rs @@ -34,7 +34,10 @@ pub fn plan(reg: &DevRegistry, requested: &[String], no_deps: bool) -> Result> = BTreeMap::new(); for (slug, entry) in ®.deployments { if let RegEntry::Local(spec) = entry { - deps.insert(slug.as_str(), spec.deps.iter().map(String::as_str).collect()); + deps.insert( + slug.as_str(), + spec.deps.iter().map(String::as_str).collect(), + ); } } for s in requested { @@ -42,8 +45,11 @@ pub fn plan(reg: &DevRegistry, requested: &[String], no_deps: bool) -> Result Result Result DevRegistry { @@ -138,11 +147,14 @@ mod tests { fn empty_request_brings_up_everything() { let r = reg(&[("db", &[]), ("api", &["db"]), ("web", &["api"])]); let plan = plan(&r, &[], false).unwrap(); - assert_eq!(plan.levels, vec![ - vec!["db".to_string()], - vec!["api".to_string()], - vec!["web".to_string()], - ]); + assert_eq!( + plan.levels, + vec![ + vec!["db".to_string()], + vec!["api".to_string()], + vec!["web".to_string()], + ] + ); } #[test] diff --git a/crates/hm/src/commands/dev/up.rs b/crates/hm/src/commands/dev/up.rs index 87df347..2059b35 100644 --- a/crates/hm/src/commands/dev/up.rs +++ b/crates/hm/src/commands/dev/up.rs @@ -43,14 +43,19 @@ struct BootCtx { /// /// Returns an error if the registry dump fails, Docker is unreachable, /// network creation fails, or any container boot fails. -#[allow(clippy::print_stderr, reason = "status messages to stderr are intentional for a foreground CLI")] +#[allow( + clippy::print_stderr, + reason = "status messages to stderr are intentional for a foreground CLI" +)] pub async fn handle(args: DevUpArgs, _ctx: RunContext) -> Result { let worktree_root = resolve_worktree_root()?; let wt_hash = worktree_hash(&worktree_root); let session_id = fresh_session_id(); eprintln!("[hm] session {session_id}. resolving deployments in .harmont/"); - let registry = dump(&worktree_root).await.context("dump deployment registry")?; + let registry = dump(&worktree_root) + .await + .context("dump deployment registry")?; let boot_plan = plan(®istry, &args.slugs, args.no_deps)?; let docker = DockerClient::connect()?; docker.ping().await.context("docker daemon ping")?; @@ -87,9 +92,7 @@ pub async fn handle(args: DevUpArgs, _ctx: RunContext) -> Result { let slug = slug.clone(); let log_tx = log_tx.clone(); let ctx = ctx.clone(); - joinset.spawn(async move { - boot_one(docker, slug, spec, ctx, log_tx).await - }); + joinset.spawn(async move { boot_one(docker, slug, spec, ctx, log_tx).await }); } while let Some(res) = joinset.join_next().await { let b = res??; @@ -112,7 +115,10 @@ pub async fn handle(args: DevUpArgs, _ctx: RunContext) -> Result { Ok(0) } -#[allow(clippy::print_stderr, reason = "per-slug ready/pull/build messages go to stderr")] +#[allow( + clippy::print_stderr, + reason = "per-slug ready/pull/build messages go to stderr" +)] async fn boot_one( docker: DockerClient, slug: String, @@ -121,8 +127,7 @@ async fn boot_one( log_tx: mpsc::UnboundedSender, ) -> Result { // Resolve image: raw or build-from-step. - let image = - resolve_image(&docker, &slug, &spec, &ctx.worktree_hash, ctx.rebuild).await?; + let image = resolve_image(&docker, &slug, &spec, &ctx.worktree_hash, ctx.rebuild).await?; let resolved = build_spec( &slug, &spec, @@ -175,8 +180,7 @@ async fn resolve_image( return Ok(tag.clone()); } if let Some(FromSource::StepChain { pipeline_v0 }) = &spec.from { - let chain_key = - extract_terminal_key(pipeline_v0).unwrap_or_else(|| "nocache".to_string()); + let chain_key = extract_terminal_key(pipeline_v0).unwrap_or_else(|| "nocache".to_string()); let tag = format!("hm-build-{worktree_hash}-{slug}:{chain_key}"); if rebuild || !docker.image_exists(&tag).await? { #[allow(clippy::print_stderr, reason = "build progress goes to stderr")] @@ -219,7 +223,13 @@ async fn stream_logs( match item { Ok(chunk) => { let bytes = chunk.into_bytes().to_vec(); - if tx.send(LogLine { slug: slug.clone(), bytes }).is_err() { + if tx + .send(LogLine { + slug: slug.clone(), + bytes, + }) + .is_err() + { break; } } diff --git a/crates/hm/src/commands/run/local.rs b/crates/hm/src/commands/run/local.rs index 27cdded..2759476 100644 --- a/crates/hm/src/commands/run/local.rs +++ b/crates/hm/src/commands/run/local.rs @@ -109,7 +109,6 @@ pub async fn handle(args: RunArgs, _ctx: RunContext) -> Result { }; let exit_code = - crate::orchestrator::run(graph, repo_root, parallelism, runner_registry, renderer) - .await?; + crate::orchestrator::run(graph, repo_root, parallelism, runner_registry, renderer).await?; Ok(exit_code) } diff --git a/crates/hm/src/config.rs b/crates/hm/src/config.rs index 5a83422..4292958 100644 --- a/crates/hm/src/config.rs +++ b/crates/hm/src/config.rs @@ -12,8 +12,7 @@ const DEFAULT_API_URL: &str = "https://api.harmont.dev"; /// (the `dirs` crate's platform-specific lookup fails — typically only /// happens in restrictive sandboxes with no `HOME` / passwd entry). pub fn user_config_dir() -> Result { - hm_util::dirs::harmont_config_dir() - .context("could not determine home directory") + hm_util::dirs::harmont_config_dir().context("could not determine home directory") } /// User preferences stored alongside the config. @@ -81,8 +80,13 @@ impl Config { pub fn save(&self) -> Result<()> { let path = Self::path()?; let serialized = toml::to_string_pretty(self).context("serializing config")?; - hm_util::os::fs::blocking::write_atomic_restricted(&path, serialized.as_bytes(), 0o644, 0o700) - .with_context(|| format!("writing {}", path.display()))?; + hm_util::os::fs::blocking::write_atomic_restricted( + &path, + serialized.as_bytes(), + 0o644, + 0o700, + ) + .with_context(|| format!("writing {}", path.display()))?; Ok(()) } diff --git a/crates/hm/src/creds_store.rs b/crates/hm/src/creds_store.rs index d6e45d0..55d5b94 100644 --- a/crates/hm/src/creds_store.rs +++ b/crates/hm/src/creds_store.rs @@ -59,13 +59,10 @@ pub fn set(service: &str, account: &str, secret: &str) { /// underlying write fails. pub fn delete(service: &str, account: &str) { let mut f = load(); - let now_empty = f - .entries - .get_mut(service) - .is_some_and(|svc| { - svc.remove(account); - svc.is_empty() - }); + let now_empty = f.entries.get_mut(service).is_some_and(|svc| { + svc.remove(account); + svc.is_empty() + }); if now_empty { f.entries.remove(service); } @@ -81,7 +78,9 @@ mod tests { let tmp = tempfile::tempdir().unwrap(); let prev = std::env::var_os("HOME"); // SAFETY: tests are single-threaded for env mutation by Cargo. - unsafe { std::env::set_var("HOME", tmp.path()); } + unsafe { + std::env::set_var("HOME", tmp.path()); + } f(); unsafe { if let Some(v) = prev { diff --git a/crates/hm/src/orchestrator/docker_client.rs b/crates/hm/src/orchestrator/docker_client.rs index fc43071..ae5657d 100644 --- a/crates/hm/src/orchestrator/docker_client.rs +++ b/crates/hm/src/orchestrator/docker_client.rs @@ -396,8 +396,7 @@ impl DockerClient { match self.inner.remove_network(name).await { Ok(()) | Err(bollard::errors::Error::DockerResponseServerError { - status_code: 404, - .. + status_code: 404, .. }) => Ok(()), Err(e) => Err(HmError::Docker(format!("remove_network({name}): {e}")).into()), } @@ -428,14 +427,20 @@ impl DockerClient { // Docker's exposed_ports type requires HashMap>. // The unit-value inner map is the Docker API convention for "no options". - #[allow(clippy::zero_sized_map_values, reason = "Docker API requires this exact type")] + #[allow( + clippy::zero_sized_map_values, + reason = "Docker API requires this exact type" + )] let (mut exposed, mut port_bindings) = ( HashMap::>::new(), HashMap::>>::new(), ); for cport in &spec.publish { let key = format!("{cport}/tcp"); - #[allow(clippy::zero_sized_map_values, reason = "Docker API requires this exact type")] + #[allow( + clippy::zero_sized_map_values, + reason = "Docker API requires this exact type" + )] exposed.insert(key.clone(), HashMap::new()); port_bindings.insert( key, @@ -559,8 +564,7 @@ impl DockerClient { { Ok(()) | Err(bollard::errors::Error::DockerResponseServerError { - status_code: 404, - .. + status_code: 404, .. }) => Ok(()), Err(e) => Err(HmError::Docker(format!("stop_container({container_id}): {e}")).into()), } @@ -585,12 +589,9 @@ impl DockerClient { { Ok(()) | Err(bollard::errors::Error::DockerResponseServerError { - status_code: 404, - .. + status_code: 404, .. }) => Ok(()), - Err(e) => { - Err(HmError::Docker(format!("remove_container({container_id}): {e}")).into()) - } + Err(e) => Err(HmError::Docker(format!("remove_container({container_id}): {e}")).into()), } } diff --git a/crates/hm/src/orchestrator/mod.rs b/crates/hm/src/orchestrator/mod.rs index 7dcb987..6f84448 100644 --- a/crates/hm/src/orchestrator/mod.rs +++ b/crates/hm/src/orchestrator/mod.rs @@ -53,11 +53,7 @@ pub async fn build_image_from_pipeline( // // NOTE: run_pipeline_v0_one_shot is currently a stub (see its doc // comment in commands/run/local.rs for the full rationale). - let container_id = crate::commands::run::run_pipeline_v0_one_shot( - docker, - pipeline_v0, - ) - .await?; + let container_id = crate::commands::run::run_pipeline_v0_one_shot(docker, pipeline_v0).await?; docker.commit_container(&container_id, image_tag).await?; docker.remove_container(&container_id).await?; Ok(()) diff --git a/crates/hm/src/orchestrator/output_subscriber.rs b/crates/hm/src/orchestrator/output_subscriber.rs index 18fad53..576756f 100644 --- a/crates/hm/src/orchestrator/output_subscriber.rs +++ b/crates/hm/src/orchestrator/output_subscriber.rs @@ -9,10 +9,7 @@ // - `print_stderr`: the Lagged arm intentionally bypasses the event // bus (which is the source of the lag) to surface a user-visible // drop signal, so an `eprintln!` direct to stderr is correct. -#![allow( - clippy::needless_pass_by_value, - clippy::print_stderr -)] +#![allow(clippy::needless_pass_by_value, clippy::print_stderr)] use std::sync::Arc; diff --git a/crates/hm/src/orchestrator/scheduler.rs b/crates/hm/src/orchestrator/scheduler.rs index 2be4273..62116eb 100644 --- a/crates/hm/src/orchestrator/scheduler.rs +++ b/crates/hm/src/orchestrator/scheduler.rs @@ -27,8 +27,8 @@ use std::path::PathBuf; use std::sync::Arc; use std::time::Instant; -use daggy::{Dag, NodeIndex, Walker}; use daggy::petgraph::algo::toposort; +use daggy::{Dag, NodeIndex, Walker}; use futures::future::{BoxFuture, FutureExt, join_all}; use anyhow::{Context, Result}; @@ -46,8 +46,8 @@ use crate::runner::{OutputRenderer, RunContext, RunnerRegistry}; use super::archive::ArchiveStore; use super::cache; -use tokio_util::sync::CancellationToken; use super::events::EventBus; +use tokio_util::sync::CancellationToken; #[derive(Clone)] struct StepOutcome { @@ -152,10 +152,11 @@ pub async fn run( join_all(preds.iter().map(|(_, f)| f.clone())).await; // Early exit if any predecessor failed or the build was cancelled. - if cancel.is_cancelled() - || pred_outcomes.iter().any(|o| o.exit_code != 0) - { - return StepOutcome { exit_code: 0, snapshot: None }; + if cancel.is_cancelled() || pred_outcomes.iter().any(|o| o.exit_code != 0) { + return StepOutcome { + exit_code: 0, + snapshot: None, + }; } // Acquire parallelism permit. @@ -189,7 +190,10 @@ pub async fn run( Ok(outcome) => outcome, Err(e) => { tracing::error!(%e, "step execution failed"); - StepOutcome { exit_code: 1, snapshot: None } + StepOutcome { + exit_code: 1, + snapshot: None, + } } } } diff --git a/crates/hm/src/runner/docker.rs b/crates/hm/src/runner/docker.rs index ecbe1c7..b6757b6 100644 --- a/crates/hm/src/runner/docker.rs +++ b/crates/hm/src/runner/docker.rs @@ -91,7 +91,11 @@ async fn run_step(ctx: &RunContext, input: ExecutorInput) -> Result }); } - let image = resolve_image(&input.step, plan.hit_tag.as_ref(), input.parent_snapshot.as_ref()); + let image = resolve_image( + &input.step, + plan.hit_tag.as_ref(), + input.parent_snapshot.as_ref(), + ); let container_name = sanitize_container_name(&input.run_id.to_string(), &input.step.key); let env_vec: Vec = input.env.iter().map(|(k, v)| format!("{k}={v}")).collect(); @@ -131,10 +135,7 @@ async fn run_in_container( // --- Extract workspace archive --- let archive = ctx.archives.read(input.workspace_archive_id, 0, u64::MAX); if archive.is_empty() { - anyhow::bail!( - "archive {} is empty or unknown", - input.workspace_archive_id - ); + anyhow::bail!("archive {} is empty or unknown", input.workspace_archive_id); } let docker = ctx.docker.clone(); @@ -188,7 +189,10 @@ async fn run_in_container( } }; - #[allow(clippy::cast_possible_truncation, reason = "docker exit codes fit in i32")] + #[allow( + clippy::cast_possible_truncation, + reason = "docker exit codes fit in i32" + )] let exit_code = rc as i32; // --- Commit snapshot on success --- diff --git a/crates/hm/tests/default_image_inheritance.rs b/crates/hm/tests/default_image_inheritance.rs index cc5ca19..9c70643 100644 --- a/crates/hm/tests/default_image_inheritance.rs +++ b/crates/hm/tests/default_image_inheritance.rs @@ -22,7 +22,9 @@ fn decode(json: &[u8]) -> PipelineGraph { fn find_step<'a>(g: &'a PipelineGraph, key: &str) -> &'a hm_pipeline_ir::CommandStep { let dag = g.dag(); - let (_, t) = dag.graph().node_references() + let (_, t) = dag + .graph() + .node_references() .find(|(_, t)| t.step.key == key) .unwrap(); &t.step @@ -51,7 +53,8 @@ fn root_step_inherits_default_image() { #[test] fn root_step_explicit_image_wins() { - let g = decode(br#"{ + let g = decode( + br#"{ "version": "0", "default_image": "ubuntu:24.04", "graph": { @@ -61,7 +64,8 @@ fn root_step_explicit_image_wins() { "edge_property": "directed", "edges": [] } - }"#); + }"#, + ); let step = find_step(&g, "rust"); assert_eq!( step.image.as_deref(), @@ -75,7 +79,8 @@ fn child_step_unchanged_by_default_image() { // Children boot from the parent's committed snapshot at runtime, // not from an image tag — leaving their image=None is the correct // wire state for chain steps. - let g = decode(br#"{ + let g = decode( + br#"{ "version": "0", "default_image": "ubuntu:24.04", "graph": { @@ -88,7 +93,8 @@ fn child_step_unchanged_by_default_image() { [0, 1, "builds_in"] ] } - }"#); + }"#, + ); let step = find_step(&g, "child"); assert!( step.image.is_none(), @@ -98,7 +104,8 @@ fn child_step_unchanged_by_default_image() { #[test] fn no_default_image_leaves_root_alone() { - let g = decode(br#"{ + let g = decode( + br#"{ "version": "0", "graph": { "nodes": [ @@ -107,7 +114,8 @@ fn no_default_image_leaves_root_alone() { "edge_property": "directed", "edges": [] } - }"#); + }"#, + ); let step = find_step(&g, "k"); assert!( step.image.is_none(), diff --git a/crates/hm/tests/dev_integration.rs b/crates/hm/tests/dev_integration.rs index dc71a78..fa87cff 100644 --- a/crates/hm/tests/dev_integration.rs +++ b/crates/hm/tests/dev_integration.rs @@ -12,11 +12,26 @@ #![cfg(feature = "docker-integration")] // Integration tests intentionally use unwrap/expect/panic to fail loudly on // docker-state mismatches; that's the correct behaviour for test code. -#![allow(clippy::unwrap_used, reason = "integration test helpers panic on docker-state mismatch")] -#![allow(clippy::expect_used, reason = "integration test helpers panic on docker-state mismatch")] -#![allow(clippy::panic, reason = "poll_http panics after timeout — correct for test code")] -#![allow(clippy::cast_possible_wrap, reason = "pid fits in i32 on all platforms we target")] -#![allow(clippy::ignore_without_reason, reason = "reason is in the test name and doc comment above")] +#![allow( + clippy::unwrap_used, + reason = "integration test helpers panic on docker-state mismatch" +)] +#![allow( + clippy::expect_used, + reason = "integration test helpers panic on docker-state mismatch" +)] +#![allow( + clippy::panic, + reason = "poll_http panics after timeout — correct for test code" +)] +#![allow( + clippy::cast_possible_wrap, + reason = "pid fits in i32 on all platforms we target" +)] +#![allow( + clippy::ignore_without_reason, + reason = "reason is in the test name and doc comment above" +)] use std::io::Read; use std::path::PathBuf; @@ -40,7 +55,9 @@ fn hm_bin() -> PathBuf { #[ignore] fn up_serves_http_and_tears_down() { let tmp = tempfile::tempdir().unwrap(); - write_deploys_py(tmp.path(), r#" + write_deploys_py( + tmp.path(), + r#" import harmont as hm @hm.deploy("hello") @@ -50,7 +67,8 @@ def hello(): cmd=["python", "-m", "http.server", "5678"], port_mapping={5678: hm.dev.port()}, ) -"#); +"#, + ); let mut up = Command::new(hm_bin()) .args(["dev", "up"]) @@ -66,27 +84,38 @@ def hello(): let started = std::time::Instant::now(); while started.elapsed().as_secs() < 60 { let n = stderr.read(&mut chunk).unwrap_or(0); - if n == 0 { break; } + if n == 0 { + break; + } buf.push_str(&String::from_utf8_lossy(&chunk[..n])); - if buf.contains("all up.") { break; } + if buf.contains("all up.") { + break; + } } - assert!(buf.contains("all up."), - "up did not become ready; stderr:\n{buf}"); + assert!( + buf.contains("all up."), + "up did not become ready; stderr:\n{buf}" + ); let port_of = Command::new(hm_bin()) .args(["dev", "port-of", "hello", "5678"]) .current_dir(tmp.path()) .output() .unwrap(); - assert!(port_of.status.success(), - "port-of failed: {}", String::from_utf8_lossy(&port_of.stderr)); + assert!( + port_of.status.success(), + "port-of failed: {}", + String::from_utf8_lossy(&port_of.stderr) + ); let host_port: u16 = String::from_utf8(port_of.stdout) .unwrap() .trim() .parse() .unwrap(); - assert!(host_port > 1024, - "expected ephemeral host port, got {host_port}"); + assert!( + host_port > 1024, + "expected ephemeral host port, got {host_port}" + ); // python -m http.server returns an HTML directory listing whose // body always contains the literal "Directory listing for /". @@ -107,9 +136,12 @@ def hello(): .current_dir(tmp.path()) .output() .unwrap(); - assert_eq!(port_of_after.status.code(), Some(4), + assert_eq!( + port_of_after.status.code(), + Some(4), "stopped slug should exit 4: {}", - String::from_utf8_lossy(&port_of_after.stderr)); + String::from_utf8_lossy(&port_of_after.stderr) + ); } fn poll_http(url: &str) -> String { From e4f5d8179353a22e4d274df50517e2cd0f021cc5 Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Sun, 24 May 2026 00:47:30 -0700 Subject: [PATCH 11/14] fix: resolve clippy lints introduced by cargo fmt Move use-items before statements (items_after_statements) and remove trailing commas in macro args (unnecessary_trailing_comma). --- crates/hm-pipeline-ir/tests/e2e_fixtures.rs | 4 ++-- crates/hm-pipeline-ir/tests/graph_serde.rs | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/hm-pipeline-ir/tests/e2e_fixtures.rs b/crates/hm-pipeline-ir/tests/e2e_fixtures.rs index 2872c0f..024ea87 100644 --- a/crates/hm-pipeline-ir/tests/e2e_fixtures.rs +++ b/crates/hm-pipeline-ir/tests/e2e_fixtures.rs @@ -166,7 +166,7 @@ fn all_fixtures_have_valid_structure() { assert!(bi + dep > 0, "{dsl}/{scenario}: no edges"); for e in g.dag().graph().edge_references() { - assert_ne!(e.source(), e.target(), "{dsl}/{scenario}: self-loop",); + assert_ne!(e.source(), e.target(), "{dsl}/{scenario}: self-loop"); } } } @@ -256,7 +256,7 @@ fn parity_env_keys() { .find(|(_, t)| t.step.label.as_deref() == Some(label)) .map(|(_, t)| t.env.keys().cloned().collect()) .unwrap(); - assert_eq!(py_env, ts_env, "parity/{scenario}/{label}: env keys",); + assert_eq!(py_env, ts_env, "parity/{scenario}/{label}: env keys"); } } } diff --git a/crates/hm-pipeline-ir/tests/graph_serde.rs b/crates/hm-pipeline-ir/tests/graph_serde.rs index 06a4f15..51ca088 100644 --- a/crates/hm-pipeline-ir/tests/graph_serde.rs +++ b/crates/hm-pipeline-ir/tests/graph_serde.rs @@ -76,13 +76,14 @@ fn build_test_graph() -> PipelineGraph { #[test] fn pipeline_graph_round_trips_through_json() { + use daggy::petgraph::visit::IntoNodeReferences; + use daggy::Walker; + let g = build_test_graph(); let json = serde_json::to_string_pretty(&g).unwrap(); let back: PipelineGraph = serde_json::from_str(&json).unwrap(); assert_eq!(back.node_count(), 3); assert_eq!(back.default_image(), Some("ubuntu:24.04")); - use daggy::Walker; - use daggy::petgraph::visit::IntoNodeReferences; let a_idx = back .dag() From 348245e89f76b48ad837d9a20a37527045d3dd38 Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Sun, 24 May 2026 00:50:37 -0700 Subject: [PATCH 12/14] fix: merge daggy imports to avoid rustfmt version ordering mismatch --- crates/hm-pipeline-ir/tests/graph_serde.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/hm-pipeline-ir/tests/graph_serde.rs b/crates/hm-pipeline-ir/tests/graph_serde.rs index 51ca088..52251db 100644 --- a/crates/hm-pipeline-ir/tests/graph_serde.rs +++ b/crates/hm-pipeline-ir/tests/graph_serde.rs @@ -76,8 +76,7 @@ fn build_test_graph() -> PipelineGraph { #[test] fn pipeline_graph_round_trips_through_json() { - use daggy::petgraph::visit::IntoNodeReferences; - use daggy::Walker; + use daggy::{Walker, petgraph::visit::IntoNodeReferences}; let g = build_test_graph(); let json = serde_json::to_string_pretty(&g).unwrap(); From 9374952d7ef4090aca157921ffeb2d12eda788f3 Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Sun, 24 May 2026 00:58:38 -0700 Subject: [PATCH 13/14] fix: use cargo test --lib in dogfood to match normal CI The full cargo test runs integration tests that need python3, which isn't available in the bare Rust Docker container. --- .harmont/ci.py | 2 +- .harmont/ci.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.harmont/ci.py b/.harmont/ci.py index 309732c..a04a144 100644 --- a/.harmont/ci.py +++ b/.harmont/ci.py @@ -31,7 +31,7 @@ def ci( ) -> tuple[hm.Step, ...]: return ( rust_project.build(), - rust_project.test(), + rust_project.run("cargo test --lib", label=":rust: test"), rust_project.clippy(), rust_project.fmt(), py_project.lint(), diff --git a/.harmont/ci.ts b/.harmont/ci.ts index 8fbd7fa..1d63144 100644 --- a/.harmont/ci.ts +++ b/.harmont/ci.ts @@ -10,7 +10,7 @@ const pipelines: PipelineDefinition[] = [ triggers: [push({ branch: "main" }), pullRequest({ branches: ["main"] })], pipeline: pipeline( rustProject.build(), - rustProject.test(), + rustProject.run("cargo test --lib", { label: ":rust: test" }), rustProject.clippy(), rustProject.fmt(), pyProject.lint(), From a18c2951a76623d2e54c30e53e43d4093474fa1d Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Sun, 24 May 2026 01:00:53 -0700 Subject: [PATCH 14/14] fix: use installed.sh() for custom cargo test --lib command RustToolchain has no run() method; use installed step directly with cargo env sourcing. --- .harmont/ci.py | 5 ++++- .harmont/ci.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.harmont/ci.py b/.harmont/ci.py index a04a144..a1afc66 100644 --- a/.harmont/ci.py +++ b/.harmont/ci.py @@ -31,7 +31,10 @@ def ci( ) -> tuple[hm.Step, ...]: return ( rust_project.build(), - rust_project.run("cargo test --lib", label=":rust: test"), + rust_project.installed.sh( + ". $HOME/.cargo/env && cd . && cargo test --lib", + label=":rust: test", + ), rust_project.clippy(), rust_project.fmt(), py_project.lint(), diff --git a/.harmont/ci.ts b/.harmont/ci.ts index 1d63144..308083e 100644 --- a/.harmont/ci.ts +++ b/.harmont/ci.ts @@ -10,7 +10,7 @@ const pipelines: PipelineDefinition[] = [ triggers: [push({ branch: "main" }), pullRequest({ branches: ["main"] })], pipeline: pipeline( rustProject.build(), - rustProject.run("cargo test --lib", { label: ":rust: test" }), + rustProject.install().sh(`. $HOME/.cargo/env && cd . && cargo test --lib`, { label: ":rust: test" }), rustProject.clippy(), rustProject.fmt(), pyProject.lint(),