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.
This commit is contained in:
russell@unturf.com 2026-06-16 13:45:08 -04:00
parent c870bdb2ac
commit 001a471549
No known key found for this signature in database
4 changed files with 171 additions and 18 deletions

43
.gitlab-ci.yml Normal file
View file

@ -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/*

View file

@ -5,7 +5,7 @@ PYTEST = $(VENV_DIR)/bin/pytest
RUFF = $(VENV_DIR)/bin/ruff RUFF = $(VENV_DIR)/bin/ruff
.DEFAULT_GOAL := all .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 all: bootstrap test lint
@ -16,6 +16,16 @@ help:
@echo " make lint - ruff check" @echo " make lint - ruff check"
@echo " make format - ruff format" @echo " make format - ruff format"
@echo " make clean - drop venv + caches" @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: venv:
test -d $(VENV_DIR) || python3 -m venv $(VENV_DIR) test -d $(VENV_DIR) || python3 -m venv $(VENV_DIR)
@ -40,3 +50,27 @@ clean:
rm -rf .pytest_cache .ruff_cache rm -rf .pytest_cache .ruff_cache
rm -rf build dist *.egg-info rm -rf build dist *.egg-info
find . -type d -name __pycache__ -exec rm -rf {} + find . -type d -name __pycache__ -exec rm -rf {} +
# -- Packaging --------------------------------------------------------------
# Build sdist + wheel; emits dist/erldistpy-<version>.tar.gz +
# dist/erldistpy-<version>-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=<test.pypi token> 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/*

View file

@ -3,28 +3,57 @@
Native Python client for our Erlang distribution protocol. Talk Erlang/Elixir Native Python client for our Erlang distribution protocol. Talk Erlang/Elixir
nodes from CPython without HTTP shim layers. nodes from CPython without HTTP shim layers.
Built to swap into `unfeed`'s `WalletTransport` so a Python web app can call Built to swap into Python web apps as a drop-in for HTTP wallet-bridge
`Unsandbox.WalletRPC` over native dist instead of HTTPS. Same gen_call clients (see [unfeed](https://git.unturf.com/foxhop/unfeed)'s
semantics, lower latency, fewer moving parts. `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 ## Quick start
```bash ```python
make bootstrap # create venv, install editable + dev deps from erldistpy import Node, Atom
make test # run pytest
make lint # ruff check 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 ## Scope
- ETF (External Term Format) codec — encode/decode Erlang terms - ETF (External Term Format) codec — encode/decode Erlang terms
- EPMD client — node name → port lookup - EPMD client — node name → port lookup
- Distribution handshake — cookie auth, version negotiation - v6 distribution handshake — MD5 cookie auth, version negotiation
- gen_call to registered processes on a remote node - `gen_call` to registered processes on a remote node
- TLS dist support (Erlang `inet_tls_dist`) - TLS dist support (Erlang `inet_tls_dist`)
Out of scope: full Erlang node impersonation, link/monitor lifecycles, 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? ## Why not Pyrlang?
Pyrlang implements a full asyncio Erlang node. Heavy, asyncio-first, complex. Pyrlang implements a full asyncio Erlang node. Heavy, asyncio-first,
We need a small synchronous client that fits behind the same `WalletTransport` complex. erldistpy is a small synchronous client that fits behind the
Protocol as `httpx`. Different shape. 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 ## License

View file

@ -5,20 +5,55 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "erldistpy" name = "erldistpy"
version = "0.0.1" version = "0.0.1"
description = "Native Python client for Erlang distribution protocol" description = "Native Python client for Erlang distribution protocol — EPMD + v6 handshake + gen_server call(), no asyncio."
readme = "README.md" readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.10" requires-python = ">=3.10"
license = { text = "Unlicense" } license = { text = "Unlicense" }
authors = [ 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 = [] 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] [project.optional-dependencies]
dev = [ dev = [
"pytest >=8.0,<9", "pytest >=8.0,<9",
"pytest-cov >=5.0,<7", "pytest-cov >=5.0,<7",
"ruff >=0.6,<1", "ruff >=0.6,<1",
"build >=1.0",
"twine >=5.0",
] ]
[tool.setuptools.packages.find] [tool.setuptools.packages.find]