pep723-to-wheel 0.1.0__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.
@@ -0,0 +1,58 @@
1
+ name: CD
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+
7
+ permissions:
8
+ contents: write
9
+ id-token: write
10
+
11
+ jobs:
12
+ publish:
13
+ if: github.actor != 'github-actions[bot]'
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ with:
18
+ fetch-depth: 0
19
+ - uses: actions/setup-python@v5
20
+ with:
21
+ python-version: "3.12"
22
+ - uses: astral-sh/setup-uv@v6
23
+ - name: Install dependencies
24
+ run: uv sync --frozen
25
+ - name: Determine version
26
+ id: version
27
+ run: |
28
+ LATEST_TAG="$(git tag --list 'v*' --sort=-v:refname | head -n1)"
29
+ export LATEST_TAG
30
+ uv run python .github/workflows/cd_version.py
31
+ - name: Commit version bump
32
+ run: |
33
+ if git status --porcelain | grep -q "pyproject.toml"; then
34
+ git config user.name "github-actions[bot]"
35
+ git config user.email "github-actions[bot]@users.noreply.github.com"
36
+ git add pyproject.toml
37
+ git commit -m "chore: bump version to v${{ steps.version.outputs.version }}"
38
+ git push
39
+ fi
40
+ - name: Tag release
41
+ run: |
42
+ VERSION="v${{ steps.version.outputs.version }}"
43
+ if git tag -l "$VERSION" | grep -q "$VERSION"; then
44
+ echo "Tag $VERSION already exists."
45
+ else
46
+ git tag "$VERSION"
47
+ git push origin "$VERSION"
48
+ fi
49
+ - name: Build
50
+ run: uv build
51
+ - name: Publish to PyPI (Trusted Publishing)
52
+ run: uv publish
53
+ - name: Create GitHub release
54
+ uses: softprops/action-gh-release@v2
55
+ with:
56
+ files: dist/*.whl
57
+ tag_name: v${{ steps.version.outputs.version }}
58
+ name: v${{ steps.version.outputs.version }}
@@ -0,0 +1,94 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import pathlib
5
+ import re
6
+ import tomllib
7
+ import tomli_w
8
+ from dataclasses import dataclass
9
+
10
+
11
+ # Strict semantic versioning pattern: MAJOR.MINOR.PATCH
12
+ # - MAJOR is either 0 (pre-1.0 semantics) or a non-zero integer without leading zeros.
13
+ # - MINOR and PATCH are non-negative integers.
14
+ # - Pre-release identifiers (e.g. "-alpha") and build metadata (e.g. "+build.1") are
15
+ # intentionally not supported, because this script only needs to handle final release
16
+ # versions when resolving and bumping versions.
17
+ VERSION_PATTERN = re.compile(r"^(?P<major>0|[1-9]\d*)\.(?P<minor>\d+)\.(?P<patch>\d+)$")
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class Version:
22
+ major: int
23
+ minor: int
24
+ patch: int
25
+
26
+ @classmethod
27
+ def parse(cls, value: str) -> "Version | None":
28
+ match = VERSION_PATTERN.match(value)
29
+ if not match:
30
+ return None
31
+ return cls(
32
+ major=int(match.group("major")),
33
+ minor=int(match.group("minor")),
34
+ patch=int(match.group("patch")),
35
+ )
36
+
37
+ def bump_patch(self) -> "Version":
38
+ return Version(self.major, self.minor, self.patch + 1)
39
+
40
+ def __str__(self) -> str:
41
+ return f"{self.major}.{self.minor}.{self.patch}"
42
+
43
+
44
+ def read_current_version(pyproject_path: pathlib.Path) -> Version:
45
+ data = tomllib.loads(pyproject_path.read_text())
46
+ current = data["project"]["version"]
47
+ parsed = Version.parse(current)
48
+ if not parsed:
49
+ raise ValueError(f"Invalid project.version in {pyproject_path}: {current}")
50
+ return parsed
51
+
52
+
53
+ def write_version(pyproject_path: pathlib.Path, version: Version) -> None:
54
+ data = tomllib.loads(pyproject_path.read_text())
55
+ project = data.get("project")
56
+ if not isinstance(project, dict):
57
+ raise ValueError(f"Missing [project] table in {pyproject_path}")
58
+ if "version" not in project:
59
+ raise ValueError(f"Missing project.version in {pyproject_path}")
60
+ project["version"] = str(version)
61
+ pyproject_path.write_text(tomli_w.dumps(data))
62
+
63
+
64
+ def resolve_version(current: Version, latest_tag: str | None) -> Version:
65
+ if not latest_tag:
66
+ return current
67
+ parsed = Version.parse(latest_tag.lstrip("v"))
68
+ if not parsed:
69
+ return current
70
+ if current.major == parsed.major and current.minor == parsed.minor:
71
+ new_patch = max(current.patch, parsed.patch) + 1
72
+ return Version(current.major, current.minor, new_patch)
73
+ return current
74
+
75
+
76
+ def main() -> None:
77
+ pyproject_path = pathlib.Path("pyproject.toml")
78
+ current = read_current_version(pyproject_path)
79
+ new_version = resolve_version(current, os.environ.get("LATEST_TAG"))
80
+
81
+ if new_version != current:
82
+ write_version(pyproject_path, new_version)
83
+
84
+ output_path = os.environ.get("GITHUB_OUTPUT")
85
+ if output_path:
86
+ with pathlib.Path(output_path).open("a") as handle:
87
+ handle.write(f"version={new_version}\n")
88
+ else:
89
+ raise RuntimeError("GITHUB_OUTPUT is not set")
90
+ print(f"Resolved version: {new_version}")
91
+
92
+
93
+ if __name__ == "__main__":
94
+ main()
@@ -0,0 +1,39 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ tests:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ python-version: ["3.12", "3.14"]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: ${{ matrix.python-version }}
20
+ - uses: astral-sh/setup-uv@v5
21
+ - name: Install dependencies
22
+ run: uv sync --dev
23
+ - name: Run tests
24
+ run: make test
25
+ - name: Upload coverage reports to Codecov
26
+ uses: codecov/codecov-action@v5
27
+ with:
28
+ token: ${{ secrets.CODECOV_TOKEN }}
29
+ files: coverage.xml
30
+ fail_ci_if_error: true
31
+ - name: Upload coverage report
32
+ uses: actions/upload-artifact@v4
33
+ with:
34
+ name: coverage-xml-${{ matrix.python-version }}
35
+ path: coverage.xml
36
+ - name: Run type checks
37
+ run: uv run ty check
38
+ - name: Run ruff
39
+ run: uv run ruff check .
@@ -0,0 +1,35 @@
1
+ name: Ruff Autofix
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - "**"
7
+
8
+ permissions:
9
+ contents: write
10
+
11
+ jobs:
12
+ autofix:
13
+ if: github.actor != 'github-actions[bot]'
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ with:
18
+ fetch-depth: 0
19
+ - uses: actions/setup-python@v5
20
+ with:
21
+ python-version: "3.12"
22
+ - uses: astral-sh/setup-uv@v5
23
+ - name: Install dependencies
24
+ run: uv sync --dev
25
+ - name: Run ruff autofix
26
+ run: uv run ruff check --fix .
27
+ - name: Commit and push changes
28
+ run: |
29
+ if [[ -n "$(git status --porcelain)" ]]; then
30
+ git config user.name "github-actions[bot]"
31
+ git config user.email "github-actions[bot]@users.noreply.github.com"
32
+ git add -A
33
+ git commit -m "chore: ruff autofix"
34
+ git push
35
+ fi
@@ -0,0 +1,65 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ dist/
13
+ *.egg-info/
14
+ .eggs/
15
+
16
+ # Installer logs
17
+ pip-log.txt
18
+ pip-delete-this-directory.txt
19
+
20
+ # Unit test / coverage reports
21
+ htmlcov/
22
+ .tox/
23
+ .nox/
24
+ .coverage
25
+ .coverage.*
26
+ .cache
27
+ nosetests.xml
28
+ coverage.xml
29
+ *.cover
30
+ *.py,cover
31
+ .hypothesis/
32
+ .pytest_cache/
33
+
34
+ # mypy
35
+ .mypy_cache/
36
+ .dmypy.json
37
+
38
+ # Pyre
39
+ .pyre/
40
+
41
+ # Ruff
42
+ .ruff_cache/
43
+
44
+ # IDEs and editors
45
+ .vscode/
46
+ .idea/
47
+
48
+ # Environments
49
+ .env
50
+ .venv/
51
+ venv/
52
+ ENV/
53
+
54
+ # Pyenv
55
+ .python-version
56
+
57
+ # OS files
58
+ .DS_Store
59
+ Thumbs.db
60
+
61
+ # Jupyter
62
+ .ipynb_checkpoints/
63
+
64
+ # marimo
65
+ __marimo__/
@@ -0,0 +1,28 @@
1
+ # Agent guide for pep723-to-wheel
2
+
3
+ ## Repository overview
4
+ - `src/pep723_to_wheel/` contains the library and CLI implementation.
5
+ - `tests/` holds pytest coverage for core behavior.
6
+ - `pyproject.toml` defines dependencies, entry points, and tooling.
7
+
8
+ ## Development setup
9
+ - Requires Python 3.12+ (see `pyproject.toml`).
10
+ - Environment management is with uv.
11
+ - Run Python and related CLI tools via `uv run` so they use the uv virtualenv.
12
+
13
+ ## Common commands
14
+ - Run tests: `make test`
15
+ - Run type checks: `make typecheck`
16
+ - Run ruff checks: `make ruff`
17
+ - Run all checks: `make all-tests`
18
+
19
+ ## Style and conventions
20
+ - TDD for all code development - write test, then run to verify it fails, then develop, then verify the test passes.
21
+ - All tasks should end by running `make all-tests` and verifying it passes.
22
+ - Prefer updating or adding pytest tests in `tests/` for behavior changes.
23
+ - For CLI changes, update both `src/pep723_to_wheel/cli.py` and any relevant tests.
24
+ - Target modern Python 3.12+ syntax, no need to be backwards compatible.
25
+
26
+ ## Tips
27
+ - Use `pep723_to_wheel.core` for the main build/import logic.
28
+ - The CLI entry point is `pep723_to_wheel.cli:app`.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Johan Carlin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,12 @@
1
+ .PHONY: test typecheck ruff all-tests
2
+
3
+ test:
4
+ uv run pytest --cov=pep723_to_wheel --cov-report=term-missing --cov-report=xml
5
+
6
+ typecheck:
7
+ uv run ty check
8
+
9
+ ruff:
10
+ uv run ruff check .
11
+
12
+ all-tests: test typecheck ruff
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.4
2
+ Name: pep723-to-wheel
3
+ Version: 0.1.0
4
+ Summary: PoC for pep-723 script to wheel and back
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.12
7
+ Requires-Dist: pydantic>=2.5
8
+ Requires-Dist: tomli-w>=1.1.0
9
+ Requires-Dist: typer>=0.21.1
10
+ Description-Content-Type: text/markdown
11
+
12
+ # pep723-to-wheel
13
+
14
+ [![CI](https://github.com/jooh/pep723-to-wheel/actions/workflows/ci.yml/badge.svg)](https://github.com/jooh/pep723-to-wheel/actions/workflows/ci.yml)
15
+ [![codecov](https://codecov.io/gh/jooh/pep723-to-wheel/branch/main/graph/badge.svg?token=PS0IS5TVBV)](https://codecov.io/gh/jooh/pep723-to-wheel)
16
+
17
+ A small utility for converting [PEP 723](https://peps.python.org/pep-0723/) inline dependency scripts into wheels and reconstructing scripts from wheels. Especially useful for taking [reproducible Marimo notebooks](https://marimo.io/blog/sandboxed-notebooks) to production environments.
18
+
19
+ ## CLI
20
+
21
+ Build a wheel from a script that has a PEP 723 inline block:
22
+
23
+ ```bash
24
+ pep723-to-wheel build path/to/script.py --output-dir dist
25
+ ```
26
+
27
+ Set an explicit wheel version (defaults to calendar versioning using the script mtime as the patch segment):
28
+
29
+ ```bash
30
+ pep723-to-wheel build path/to/script.py --version 2024.12.25
31
+ ```
32
+
33
+ Reconstruct a script from a wheel or package name:
34
+
35
+ ```bash
36
+ pep723-to-wheel import path/to/package.whl --output reconstructed.py
37
+ pep723-to-wheel import requests --output reconstructed.py
38
+ ```
39
+
40
+ ## Library
41
+
42
+ ```python
43
+ from pathlib import Path
44
+ from pep723_to_wheel import build_script_to_wheel, import_wheel_to_script
45
+
46
+ result = build_script_to_wheel(Path("script.py"))
47
+ print(result.wheel_path)
48
+
49
+ import_result = import_wheel_to_script("requests", Path("reconstructed.py"))
50
+ print(import_result.script_path)
51
+ ```
52
+
53
+ ## Development
54
+
55
+ ```bash
56
+ make test
57
+ make typecheck
58
+ make ruff
59
+ ```
60
+
61
+ ## Release process
62
+
63
+ Releases are automated on pushes to `main` by the CD workflow in `.github/workflows/cd.yml`.
64
+
65
+ 1. The workflow determines the latest `v*` Git tag and runs `.github/workflows/cd_version.py` to
66
+ resolve the next version. If the latest tag matches the current major/minor, it bumps the patch
67
+ to one higher than the max of the current patch and the tag patch.
68
+ 2. If `pyproject.toml` changes, the workflow commits the version bump back to `main`.
69
+ 3. It tags the release as `v<version>`, builds the wheel with `uv build --wheel`, publishes to
70
+ PyPI, and creates a GitHub release with the wheel attached.
71
+
72
+ To trigger a release, merge or push changes to `main` and ensure `PYPI_API_TOKEN` is configured in
73
+ the repository secrets.
@@ -0,0 +1,62 @@
1
+ # pep723-to-wheel
2
+
3
+ [![CI](https://github.com/jooh/pep723-to-wheel/actions/workflows/ci.yml/badge.svg)](https://github.com/jooh/pep723-to-wheel/actions/workflows/ci.yml)
4
+ [![codecov](https://codecov.io/gh/jooh/pep723-to-wheel/branch/main/graph/badge.svg?token=PS0IS5TVBV)](https://codecov.io/gh/jooh/pep723-to-wheel)
5
+
6
+ A small utility for converting [PEP 723](https://peps.python.org/pep-0723/) inline dependency scripts into wheels and reconstructing scripts from wheels. Especially useful for taking [reproducible Marimo notebooks](https://marimo.io/blog/sandboxed-notebooks) to production environments.
7
+
8
+ ## CLI
9
+
10
+ Build a wheel from a script that has a PEP 723 inline block:
11
+
12
+ ```bash
13
+ pep723-to-wheel build path/to/script.py --output-dir dist
14
+ ```
15
+
16
+ Set an explicit wheel version (defaults to calendar versioning using the script mtime as the patch segment):
17
+
18
+ ```bash
19
+ pep723-to-wheel build path/to/script.py --version 2024.12.25
20
+ ```
21
+
22
+ Reconstruct a script from a wheel or package name:
23
+
24
+ ```bash
25
+ pep723-to-wheel import path/to/package.whl --output reconstructed.py
26
+ pep723-to-wheel import requests --output reconstructed.py
27
+ ```
28
+
29
+ ## Library
30
+
31
+ ```python
32
+ from pathlib import Path
33
+ from pep723_to_wheel import build_script_to_wheel, import_wheel_to_script
34
+
35
+ result = build_script_to_wheel(Path("script.py"))
36
+ print(result.wheel_path)
37
+
38
+ import_result = import_wheel_to_script("requests", Path("reconstructed.py"))
39
+ print(import_result.script_path)
40
+ ```
41
+
42
+ ## Development
43
+
44
+ ```bash
45
+ make test
46
+ make typecheck
47
+ make ruff
48
+ ```
49
+
50
+ ## Release process
51
+
52
+ Releases are automated on pushes to `main` by the CD workflow in `.github/workflows/cd.yml`.
53
+
54
+ 1. The workflow determines the latest `v*` Git tag and runs `.github/workflows/cd_version.py` to
55
+ resolve the next version. If the latest tag matches the current major/minor, it bumps the patch
56
+ to one higher than the max of the current patch and the tag patch.
57
+ 2. If `pyproject.toml` changes, the workflow commits the version bump back to `main`.
58
+ 3. It tags the release as `v<version>`, builds the wheel with `uv build --wheel`, publishes to
59
+ PyPI, and creates a GitHub release with the wheel attached.
60
+
61
+ To trigger a release, merge or push changes to `main` and ensure `PYPI_API_TOKEN` is configured in
62
+ the repository secrets.
@@ -0,0 +1,41 @@
1
+ [project]
2
+ name = "pep723-to-wheel"
3
+ version = "0.1.0"
4
+ description = "PoC for pep-723 script to wheel and back"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "pydantic>=2.5",
9
+ "tomli-w>=1.1.0",
10
+ "typer>=0.21.1",
11
+ ]
12
+
13
+ [project.scripts]
14
+ pep723-to-wheel = "pep723_to_wheel.cli:app"
15
+
16
+ [build-system]
17
+ requires = ["hatchling>=1.27.0"]
18
+ build-backend = "hatchling.build"
19
+
20
+ [tool.hatch.build.targets.wheel]
21
+ packages = ["src/pep723_to_wheel"]
22
+
23
+ [dependency-groups]
24
+ dev = [
25
+ "pytest>=9.0.2",
26
+ "pytest-cov>=6.0.0",
27
+ "ruff>=0.9.10",
28
+ "ty>=0.0.14",
29
+ ]
30
+
31
+ [tool.ty.src]
32
+ exclude = ["tests/examples/**"]
33
+
34
+ [tool.coverage.run]
35
+ branch = true
36
+ source = ["pep723_to_wheel"]
37
+
38
+ [tool.coverage.report]
39
+ fail_under = 100
40
+ show_missing = true
41
+ skip_covered = true
@@ -0,0 +1,5 @@
1
+ """Library for converting PEP 723 scripts to wheels and back."""
2
+
3
+ from pep723_to_wheel.core import build_script_to_wheel, import_wheel_to_script
4
+
5
+ __all__ = ["build_script_to_wheel", "import_wheel_to_script"]
@@ -0,0 +1,45 @@
1
+ """Typer-based CLI entry points."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import typer
8
+
9
+ from pep723_to_wheel.core import build_script_to_wheel, import_wheel_to_script
10
+
11
+ app = typer.Typer(add_completion=False, no_args_is_help=True)
12
+
13
+
14
+ @app.command("build")
15
+ def build_command(
16
+ script_path: Path = typer.Argument(..., help="Path to the PEP 723 script."),
17
+ output_dir: Path | None = typer.Option(
18
+ None, "--output-dir", "-o", help="Directory for the built wheel."
19
+ ),
20
+ version: str | None = typer.Option(
21
+ None,
22
+ "--version",
23
+ "-v",
24
+ help="Wheel version (defaults to calendar versioning).",
25
+ ),
26
+ ) -> None:
27
+ """Build a wheel from a PEP 723 script."""
28
+
29
+ result = build_script_to_wheel(script_path, output_dir, version)
30
+ typer.echo(str(result.wheel_path))
31
+
32
+
33
+ @app.command("import")
34
+ def import_command(
35
+ wheel_or_package: str = typer.Argument(
36
+ ..., help="Wheel path or package name to import."
37
+ ),
38
+ output_path: Path = typer.Option(
39
+ ..., "--output", "-o", help="Path to write the reconstructed script."
40
+ ),
41
+ ) -> None:
42
+ """Reconstruct a script from a wheel or package name."""
43
+
44
+ result = import_wheel_to_script(wheel_or_package, output_path)
45
+ typer.echo(str(result.script_path))