python-devkit 0.1.0__tar.gz → 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. {python_devkit-0.1.0 → python_devkit-0.1.1}/.github/workflows/release.yml +1 -1
  2. python_devkit-0.1.1/AGENTS.md +30 -0
  3. python_devkit-0.1.1/PKG-INFO +11 -0
  4. python_devkit-0.1.1/README.md +50 -0
  5. {python_devkit-0.1.0 → python_devkit-0.1.1}/pyproject.toml +8 -4
  6. python_devkit-0.1.1/src/python_devkit/lock_python.py +130 -0
  7. python_devkit-0.1.1/src/python_devkit/preflight_runner.py +43 -0
  8. python_devkit-0.1.1/uv.lock +319 -0
  9. python_devkit-0.1.0/AGENTS.md +0 -31
  10. python_devkit-0.1.0/PKG-INFO +0 -10
  11. python_devkit-0.1.0/README.md +0 -34
  12. python_devkit-0.1.0/src/python_devkit/ci_step.py +0 -52
  13. python_devkit-0.1.0/src/python_devkit/setup.py +0 -127
  14. python_devkit-0.1.0/src/python_devkit/setup_oras.py +0 -51
  15. python_devkit-0.1.0/src/python_devkit/third-party/README.md +0 -9
  16. python_devkit-0.1.0/src/python_devkit/third-party/dotnet-install.sh +0 -1887
  17. {python_devkit-0.1.0 → python_devkit-0.1.1}/.github/workflows/ci.yml +0 -0
  18. {python_devkit-0.1.0 → python_devkit-0.1.1}/.gitignore +0 -0
  19. {python_devkit-0.1.0 → python_devkit-0.1.1}/.python-version +0 -0
  20. {python_devkit-0.1.0 → python_devkit-0.1.1}/CLAUDE.md +0 -0
  21. {python_devkit-0.1.0 → python_devkit-0.1.1}/LICENSE +0 -0
  22. {python_devkit-0.1.0 → python_devkit-0.1.1}/NOTICE +0 -0
  23. {python_devkit-0.1.0 → python_devkit-0.1.1}/publish-config.json +0 -0
  24. {python_devkit-0.1.0 → python_devkit-0.1.1}/ruff.toml +0 -0
  25. {python_devkit-0.1.0 → python_devkit-0.1.1}/src/python_devkit/__init__.py +0 -0
  26. {python_devkit-0.1.0 → python_devkit-0.1.1}/src/python_devkit/py.typed +0 -0
@@ -22,4 +22,4 @@ jobs:
22
22
  - uses: astral-sh/setup-uv@v7
23
23
 
24
24
  - name: Publish
