From 001a471549b8d3ad322f133a1ef58fef61110374 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Tue, 16 Jun 2026 13:45:08 -0400 Subject: [PATCH] packaging: ship to PyPI via tag + GitLab CI (mirrors ago's pattern) pyproject.toml gains the metadata PyPI expects: - Real author name + email - Keywords (erlang, elixir, distribution, gen_server, ...) - Classifiers (status, license, Python versions, topic) - Project URLs (Homepage / Repository / Bug Tracker) - readme content-type = text/markdown so PyPI renders our README - build + twine added to the [dev] extra .gitlab-ci.yml mirrors ago's two-stage pattern: - test stage on every push (venv + pip install -e .[dev] + pytest) - pypi-twine stage only on tags: python -m build, twine check, twine upload. Credentials come from GitLab CI variables TWINE_USERNAME (typically __token__) and TWINE_PASSWORD. Makefile gains build / dist-check / publish-test / publish targets so the release flow is also runnable locally if a dry run is needed. Release flow: 1. Bump version in pyproject.toml 2. Commit + push 3. git tag -a vX.Y.Z -m 'release vX.Y.Z' && git push --tags 4. CI's pypi-twine stage picks up the tag and uploads README.md adds an Install section + Quick start (plain dist + TLS dist) so PyPI's project page shows usable copy on first visit. dist/ artifacts build cleanly and both pass twine check. --- .gitlab-ci.yml | 43 +++++++++++++++++++++++++++++++ Makefile | 36 +++++++++++++++++++++++++- README.md | 69 ++++++++++++++++++++++++++++++++++++++++---------- pyproject.toml | 41 +++++++++++++++++++++++++++--- 4 files changed, 171 insertions(+), 18 deletions(-) create mode 100644 .gitlab-ci.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..deefd60 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,43 @@ +stages: + - test + - pypi-twine + +# --------------------------------------------------------------------------- +# Tests run on every push to any branch. +# +# Live-integration tests need EPMD on the runner — ``make all`` skips +# them automatically when EPMD isn't reachable. CI runners typically +# don't run EPMD, so we get unit-level coverage in CI and full +# integration coverage on the dev machine. +# --------------------------------------------------------------------------- +test: + stage: test + tags: ["build"] + script: + - python3 -m venv .venv + - . .venv/bin/activate + - pip install --upgrade pip wheel + - pip install -e ".[dev]" + - pytest -v + +# --------------------------------------------------------------------------- +# Ship to PyPI on tag pushes. +# +# Tag the commit (``git tag -a v0.0.1 -m 'release'`` then +# ``git push --tags``) and CI builds sdist + wheel and uploads with +# twine. Credentials come from GitLab CI variables ``TWINE_USERNAME`` +# (typically ``__token__``) and ``TWINE_PASSWORD`` (the PyPI API token). +# --------------------------------------------------------------------------- +pypi-twine: + stage: pypi-twine + tags: ["build"] + only: + - tags + script: + - python3 -m venv .venv + - . .venv/bin/activate + - pip install --upgrade pip + - pip install build twine + - python -m build + - twine check dist/* + - twine upload dist/* diff --git a/Makefile b/Makefile index c5bc854..e488989 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ PYTEST = $(VENV_DIR)/bin/pytest RUFF = $(VENV_DIR)/bin/ruff .DEFAULT_GOAL := all -.PHONY: all bootstrap venv install test lint format clean help +.PHONY: all bootstrap venv install test lint format clean help build dist-check publish-test publish all: bootstrap test lint @@ -16,6 +16,16 @@ help: @echo " make lint - ruff check" @echo " make format - ruff format" @echo " make clean - drop venv + caches" + @echo "" + @echo "Packaging (PyPI):" + @echo " make build - build sdist + wheel into dist/" + @echo " make dist-check - validate built artifacts (twine check)" + @echo " make publish-test- upload to test.pypi.org (needs TWINE_PASSWORD)" + @echo " make publish - upload to pypi.org (needs TWINE_PASSWORD)" + @echo "" + @echo "Release flow: bump version in pyproject.toml, commit, then" + @echo " git tag -a vX.Y.Z -m 'release vX.Y.Z' && git push --tags" + @echo " CI's pypi-twine stage handles the upload from there." venv: test -d $(VENV_DIR) || python3 -m venv $(VENV_DIR) @@ -40,3 +50,27 @@ clean: rm -rf .pytest_cache .ruff_cache rm -rf build dist *.egg-info find . -type d -name __pycache__ -exec rm -rf {} + + +# -- Packaging -------------------------------------------------------------- +# Build sdist + wheel; emits dist/erldistpy-.tar.gz + +# dist/erldistpy--py3-none-any.whl. +build: bootstrap + rm -rf dist build + $(PIP) install --upgrade build + $(PYTHON) -m build + +# Validate built artifacts before upload — twine checks long-description +# rendering and required metadata. +dist-check: build + $(PIP) install --upgrade twine + $(VENV_DIR)/bin/twine check dist/* + +# Upload to TestPyPI for a dry run before tagging a real release. +# TWINE_USERNAME=__token__ and TWINE_PASSWORD= required. +publish-test: dist-check + $(VENV_DIR)/bin/twine upload --repository testpypi dist/* + +# Upload to PyPI — this is what CI runs on tag. Run locally only if you +# explicitly know you want to bypass CI. +publish: dist-check + $(VENV_DIR)/bin/twine upload dist/* diff --git a/README.md b/README.md index b5efbc5..15f881f 100644 --- a/README.md +++ b/README.md @@ -3,28 +3,57 @@ Native Python client for our Erlang distribution protocol. Talk Erlang/Elixir nodes from CPython without HTTP shim layers. -Built to swap into `unfeed`'s `WalletTransport` so a Python web app can call -`Unsandbox.WalletRPC` over native dist instead of HTTPS. Same gen_call -semantics, lower latency, fewer moving parts. +Built to swap into Python web apps as a drop-in for HTTP wallet-bridge +clients (see [unfeed](https://git.unturf.com/foxhop/unfeed)'s +`WalletTransport` and `make_post_sell`'s crypto watcher) so they can call +Elixir `gen_server` processes over native Erlang dist instead of JSON-RPC +or HTTPS. Same call semantics, lower latency, fewer moving parts. -## Status +## Install -Phase 0 — repo bones. See `docs/ROADMAP.md` for the phase plan. +```bash +pip install erldistpy +``` ## Quick start -```bash -make bootstrap # create venv, install editable + dev deps -make test # run pytest -make lint # ruff check +```python +from erldistpy import Node, Atom + +with Node( + our_name="myapp@host", + peer_name="wallet", # short EPMD name + peer_host="wallet.example.com", + cookie="SHARED_COOKIE", # read from a file path, never inline +) as node: + reply = node.call( + "Elixir.Wallet.Service", + (Atom("monero"), Atom("get_height"), []), + timeout=5.0, + ) + # reply is whatever the gen_server returned — atoms / binaries / + # tuples / maps / lists / pids / refs decode to Python natives. +``` + +For TLS dist (Erlang `inet_tls_dist`): + +```python +from erldistpy import Node, make_dist_tls_context + +ctx = make_dist_tls_context( + cert="/etc/myapp/client.pem", + key="/etc/myapp/client.key", + ca="/etc/myapp/ca.pem", +) +node = Node(our_name=..., peer_name=..., cookie=..., tls_context=ctx) ``` ## Scope - ETF (External Term Format) codec — encode/decode Erlang terms - EPMD client — node name → port lookup -- Distribution handshake — cookie auth, version negotiation -- gen_call to registered processes on a remote node +- v6 distribution handshake — MD5 cookie auth, version negotiation +- `gen_call` to registered processes on a remote node - TLS dist support (Erlang `inet_tls_dist`) Out of scope: full Erlang node impersonation, link/monitor lifecycles, @@ -32,9 +61,21 @@ distributed Mnesia. We are a *client*, not a peer node. ## Why not Pyrlang? -Pyrlang implements a full asyncio Erlang node. Heavy, asyncio-first, complex. -We need a small synchronous client that fits behind the same `WalletTransport` -Protocol as `httpx`. Different shape. +Pyrlang implements a full asyncio Erlang node. Heavy, asyncio-first, +complex. erldistpy is a small synchronous client that fits behind the +same Protocol surface as `httpx`. Different shape, different audience. + +## Development + +```bash +make bootstrap # create venv, install editable + dev deps +make test # run pytest (122 tests) +make lint # ruff check +make build # build sdist + wheel into dist/ +make dist-check # twine check dist/* +``` + +See `docs/ROADMAP.md` for the phase-by-phase build log. ## License diff --git a/pyproject.toml b/pyproject.toml index 1850713..89c771e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,20 +5,55 @@ build-backend = "setuptools.build_meta" [project] name = "erldistpy" version = "0.0.1" -description = "Native Python client for Erlang distribution protocol" -readme = "README.md" +description = "Native Python client for Erlang distribution protocol — EPMD + v6 handshake + gen_server call(), no asyncio." +readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.10" license = { text = "Unlicense" } authors = [ - { name = "fox", email = "russell@unturf.com" }, + { name = "Russell Ballestrini", email = "russell.ballestrini@gmail.com" }, +] +keywords = [ + "erlang", + "elixir", + "distribution", + "dist", + "gen_server", + "rpc", + "etf", + "epmd", + "inet_tls_dist", + "interop", +] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: Public Domain", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Erlang", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: System :: Networking", ] dependencies = [] +[project.urls] +Homepage = "https://git.unturf.com/python/erldistpy" +Repository = "https://git.unturf.com/python/erldistpy" +"Bug Tracker" = "https://git.unturf.com/python/erldistpy/-/issues" + [project.optional-dependencies] dev = [ "pytest >=8.0,<9", "pytest-cov >=5.0,<7", "ruff >=0.6,<1", + "build >=1.0", + "twine >=5.0", ] [tool.setuptools.packages.find]