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):