25
- run: uvx --from release-devkit==0.1.0 publish-packages --config publish-config.json
25
+ run: uvx --from release-devkit==0.1.1 publish-packages --config publish-config.json
@@ -0,0 +1,30 @@
1
+ # python-devkit
2
+
3
+ The python repo-lifecycle devkit — workspace locking (`lock-python`), the preflight check runner, and the canonical ruff configuration with its sync verb and drift gate, per the tooling-consolidation program. Purely python-domain by charter: the CI runner floor (step wrapper, runner provisioning, git tags, OCI cache) lives in [`ci-devkit`](https://github.com/outernet-foundation/ci-devkit), which this package depends on (`ci-devkit>=0.1.0`).
4
+
5
+ The package is `python_devkit` (src-layout under `src/python_devkit/`); all dependencies resolve from PyPI (`bashrun`, `ci-devkit`, `pydantic`, `typer`; git-source pins only in scratch branches testing unreleased changes).
6
+
7
+ ## Shape
8
+
9
+ Entry point (`[project.scripts]`): `lock-python` → `lock_python.py:app`. The command function doubles as the library API — consumer preflights import `lock_python` and call `lock_python(check=True)` directly.
10
+
11
+ `lock_python.py` regenerates a uv workspace's `uv.lock` plus per-service `pylock.toml` exports. Discovery: members of `[tool.uv.workspace]` in the root `pyproject.toml` that carry a `Dockerfile`; each gets `pylock.toml` (`--no-default-groups`) and one `pylock.<group>.toml` (`--only-group`) per non-`dev` dependency group. Redirect: a group matching a key of `[tool.python-devkit.lock-python] group-export-dirs` (glob → repo-relative directory) exports into that directory instead of the member's, once, deduplicated across members — the generalization of placeframe's neural-networks accelerator groups, which several members declare identically but only a non-member base image consumes. `--check` compares normalized exports against committed files and exits non-zero on any staleness.
12
+
13
+ `preflight_runner.py` is the labeled-check runner consumer preflights thin into: `run_checks(checks)` executes a declarative list of `CommandCheck` (label + shell command under a `ci_step` group) and `GeneratedCheck` (label + generate command + pathspec + fix command — runs the generator, then fails on any `git status --porcelain` dirt under the paths, showing the diff and the fix command). Fail-fast; each check's duration lands in the Actions step summary via `ci_step`.
14
+
15
+ ## Constraints
16
+
17
+ - **The module surface is cross-repo Python API.** `python_devkit.lock_python.lock_python(check, root)` and `python_devkit.preflight_runner.run_checks(checks)` are contract: placeframe's preflight calls both in-process, the lock-python entry point is on CI's path, and the `[tool.python-devkit.lock-python]` table in consumer root pyprojects is its declarative config surface.
18
+ - **`group-export-dirs` is devkit-owned and `extra="forbid"`.** A typo'd key in that table fails validation loudly rather than silently dropping the redirect (the failure mode is per-group pylocks appearing in member directories).
19
+ - **The redirect contract assumes identical group declarations.** Dedup is first-member-wins; members declaring the same redirected group differently get whichever member enumerates first, silently.
20
+ - **Export commands are byte-stability load-bearing.** The exact `uv export` flag order must not churn — committed pylocks are compared byte-for-byte (after newline normalization), so a cosmetic flag change ripples into every consumer's diff.
21
+ - **uv invocations run with `cwd=root`.** `--root` (default: current directory) exists so the verb and the library call work from anywhere; the uv commands themselves must execute inside the workspace root.
22
+
23
+ ## Release flow
24
+
25
+ `release.yml` (workflow_run-gated on CI) publishes via release-devkit uvx-isolated under OIDC trusted publishing (publisher bound to `release.yml`, no environment); the first release is 0.1.0 on the fresh `python-devkit-v*` tag ledger. The committed `pyproject.toml` version is permanently the `0.0.0.dev0` sentinel; the tags are the version ledger. API-breaking changes ship with a manually bumped version — patch-auto assumes additive changes.
26
+
27
+ ## See also
28
+
29
+ - `README.md` — human-facing overview.
30
+ - [tooling-consolidation.md](https://github.com/outernet-foundation/placeframe/blob/dev/design/tooling-consolidation.md) — the program this repo belongs to.
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.5
2
+ Name: python-devkit
3
+ Version: 0.1.1
4
+ Summary: Python repo-lifecycle tooling: workspace locking, preflight checks, canonical ruff configuration
5
+ License-File: LICENSE
6
+ License-File: NOTICE
7
+ Requires-Python: >=3.13
8
+ Requires-Dist: bashrun>=0.1.0
9
+ Requires-Dist: ci-devkit>=0.1.0
10
+ Requires-Dist: pydantic>=2.11.7
11
+ Requires-Dist: typer>=0.17.4
@@ -0,0 +1,50 @@
1
+ # python-devkit
2
+
3
+ Python repo-lifecycle tooling shared across outernet-foundation repos: workspace locking (`lock-python`), the preflight check runner, and the canonical ruff configuration with its sync verb and drift gate. Sibling of `ci-devkit` (the CI runner floor it will depend on), `unity-devkit`, `docker-devkit`, and `release-devkit`.
4
+
5
+ ## lock-python
6
+
7
+ Regenerates a uv workspace's `uv.lock` and the per-service `pylock.toml` exports consumed by service Dockerfiles:
8
+
9
+ ```bash
10
+ uv run lock-python # write: uv lock + every export
11
+ uv run lock-python --check # validate; exit 1 on any stale lock
12
+ ```
13
+
14
+ For every workspace member that carries a `Dockerfile`, it exports `pylock.toml` (no default groups) plus one `pylock.<group>.toml` per non-`dev` dependency group. Dependency groups whose names match a pattern in the root `pyproject.toml`'s `[tool.python-devkit.lock-python] group-export-dirs` map export into the mapped directory instead, once, deduplicated across members — e.g. placeframe's accelerator groups:
15
+
16
+ ```toml
17
+ [tool.python-devkit.lock-python]
18
+ group-export-dirs = { "neural-networks-*" = "docker/neural-networks-base" }
19
+ ```
20
+
21
+ ## preflight runner
22
+
23
+ Consumer preflights thin into a declarative check list over `run_checks`:
24
+
25
+ ```python
26
+ from python_devkit.preflight_runner import CommandCheck, GeneratedCheck, run_checks
27
+
28
+ run_checks([
29
+ CommandCheck(label="Lint", command="uv run ruff check ."),
30
+ GeneratedCheck(
31
+ label="Check client codegen",
32
+ generate_command="uv run generate-clients",
33
+ paths=[Path("packages/generated/")],
34
+ fix_command="uv run generate-clients",
35
+ ),
36
+ ])
37
+ ```
38
+
39
+ `CommandCheck` wraps a shell command in a labeled CI step group; `GeneratedCheck` additionally guards the checked-in generator output with a git-status staleness check (diff shown, fix command named). Checks run fail-fast with per-step durations in the Actions step summary.
40
+
41
+ ## Development
42
+
43
+ Requires Python 3.13+ and [uv](https://docs.astral.sh/uv/).
44
+
45
+ ```bash
46
+ uv sync
47
+ uv run ruff check .
48
+ uv run ruff format --check .
49
+ uv run basedpyright
50
+ ```
@@ -1,14 +1,18 @@
1
1
  [project]
2
2
  name = "python-devkit"
3
- version = "0.1.0"
4
- description = "Python repo-lifecycle tooling: CI step wrapper, runner provisioning, workspace locking, preflight checks"
3
+ version = "0.1.1"
4
+ description = "Python repo-lifecycle tooling: workspace locking, preflight checks, canonical ruff configuration"
5
5
  requires-python = ">=3.13"
6
6
  dependencies = [
7
7
  "bashrun>=0.1.0",
8
+ "ci-devkit>=0.1.0",
8
9
  "pydantic>=2.11.7",
9
- "pydantic-settings>=2.9.1",
10
+ "typer>=0.17.4",
10
11
  ]
11
12
 
13
+ [project.scripts]
14
+ lock-python = "python_devkit.lock_python:app"
15
+
12
16
  [dependency-groups]
13
17
  dev = ["basedpyright>=1.39.10", "ruff>=0.14.11"]
14
18
 
@@ -18,7 +22,7 @@ build-backend = "hatchling.build"
18
22
 
19
23
  [tool.hatch.build.targets.wheel]
20
24
  packages = ["src/python_devkit"]
21
- include = ["src/python_devkit/py.typed", "src/python_devkit/third-party/**"]
25
+ include = ["src/python_devkit/py.typed"]
22
26
 
23
27
  [tool.uv.sources]
24
28
 
@@ -0,0 +1,130 @@
1
+ from __future__ import annotations
2
+
3
+ from fnmatch import fnmatch
4
+ from pathlib import Path
5
+ from tomllib import load
6
+ from typing import Annotated, Any
7
+
8
+ import typer
9
+ from bashrun import bash, bash_check, bash_output
10
+ from pydantic import BaseModel, ConfigDict, Field
11
+
12
+ app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
13
+
14
+
15
+ class LockPythonConfig(BaseModel):
16
+ model_config = ConfigDict(extra="forbid")
17
+
18
+ group_export_dirs: dict[str, Path] = Field(default_factory=dict, alias="group-export-dirs")
19
+
20
+
21
+ class WorkspaceConfig(BaseModel):
22
+ members: list[str] = Field(default_factory=list)
23
+
24
+
25
+ class ProjectConfig(BaseModel):
26
+ name: str
27
+
28
+
29
+ class PackageConfig(BaseModel):
30
+ project: ProjectConfig
31
+ dependency_groups: dict[str, Any] = Field(default_factory=dict, alias="dependency-groups")
32
+
33
+
34
+ @app.command()
35
+ def lock_python(
36
+ check: Annotated[
37
+ bool, typer.Option("--check", help="Validate lock files without writing; exit non-zero if stale.")
38
+ ] = False,
39
+ root: Annotated[Path, typer.Option(help="uv workspace root; defaults to the current directory.")] = Path(),
40
+ ) -> None:
41
+ root = root.resolve()
42
+ stale = False
43
+
44
+ if check:
45
+ print("Checking uv.lock...")
46
+ if not bash_check("uv lock --check", cwd=root):
47
+ print(" STALE: uv.lock is out of date. Run 'uv run lock-python' to update.")
48
+ stale = True
49
+ else:
50
+ print(" OK")
51
+ else:
52
+ bash("uv lock", cwd=root)
53
+
54
+ with (root / "pyproject.toml").open("rb") as file:
55
+ workspace_toml = load(file)
56
+
57
+ tool = workspace_toml.get("tool", {})
58
+ workspace = WorkspaceConfig.model_validate(tool.get("uv", {}).get("workspace", {}))
59
+ config = LockPythonConfig.model_validate(tool.get("python-devkit", {}).get("lock-python", {}))
60
+
61
+ seen_redirected: set[str] = set()
62
+
63
+ for member in workspace.members:
64
+ member_dir = root / member
65
+
66
+ if not (member_dir / "Dockerfile").exists():
67
+ continue
68
+
69
+ with (member_dir / "pyproject.toml").open("rb") as file:
70
+ package = PackageConfig.model_validate(load(file))
71
+
72
+ stale |= _export_pylock(check, root, member_dir, package.project.name, group=None)
73
+
74
+ for group in package.dependency_groups:
75
+ if group == "dev":
76
+ continue
77
+ redirect = _redirect_dir(group, config, root)
78
+ if redirect is None:
79
+ stale |= _export_pylock(check, root, member_dir, package.project.name, group=group)
80
+ continue
81
+ if group in seen_redirected:
82
+ continue
83
+ seen_redirected.add(group)
84
+ stale |= _export_pylock(check, root, redirect, package.project.name, group=group)
85
+
86
+ if check and stale:
87
+ raise SystemExit(1)
88
+
89
+ if check:
90
+ print("\nAll Python lock files are up to date.")
91
+
92
+
93
+ def _export_pylock(check: bool, root: Path, export_dir: Path, package_name: str, group: str | None) -> bool:
94
+ if group:
95
+ pylock = export_dir / f"pylock.{group}.toml"
96
+ group_flags = f"--only-group {group} "
97
+ else:
98
+ pylock = export_dir / "pylock.toml"
99
+ group_flags = "--no-default-groups "
100
+
101
+ export_command = (
102
+ f"uv export --format pylock.toml --no-header --package {package_name} {group_flags}--no-emit-local --frozen "
103
+ )
104
+
105
+ if check:
106
+ print(f"Checking {pylock}...")
107
+ exported = _normalize_line_endings(bash_output(export_command, cwd=root))
108
+ committed = _normalize_line_endings(pylock.read_text(encoding="utf-8")) if pylock.exists() else ""
109
+ if exported != committed:
110
+ print(f" STALE: {pylock} is out of date.")
111
+ return True
112
+ print(" OK")
113
+ return False
114
+
115
+ bash(export_command + f"--output-file {pylock} ", cwd=root)
116
+ text = pylock.read_text(encoding="utf-8")
117
+ with pylock.open("w", encoding="utf-8", newline="\n") as file:
118
+ file.write(text)
119
+ return False
120
+
121
+
122
+ def _normalize_line_endings(text: str) -> str:
123
+ return text.replace("\r\n", "\n").replace("\r", "\n")
124
+
125
+
126
+ def _redirect_dir(group: str, config: LockPythonConfig, root: Path) -> Path | None:
127
+ for pattern, directory in config.group_export_dirs.items():
128
+ if fnmatch(group, pattern):
129
+ return root / directory
130
+ return None
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+ from pathlib import Path
5
+
6
+ from bashrun import bash, bash_output
7
+ from ci_devkit.ci_step import ci_step
8
+ from pydantic import BaseModel
9
+
10
+
11
+ class CommandCheck(BaseModel):
12
+ label: str
13
+ command: str
14
+
15
+
16
+ class GeneratedCheck(BaseModel):
17
+ label: str
18
+ generate_command: str
19
+ paths: list[Path]
20
+ fix_command: str
21
+
22
+
23
+ def run_checks(checks: Sequence[CommandCheck | GeneratedCheck], *, cwd: Path | None = None) -> None:
24
+ for check in checks:
25
+ if isinstance(check, CommandCheck):
26
+ _run_command_check(check, cwd)
27
+ else:
28
+ _run_generated_check(check, cwd)
29
+
30
+
31
+ def _run_command_check(check: CommandCheck, cwd: Path | None) -> None:
32
+ with ci_step(check.label):
33
+ bash(check.command, cwd=cwd)
34
+
35
+
36
+ def _run_generated_check(check: GeneratedCheck, cwd: Path | None) -> None:
37
+ with ci_step(check.label):
38
+ bash(check.generate_command, cwd=cwd)
39
+ pathspec = " ".join(str(path) for path in check.paths)
40
+ staleness_output = bash_output(f"git status --porcelain -- {pathspec}", cwd=cwd)
41
+ if staleness_output.strip():
42
+ bash(f"git diff -- {pathspec}", cwd=cwd)
43
+ raise SystemExit(f"{check.label} output is stale. Run '{check.fix_command}' locally and commit the result.")