Add versioning scripts and update README for packaging instructions

This commit is contained in:
claudi 2026-04-07 11:37:43 +02:00
parent 904f4e487e
commit 2be62e36fd
5 changed files with 257 additions and 0 deletions

View file

@ -43,6 +43,32 @@ Install development dependencies when you want tests and code generation tooling
& .\.venv\Scripts\python.exe -m pip install -e .[dev] & .\.venv\Scripts\python.exe -m pip install -e .[dev]
``` ```
## Packaging
Print the current package version:
```powershell
& .\.venv\Scripts\python.exe .\scripts\set_version.py --print-current
```
Update the package version in `pyproject.toml`:
```powershell
& .\.venv\Scripts\python.exe .\scripts\set_version.py 0.1.1
```
Build a wheel into `dist\`:
```powershell
& .\.venv\Scripts\python.exe .\scripts\build_wheel.py
```
Set the version and build the wheel in one step:
```powershell
& .\.venv\Scripts\python.exe .\scripts\build_wheel.py --version 0.1.1
```
Create a client with your eBay credentials: Create a client with your eBay credentials:
```python ```python

View file

@ -17,6 +17,7 @@ dependencies = [
[project.optional-dependencies] [project.optional-dependencies]
dev = [ dev = [
"build>=1.2,<2",
"datamodel-code-generator>=0.28,<0.31", "datamodel-code-generator>=0.28,<0.31",
"pytest>=8,<9", "pytest>=8,<9",
"pytest-httpx>=0.35,<0.36", "pytest-httpx>=0.35,<0.36",

87
scripts/build_wheel.py Normal file
View file

@ -0,0 +1,87 @@
from __future__ import annotations
import argparse
import importlib.util
import shutil
import subprocess
import sys
from pathlib import Path
from set_version import PYPROJECT_PATH, extract_project_version, write_project_version
ROOT = Path(__file__).resolve().parent.parent
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Build a wheel for the project.")
parser.add_argument("--version", help="Optionally set the project version before building.")
parser.add_argument("--outdir", default="dist", help="Directory where the wheel will be written.")
parser.add_argument("--no-clean", action="store_true", help="Keep existing build and output directories.")
return parser.parse_args()
def resolve_output_dir(outdir: str) -> Path:
output_dir = Path(outdir)
if not output_dir.is_absolute():
output_dir = ROOT / output_dir
return output_dir
def ensure_build_dependency() -> None:
if importlib.util.find_spec("build") is None:
raise RuntimeError(
'Missing dependency "build". Install development dependencies with '\
'"python -m pip install -e .[dev]" or install "build" directly.'
)
def clean_build_artifacts(output_dir: Path) -> None:
for path in (ROOT / "build", output_dir):
if path.exists():
shutil.rmtree(path)
def build_wheel(output_dir: Path) -> None:
command = [
sys.executable,
"-m",
"build",
"--wheel",
"--outdir",
str(output_dir),
]
subprocess.run(command, check=True, cwd=ROOT)
def main() -> int:
args = parse_args()
output_dir = resolve_output_dir(args.outdir)
if args.version:
previous_version, updated_version = write_project_version(PYPROJECT_PATH, args.version)
if previous_version == updated_version:
print(f"Version already set to {updated_version}")
else:
print(f"Updated version: {previous_version} -> {updated_version}")
ensure_build_dependency()
if not args.no_clean:
clean_build_artifacts(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
version = extract_project_version(PYPROJECT_PATH.read_text(encoding="utf-8"))
print(f"Building wheel for version {version} into {output_dir}")
build_wheel(output_dir)
wheels = sorted(output_dir.glob("*.whl"))
if not wheels:
raise RuntimeError("Build completed without producing a wheel file.")
for wheel in wheels:
print(f"Built wheel: {wheel}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

88
scripts/set_version.py Normal file
View file

@ -0,0 +1,88 @@
from __future__ import annotations
import argparse
import re
import tomllib
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
PYPROJECT_PATH = ROOT / "pyproject.toml"
PROJECT_VERSION_PATTERN = re.compile(
r'(?ms)(^\[project\]\s*$.*?^version\s*=\s*")([^"\r\n]+)(")'
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Update the project version in pyproject.toml.")
parser.add_argument("version", nargs="?", help="New project version.")
parser.add_argument("--print-current", action="store_true", help="Print the current project version.")
args = parser.parse_args()
if not args.print_current and not args.version:
parser.error("Provide a version or use --print-current.")
return args
def validate_version(version: str) -> str:
normalized = version.strip()
if not normalized:
raise ValueError("Version cannot be empty.")
if normalized != version:
raise ValueError("Version cannot start or end with whitespace.")
if any(character.isspace() for character in version):
raise ValueError("Version cannot contain whitespace.")
if '"' in version or "'" in version:
raise ValueError("Version cannot contain quote characters.")
return version
def extract_project_version(pyproject_text: str) -> str:
match = PROJECT_VERSION_PATTERN.search(pyproject_text)
if match is None:
raise ValueError("Could not find [project].version in pyproject.toml.")
return match.group(2)
def set_project_version_in_text(pyproject_text: str, new_version: str) -> str:
validate_version(new_version)
def replace(match: re.Match[str]) -> str:
return f"{match.group(1)}{new_version}{match.group(3)}"
updated_text, replacements = PROJECT_VERSION_PATTERN.subn(replace, pyproject_text, count=1)
if replacements != 1:
raise ValueError("Could not update [project].version in pyproject.toml.")
tomllib.loads(updated_text)
return updated_text
def write_project_version(pyproject_path: Path, new_version: str) -> tuple[str, str]:
original_text = pyproject_path.read_text(encoding="utf-8")
current_version = extract_project_version(original_text)
updated_text = set_project_version_in_text(original_text, new_version)
if updated_text != original_text:
pyproject_path.write_text(updated_text, encoding="utf-8")
return current_version, new_version
def main() -> int:
args = parse_args()
pyproject_text = PYPROJECT_PATH.read_text(encoding="utf-8")
if args.print_current:
print(extract_project_version(pyproject_text))
return 0
previous_version, updated_version = write_project_version(PYPROJECT_PATH, args.version)
if previous_version == updated_version:
print(f"Version already set to {updated_version}")
else:
print(f"Updated version: {previous_version} -> {updated_version}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -0,0 +1,55 @@
from __future__ import annotations
import sys
import tomllib
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "scripts"))
from set_version import extract_project_version, set_project_version_in_text, write_project_version
def test_extract_project_version_reads_project_version() -> None:
pyproject_text = (
"[build-system]\n"
'requires = ["setuptools>=69", "wheel"]\n\n'
"[project]\n"
'name = "ebay-rest-client"\n'
'version = "0.1.0"\n'
)
assert extract_project_version(pyproject_text) == "0.1.0"
def test_set_project_version_in_text_updates_project_version_only() -> None:
pyproject_text = (
"[build-system]\n"
'requires = ["setuptools>=69", "wheel"]\n\n'
"[project]\n"
'name = "ebay-rest-client"\n'
'version = "0.1.0"\n'
'description = "test package"\n'
)
updated_text = set_project_version_in_text(pyproject_text, "1.2.3")
assert extract_project_version(updated_text) == "1.2.3"
assert tomllib.loads(updated_text)["project"]["version"] == "1.2.3"
def test_write_project_version_updates_file(tmp_path: Path) -> None:
pyproject_path = tmp_path / "pyproject.toml"
pyproject_path.write_text(
"[project]\n"
'name = "ebay-rest-client"\n'
'version = "0.1.0"\n',
encoding="utf-8",
)
previous_version, updated_version = write_project_version(pyproject_path, "2.0.0")
assert previous_version == "0.1.0"
assert updated_version == "2.0.0"
assert tomllib.loads(pyproject_path.read_text(encoding="utf-8"))["project"]["version"] == "2.0.0"