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:
parent
c870bdb2ac
commit
001a471549
4 changed files with 171 additions and 18 deletions
43
.gitlab-ci.yml
Normal file
43
.gitlab-ci.yml
Normal 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/*
|
||||
36
Makefile
36
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-<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/*
|
||||
|
|
|
|||
69
README.md
69
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue