From 74033a42959da9f9c10f51daf3c77c1c39b9304f Mon Sep 17 00:00:00 2001 From: bensynapse <118375461+bensynapse@users.noreply.github.com> Date: Tue, 6 Oct 2026 04:36:29 +0300 Subject: [PATCH] Prepare 5.0.10: package metadata, a wheel and the release workflow The metadata moves from setup.py to pyproject.toml, built with flit like jsonrpcclient. It adds requires-python >=3.8, classifiers up to 3.14, and project links to this repo and the new docs. The dependencies stay as in 5.0.9. The test extra gains pytest-asyncio and hypothesis, and the unused examples extra goes. The version is now jsonrpcserver.__version__, set to 5.0.10, and a test checks it against the installed metadata. .github/workflows/release.yml publishes the sdist and the wheel to PyPI with Trusted Publishing when a v* tag is pushed, through the pypi environment. RELEASING.md has the PyPI publisher settings and the steps. --- .github/workflows/release.yml | 93 +++++++++++++++++++++++++++++++++++ CHANGELOG.md | 29 +++++++++-- MANIFEST.in | 2 - RELEASING.md | 68 +++++++++++++++++++++++++ jsonrpcserver/__init__.py | 11 ++++- pyproject.toml | 71 ++++++++++++++++++++++++++ setup.py | 47 ------------------ tests/test_version.py | 9 ++++ 8 files changed, 276 insertions(+), 54 deletions(-) create mode 100644 .github/workflows/release.yml delete mode 100644 MANIFEST.in create mode 100644 RELEASING.md delete mode 100644 setup.py create mode 100644 tests/test_version.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..88226b6 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,93 @@ +name: Release + +# Pushing a tag like v5.0.10 builds the package, publishes it to PyPI with +# Trusted Publishing and creates the GitHub release. See RELEASING.md. + +on: + push: + tags: ["v*"] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-24.04 + permissions: + contents: read + id-token: write + attestations: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.13" + - run: python -m pip install -r requirements/build.txt + - name: Tag must match the package version + run: | + version=$(sed -n 's/^__version__ = "\(.*\)"$/\1/p' jsonrpcserver/__init__.py) + if [ "v$version" != "$GITHUB_REF_NAME" ]; then + echo "Tag $GITHUB_REF_NAME does not match __version__ $version" + exit 1 + fi + - name: CHANGELOG must have an entry for this version + run: grep -q "^## ${GITHUB_REF_NAME#v}\b" CHANGELOG.md + - run: python -m build + - run: python -m twine check --strict dist/* + - run: check-wheel-contents dist/*.whl + - name: Metadata must not link to hijacked domains + run: python .github/scripts/check_metadata.py dist/* + - uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-path: dist/* + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: dist + path: dist/ + if-no-files-found: error + + publish: + needs: build + runs-on: ubuntu-24.04 + environment: + name: pypi + url: https://pypi.org/project/jsonrpcserver/ + permissions: + id-token: write + steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: dist + path: dist/ + - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 + + github-release: + needs: publish + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: dist + path: dist/ + - name: Create the GitHub release from the CHANGELOG entry + env: + GH_TOKEN: ${{ github.token }} + run: | + version="${GITHUB_REF_NAME#v}" + awk -v v="## $version" ' + index($0, v) == 1 { on = 1; next } + on && /^## / { exit } + on { print } + ' CHANGELOG.md > notes.md + gh release create "$GITHUB_REF_NAME" dist/* \ + --repo "$GITHUB_REPOSITORY" \ + --title "$version" \ + --notes-file notes.md \ + --verify-tag diff --git a/CHANGELOG.md b/CHANGELOG.md index c1307b7..4fa892d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## 5.0.10 +The first release since the project moved to +[bensynapse/jsonrpcserver](https://github.com/bensynapse/jsonrpcserver). It +fixes a security problem, so please upgrade. Code that works with 5.0.9 keeps +working, apart from the security fix and the "Behaviour changes" below. + ### Security When a method raised an exception it didn't catch, the error response sent to @@ -93,6 +98,22 @@ it requests, and runs every code example in the docs. The websockets example uses the current `websockets.asyncio` API (#287). A new Security page covers the settings to check before exposing a server. +### Packaging + +- A wheel is published as well as the source distribution, so installs no + longer have to build the package. +- The package metadata says it needs Python 3.8 or later, and lists 3.8 to + 3.14. CI tests all of those, plus free-threaded 3.14. +- The project links on PyPI point to this repository and the new docs. The + old website domains belong to someone else now. Please don't follow links + to them from older releases. +- `jsonrpcserver.__version__` gives the version. +- The `test` extra now includes everything the tests need (`pytest-asyncio` + and `hypothesis` were missing). The `examples` extra is gone, because the + examples it was for are in the docs now, with their own requirements file. +- The build uses `pyproject.toml` with flit instead of `setup.py`. The + dependencies are the same as in 5.0.9. + ### Deprecations Three functions in `jsonrpcserver.response` have new names. The old names still @@ -151,7 +172,7 @@ work in 5.x but give a `DeprecationWarning`, and will be removed in 6.0. ## 5.0.6 (Jan 14, 2022) -- Fix reversed Result Either type ([#227](https://github.com/explodinglabs/jsonrpcserver/pull/227)). +- Fix reversed Result Either type ([#227](https://github.com/bensynapse/jsonrpcserver/pull/227)). ## 5.0.5 (Nov 27, 2021) @@ -185,9 +206,9 @@ A complete rebuild, with a few important usage changes. ## 4.2.0 (Nov 9, 2020) -- Add ability to use custom serializer and deserializer ([#125](https://github.com/explodinglabs/jsonrpcserver/pull/125)) -- Add ability to use custom method name ([#127](https://github.com/explodinglabs/jsonrpcserver/pull/127)) -- Deny additional parameters in json-rpc request ([#128](https://github.com/explodinglabs/jsonrpcserver/pull/128)) +- Add ability to use custom serializer and deserializer ([#125](https://github.com/bensynapse/jsonrpcserver/pull/125)) +- Add ability to use custom method name ([#127](https://github.com/bensynapse/jsonrpcserver/pull/127)) +- Deny additional parameters in json-rpc request ([#128](https://github.com/bensynapse/jsonrpcserver/pull/128)) Thanks to deptyped. diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 3f40709..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,2 +0,0 @@ -include README.md CHANGELOG.md LICENSE -include jsonrpcserver/py.typed jsonrpcserver/request-schema.json diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..a5165d3 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,68 @@ +# Releasing + +Releases are published by `.github/workflows/release.yml` when a tag like +`v5.0.10` is pushed. It builds and checks the sdist and wheel, and attests +their build provenance. Then it uploads them to PyPI with Trusted Publishing +and creates the GitHub release from the CHANGELOG entry. Nobody needs a PyPI +token. + +## One-time setup on PyPI + +This needs an Owner or Maintainer role on the `jsonrpcserver` project on PyPI. + +1. Open https://pypi.org/manage/project/jsonrpcserver/settings/publishing/ +2. Under "Add a new publisher", choose **GitHub** and fill in exactly: + + | Field | Value | + |---|---| + | Owner | `bensynapse` | + | Repository name | `jsonrpcserver` | + | Workflow name | `release.yml` | + | Environment name | `pypi` | + +3. Click "Add". + +The `pypi` environment already exists in the GitHub repo settings. It only +accepts deployments from tags matching `v*`. To require a manual approval +before each upload, add yourself under "Required reviewers" in +Settings → Environments → pypi. + +## Each release + +1. Merge a pull request that sets `__version__` in `jsonrpcserver/__init__.py` + and adds a `## ` section to CHANGELOG.md. The release workflow + fails if either is missing or they don't match the tag. +2. Tag the merge commit on main and push the tag: + + ```sh + git fetch origin + git tag -a v5.0.10 -m "5.0.10" + git push origin v5.0.10 + ``` + +3. Watch the "Release" workflow in the Actions tab. +4. Check the result: + + ```sh + curl -s https://pypi.org/pypi/jsonrpcserver/json | python3 -c \ + "import sys, json; i = json.load(sys.stdin)['info']; print(i['version'], i['project_urls'])" + ``` + + The release should have two files, a `.tar.gz` and a `.whl`. + +## If the upload fails + +- `invalid-publisher`: the PyPI publisher settings above don't match. Check + every field, including the `.yml` in the workflow name. +- `File already exists`: that version is already on PyPI, and PyPI never + allows the same file twice. Bump the version and tag again. + +A failed run can be re-run from the Actions tab once the cause is fixed. If the +tag itself was wrong, delete it locally and on GitHub +(`git push origin :refs/tags/v5.0.10`) before tagging again. Never delete a tag +whose version reached PyPI. + +## Docs + +The docs site deploys itself from main through `.github/workflows/docs.yml`, +so it changes when the docs change, not at release time. diff --git a/jsonrpcserver/__init__.py b/jsonrpcserver/__init__.py index f577e64..32fbe95 100644 --- a/jsonrpcserver/__init__.py +++ b/jsonrpcserver/__init__.py @@ -1,5 +1,11 @@ -"""Use __all__ so mypy considers these re-exported.""" +"""Process incoming JSON-RPC requests in Python. +Write methods with @method, then pass each request string to dispatch, or to +async_dispatch in an asyncio server. Documentation: +https://bensynapse.github.io/jsonrpcserver/ +""" + +# __all__ also tells type checkers that these names are re-exported. __all__ = [ "Error", "InvalidParams", @@ -31,3 +37,6 @@ from .methods import method from .result import Error, InvalidParams, Result, Success from .server import serve + +__version__ = "5.0.10" +"""The version of jsonrpcserver, as a string. Added in 5.0.10.""" diff --git a/pyproject.toml b/pyproject.toml index fc37e8e..8b56e3a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,73 @@ +[build-system] +requires = ["flit_core>=3.12,<5"] +build-backend = "flit_core.buildapi" + +[project] +name = "jsonrpcserver" +dynamic = ["version"] +description = "Process incoming JSON-RPC requests in Python" +readme = "README.md" +license = "MIT" +license-files = ["LICENSE"] +requires-python = ">=3.8" +authors = [ + {name = "Beau Barker", email = "beau@explodinglabs.com"}, +] +maintainers = [ + {name = "bensynapse"}, +] +keywords = ["json-rpc", "jsonrpc", "json-rpc-2.0", "rpc", "server"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: Free Threading :: 3 - Stable", + "Programming Language :: Python :: Implementation :: CPython", + "Topic :: Internet", + "Topic :: Software Development :: Libraries :: Python Modules", + "Typing :: Typed", +] +# The same ranges as 5.0.9, so upgrading never forces a different jsonschema +# or oslash. +dependencies = [ + "jsonschema<5", + "oslash<1", +] + +[project.urls] +Homepage = "https://bensynapse.github.io/jsonrpcserver/" +Documentation = "https://bensynapse.github.io/jsonrpcserver/" +Repository = "https://github.com/bensynapse/jsonrpcserver" +Issues = "https://github.com/bensynapse/jsonrpcserver/issues" +Changelog = "https://github.com/bensynapse/jsonrpcserver/blob/main/CHANGELOG.md" + +[project.optional-dependencies] +test = [ + "hypothesis", + "pytest", + "pytest-asyncio", + "pytest-cov", +] + +[tool.flit.sdist] +include = [ + "CHANGELOG.md", + "docs/", + "mkdocs.yml", + "requirements/", + "tests/", + "tox.ini", +] + [tool.ruff] target-version = "py38" @@ -37,6 +107,7 @@ reportUnnecessaryTypeIgnoreComment = "error" [tool.pytest.ini_options] testpaths = ["tests"] +asyncio_default_fixture_loop_scope = "function" [tool.coverage.run] branch = true diff --git a/setup.py b/setup.py deleted file mode 100644 index 4f8df79..0000000 --- a/setup.py +++ /dev/null @@ -1,47 +0,0 @@ -"""setup.py""" - -from setuptools import setup - -with open("README.md", encoding="utf-8") as f: - README = f.read() - -setup( - author="Beau Barker", - author_email="beau@explodinglabs.com", - classifiers=[ - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - ], - description="Process JSON-RPC requests", - extras_require={ - "examples": [ - "aiohttp", - "aiozmq", - "flask", - "flask-socketio", - "gmqtt", - "pyzmq", - "tornado", - "websockets", - "werkzeug", - ], - "test": [ - "pytest", - "pytest-cov", - "tox", - ], - }, - include_package_data=True, - install_requires=["jsonschema<5", "oslash<1"], - license="MIT", - long_description=README, - long_description_content_type="text/markdown", - name="jsonrpcserver", - packages=["jsonrpcserver"], - url="https://github.com/explodinglabs/jsonrpcserver", - version="5.0.9", - # Be PEP 561 compliant - # https://mypy.readthedocs.io/en/stable/installed_packages.html#making-pep-561-compatible-packages - zip_safe=False, -) diff --git a/tests/test_version.py b/tests/test_version.py new file mode 100644 index 0000000..5dd8782 --- /dev/null +++ b/tests/test_version.py @@ -0,0 +1,9 @@ +"""__version__ must match the installed package metadata.""" + +from importlib.metadata import version + +import jsonrpcserver + + +def test_version() -> None: + assert jsonrpcserver.__version__ == version("jsonrpcserver")