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.
- {python_devkit-0.1.0 → python_devkit-0.1.1}/.github/workflows/release.yml +1 -1
- python_devkit-0.1.1/AGENTS.md +30 -0
- python_devkit-0.1.1/PKG-INFO +11 -0
- python_devkit-0.1.1/README.md +50 -0
- {python_devkit-0.1.0 → python_devkit-0.1.1}/pyproject.toml +8 -4
- python_devkit-0.1.1/src/python_devkit/lock_python.py +130 -0
- python_devkit-0.1.1/src/python_devkit/preflight_runner.py +43 -0
- python_devkit-0.1.1/uv.lock +319 -0
- python_devkit-0.1.0/AGENTS.md +0 -31
- python_devkit-0.1.0/PKG-INFO +0 -10
- python_devkit-0.1.0/README.md +0 -34
- python_devkit-0.1.0/src/python_devkit/ci_step.py +0 -52
- python_devkit-0.1.0/src/python_devkit/setup.py +0 -127
- python_devkit-0.1.0/src/python_devkit/setup_oras.py +0 -51
- python_devkit-0.1.0/src/python_devkit/third-party/README.md +0 -9
- python_devkit-0.1.0/src/python_devkit/third-party/dotnet-install.sh +0 -1887
- {python_devkit-0.1.0 → python_devkit-0.1.1}/.github/workflows/ci.yml +0 -0
- {python_devkit-0.1.0 → python_devkit-0.1.1}/.gitignore +0 -0
- {python_devkit-0.1.0 → python_devkit-0.1.1}/.python-version +0 -0
- {python_devkit-0.1.0 → python_devkit-0.1.1}/CLAUDE.md +0 -0
- {python_devkit-0.1.0 → python_devkit-0.1.1}/LICENSE +0 -0
- {python_devkit-0.1.0 → python_devkit-0.1.1}/NOTICE +0 -0
- {python_devkit-0.1.0 → python_devkit-0.1.1}/publish-config.json +0 -0
- {python_devkit-0.1.0 → python_devkit-0.1.1}/ruff.toml +0 -0
- {python_devkit-0.1.0 → python_devkit-0.1.1}/src/python_devkit/__init__.py +0 -0
- {python_devkit-0.1.0 → python_devkit-0.1.1}/src/python_devkit/py.typed +0 -0
|
@@ -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.
|
|
4
|
-
description = "Python repo-lifecycle tooling:
|
|
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
|
-
"
|
|
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"
|
|
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.")
|