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
|
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/*
|
||||||
|
|
|
||||||
69
README.md
69
README.md
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue