From 87ea9c99fa20fa8671b39f0baa62b84663574f58 Mon Sep 17 00:00:00 2001 From: Stefano Noferi Date: Tue, 6 Oct 2026 20:51:35 +0200 Subject: [PATCH] feat(packaging): publish the admina alias package packaging/admina builds the PyPI package "admina": no module of its own, a dependency on admina-framework at the same version, and one extra per extra of admina-framework (admina[proxy] installs admina-framework[proxy]). The alias holds no files of the admina module, so installing, upgrading or removing either package leaves the other intact, with pip and with uv. release.yml builds the alias next to admina-framework and publishes it after admina-framework, through the Trusted Publisher of the "admina" project (release.yml, environment pypi); the GitHub Release keeps the admina-framework files only. scripts/check-versions.py tracks the alias version and its admina-framework pin, and tests/test_admina_alias.py checks that its extras match those of admina-framework. --- .github/workflows/release.yml | 52 ++++++++++++++++++++++--- CHANGELOG.md | 9 +++++ README.md | 5 +++ packaging/admina/README.md | 15 +++++++ packaging/admina/pyproject.toml | 69 +++++++++++++++++++++++++++++++++ scripts/check-versions.py | 15 +++++++ tests/test_admina_alias.py | 56 ++++++++++++++++++++++++++ tests/test_check_versions.py | 17 +++++++- 8 files changed, 230 insertions(+), 8 deletions(-) create mode 100644 packaging/admina/README.md create mode 100644 packaging/admina/pyproject.toml create mode 100644 tests/test_admina_alias.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 290c16e..a0f28c9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,8 +14,9 @@ # Release pipeline — fires on version tags (vX.Y.Z). # -# Publishes the pure-Python root package admina-framework to PyPI and -# creates the GitHub Release. The Rust accelerator admina-core is built +# Publishes the pure-Python root package admina-framework to PyPI, then the +# alias package admina (packaging/admina: no code, it installs +# admina-framework at the same version), and creates the GitHub Release. The Rust accelerator admina-core is built # and published by a sibling workflow (.github/workflows/release-core.yml) # triggered by the same tag — they run in parallel, independently. # @@ -34,7 +35,9 @@ # Workflow name: release.yml # Environment name: pypi # -# The matching publisher for admina-core is configured for release-core.yml. +# The publisher of the alias project "admina" has the same values, with +# PyPI Project Name: admina. The matching publisher for admina-core is +# configured for release-core.yml. # See https://docs.pypi.org/trusted-publishers/ for details. # # ── How to cut a release ─────────────────────────────────────────────────── @@ -102,6 +105,10 @@ jobs: run: | uv build --out-dir dist/ + - name: Build the admina alias (sdist + wheel) + run: | + uv build packaging/admina --out-dir dist-alias/ + - name: Upload artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: @@ -110,6 +117,14 @@ jobs: if-no-files-found: error retention-days: 7 + - name: Upload alias artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: admina-alias-dist + path: dist-alias/* + if-no-files-found: error + retention-days: 7 + # ── 4. Publish root admina-framework to PyPI ──────────────────────────── publish-root: name: Publish admina-framework to PyPI @@ -138,7 +153,32 @@ jobs: packages-dir: dist/ skip-existing: true - # ── 5. Create GitHub Release ──────────────────────────────────────────── + # ── 5. Publish the admina alias to PyPI ───────────────────────────────── + publish-alias: + name: Publish admina (alias) to PyPI + # After admina-framework, so that the version the alias pins exists. + needs: [build-root, tests, publish-root] + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/project/admina/ + steps: + - name: Download alias artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: admina-alias-dist + path: dist + + - name: List artifacts + run: ls -la dist/ + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 + with: + packages-dir: dist/ + skip-existing: true + + # ── 6. Create GitHub Release ──────────────────────────────────────────── github-release: name: Create GitHub Release needs: [publish-root] @@ -148,11 +188,11 @@ jobs: with: fetch-depth: 0 # needed to extract the CHANGELOG section for this tag - - name: Download all artifacts + - name: Download admina-framework artifacts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: + name: admina-root-dist path: artifacts - merge-multiple: true - name: Extract changelog for this version id: changelog diff --git a/CHANGELOG.md b/CHANGELOG.md index 39ee24a..c259ce3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,15 @@ stability commitment. See [ROADMAP.md](ROADMAP.md) for planned milestones. ## [Unreleased] +### Added + +- The PyPI package `admina`: `pip install admina` installs + `admina-framework` at the same version, and `admina[X]` installs + `admina-framework[X]` for every extra. It has no code of its own + (`packaging/admina`), and `release.yml` publishes it after + admina-framework from the same tag. `scripts/check-versions.py` checks its + version and its admina-framework pin. + ### Notes - `tests/test_iban_it.py::test_matching_time_on_long_runs` (a 50 ms budget diff --git a/README.md b/README.md index cafeb19..a586772 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,11 @@ pip install admina-framework > matters more than that extra coverage. See > [Performance](#performance--hybrid-python--rust-engine) for the trade-off. +The PyPI package `admina` is an alias: `pip install admina` installs +`admina-framework` at the same version, and `admina[X]` installs +`admina-framework[X]` for every extra above. It has no code of its own, so +both names install the same `admina` module. + ### Or install from source ```bash diff --git a/packaging/admina/README.md b/packaging/admina/README.md new file mode 100644 index 0000000..9b8fed1 --- /dev/null +++ b/packaging/admina/README.md @@ -0,0 +1,15 @@ +# admina + +`pip install admina` installs [`admina-framework`](https://pypi.org/project/admina-framework/), +the Admina governed AI development framework, at the same version. This +package has no code of its own; `import admina` comes from `admina-framework`. + +Every extra of `admina-framework` is available under the same name: + +```bash +pip install admina # SDK +pip install "admina[proxy]" # SDK + proxy + dashboard +pip install "admina[full]" # proxy + NLP + telemetry +``` + +Documentation, changelog and source: https://github.com/admina-org/admina diff --git a/packaging/admina/pyproject.toml b/packaging/admina/pyproject.toml new file mode 100644 index 0000000..6359b52 --- /dev/null +++ b/packaging/admina/pyproject.toml @@ -0,0 +1,69 @@ + +# Copyright © 2025–2026 Stefano Noferi & Admina contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# The PyPI project "admina": no code of its own, it installs admina-framework +# at the same version. Every extra of admina-framework has an extra of the same +# name here (admina[proxy] installs admina-framework[proxy]). The version and +# the pin are checked by scripts/check-versions.py, the extras by +# tests/test_admina_alias.py; release.yml publishes it after admina-framework. + +[build-system] +requires = ["setuptools>=68.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "admina" +version = "0.13.1" +description = "Admina — governed AI development framework (installs admina-framework)" +readme = "README.md" +requires-python = ">=3.11" +license = "Apache-2.0" +authors = [ + {name = "Stefano Noferi", email = "info@admina.org"}, +] +maintainers = [ + {name = "Stefano Noferi", email = "info@admina.org"}, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Programming Language :: Python :: 3", +] +dependencies = ["admina-framework==0.13.1"] + +[project.urls] +Homepage = "https://admina.org" +Repository = "https://github.com/admina-org/admina" +Documentation = "https://github.com/admina-org/admina#readme" +Changelog = "https://github.com/admina-org/admina/blob/main/CHANGELOG.md" + +[project.optional-dependencies] +proxy = ["admina-framework[proxy]==0.13.1"] +proxy-minimal = ["admina-framework[proxy-minimal]==0.13.1"] +nlp = ["admina-framework[nlp]==0.13.1"] +telemetry = ["admina-framework[telemetry]==0.13.1"] +rust = ["admina-framework[rust]==0.13.1"] +full = ["admina-framework[full]==0.13.1"] +openai = ["admina-framework[openai]==0.13.1"] +ollama = ["admina-framework[ollama]==0.13.1"] +anthropic = ["admina-framework[anthropic]==0.13.1"] +mistral = ["admina-framework[mistral]==0.13.1"] +gemini = ["admina-framework[gemini]==0.13.1"] +bedrock = ["admina-framework[bedrock]==0.13.1"] +adapters = ["admina-framework[adapters]==0.13.1"] +presidio = ["admina-framework[presidio]==0.13.1"] +all = ["admina-framework[all]==0.13.1"] + +[tool.setuptools] +packages = [] diff --git a/scripts/check-versions.py b/scripts/check-versions.py index 0869468..ae96c14 100755 --- a/scripts/check-versions.py +++ b/scripts/check-versions.py @@ -33,6 +33,8 @@ 6. uv.lock → resolved admina-framework entry 7. uv.lock → resolved admina-core entry ([rust] extra) 8. core-rust/uv.lock → resolved admina-core entry + 9. packaging/admina/pyproject.toml → admina (PyPI alias) + 10. packaging/admina/pyproject.toml → its admina-framework pin Docker images and dashboard HTML derive their version dynamically from pyproject.toml at build time, so they need no separate check here. @@ -103,7 +105,18 @@ def _uv_lock_package(path: Path, pkg: str) -> str: raise RuntimeError(f"{pkg!r} entry not found in {path}") +def _alias_framework_pin(path: Path) -> str: + """The version of the ``admina-framework==X`` dependency of the alias.""" + data = tomllib.loads(path.read_text(encoding="utf-8")) + for dep in data["project"].get("dependencies", []): + m = re.fullmatch(r"admina-framework==(\S+)", dep.strip()) + if m: + return m.group(1) + raise RuntimeError(f"no admina-framework==X dependency in {path}") + + def main(repo: Path = REPO) -> int: + alias = repo / "packaging" / "admina" / "pyproject.toml" versions: dict[str, str] = { "pyproject.toml": _toml_version(repo / "pyproject.toml"), "admina/__init__.py": _python_dunder_version(repo / "admina" / "__init__.py"), @@ -113,6 +126,8 @@ def main(repo: Path = REPO) -> int: "core-rust/Cargo.toml": _toml_version(repo / "core-rust" / "Cargo.toml", table="package"), "core-rust/Cargo.lock": _cargo_lock_admina_core(repo / "core-rust" / "Cargo.lock"), "core-rust/uv.lock": _uv_lock_package(repo / "core-rust" / "uv.lock", "admina-core"), + "packaging/admina/pyproject.toml": _toml_version(alias), + "packaging/admina (admina-framework pin)": _alias_framework_pin(alias), } canonical = versions["pyproject.toml"] diff --git a/tests/test_admina_alias.py b/tests/test_admina_alias.py new file mode 100644 index 0000000..ab63bac --- /dev/null +++ b/tests/test_admina_alias.py @@ -0,0 +1,56 @@ +# Copyright © 2025–2026 Stefano Noferi & Admina contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""The ``admina`` alias package (``packaging/admina``) mirrors admina-framework. + +``pip install admina[X]`` installs ``admina-framework[X]`` at the same +version, for every extra X of admina-framework, and the alias ships no +module of its own (``import admina`` comes from admina-framework only). +""" + +from __future__ import annotations + +import tomllib +from pathlib import Path + +REPO = Path(__file__).resolve().parent.parent + + +def _project(path: Path) -> dict: + return tomllib.loads(path.read_text(encoding="utf-8")) + + +ROOT = _project(REPO / "pyproject.toml") +ALIAS = _project(REPO / "packaging" / "admina" / "pyproject.toml") + + +def test_the_alias_installs_admina_framework_at_its_version(): + version = ROOT["project"]["version"] + assert ALIAS["project"]["name"] == "admina" + assert ALIAS["project"]["version"] == version + assert ALIAS["project"]["dependencies"] == [f"admina-framework=={version}"] + + +def test_every_extra_of_admina_framework_has_an_alias_extra(): + version = ROOT["project"]["version"] + expected = { + extra: [f"admina-framework[{extra}]=={version}"] + for extra in ROOT["project"]["optional-dependencies"] + } + assert ALIAS["project"]["optional-dependencies"] == expected + + +def test_the_alias_ships_no_module(): + assert ALIAS["tool"]["setuptools"]["packages"] == [] + assert ALIAS["project"]["requires-python"] == ROOT["project"]["requires-python"] diff --git a/tests/test_check_versions.py b/tests/test_check_versions.py index fd2c5ef..edf29ac 100644 --- a/tests/test_check_versions.py +++ b/tests/test_check_versions.py @@ -44,13 +44,24 @@ def _write_repo( cargo: str | None = None, runtime: str | None = None, locked_core: str | None = None, + alias: str | None = None, + alias_pin: str | None = None, ) -> Path: - """Write the eight tracked manifests of a repository at ``version``. + """Write the tracked manifests of a repository at ``version``. ``cargo`` is the crate version (Cargo.toml and Cargo.lock), ``runtime`` the ``__version__`` and ``locked_core`` the admina-core entry of uv.lock; - each defaults to ``version``. + each defaults to ``version``. ``alias`` and ``alias_pin`` are the + version of the ``admina`` alias and its admina-framework pin. """ + alias = alias or version + alias_pin = alias_pin or version + (root / "packaging" / "admina").mkdir(parents=True) + (root / "packaging" / "admina" / "pyproject.toml").write_text( + f'[project]\nname = "admina"\nversion = "{alias}"\n' + f'dependencies = ["admina-framework=={alias_pin}"]\n', + encoding="utf-8", + ) cargo = cargo or version runtime = runtime or version locked_core = locked_core or version @@ -128,6 +139,8 @@ def test_aligned_final_release_passes(tmp_path): ({"cargo": "0.13.0"}, "core-rust/Cargo.toml"), ({"runtime": "0.12.1"}, "admina/__init__.py"), ({"locked_core": "0.9.3"}, "uv.lock (admina-core)"), + ({"alias": "0.12.2"}, "packaging/admina/pyproject.toml"), + ({"alias_pin": "0.12.2"}, "packaging/admina (admina-framework pin)"), ], ) def test_real_drift_fails(tmp_path, capsys, overrides, drifting):