From 1cb99915700f1eede482b62b8d3385eee3dfee70 Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Tue, 27 Oct 2026 08:24:27 -0500 Subject: [PATCH 1/2] feat(cli): rename package and command to morphir-python Publish the CLI distribution as morphir-python while keeping its internal morphir_tools import. Update package checks, artifacts, diagnostics and examples to use the new public name. Accept python-v release tags so the corrected 0.0.1 candidate can be published without moving the previous failed v0.0.1 tag. --- .config/mise/tasks/build | 2 +- .config/mise/tasks/release-check | 12 +++++----- .github/workflows/ci.yml | 4 ++-- .github/workflows/release.yml | 13 ++++++----- AGENTS.md | 8 +++---- README.md | 23 ++++++++++--------- openspec/specs/tools/ir-interchange/spec.md | 2 +- packages/morphir-tools/README.md | 16 ++++++------- packages/morphir-tools/pyproject.toml | 4 ++-- .../src/morphir_tools/__init__.py | 2 +- .../morphir-tools/src/morphir_tools/cli.py | 6 ++--- packages/morphir/README.md | 4 ++-- packages/morphir/src/morphir/ir/v4/codec.py | 2 +- tests/tools/__init__.py | 2 +- tests/tools/test_cli.py | 9 ++++++-- tests/tools/test_tools.py | 19 +++++++++++++-- uv.lock | 4 ++-- 17 files changed, 77 insertions(+), 55 deletions(-) diff --git a/.config/mise/tasks/build b/.config/mise/tasks/build index c56bfdd3..d6d4507c 100755 --- a/.config/mise/tasks/build +++ b/.config/mise/tasks/build @@ -5,7 +5,7 @@ set -euo pipefail echo "Building morphir package..." uv build packages/morphir --out-dir packages/morphir/dist -echo "Building morphir-tools package..." +echo "Building morphir-python package..." uv build packages/morphir-tools --out-dir packages/morphir-tools/dist echo "Build complete!" diff --git a/.config/mise/tasks/release-check b/.config/mise/tasks/release-check index e6ec9580..ab22d69d 100755 --- a/.config/mise/tasks/release-check +++ b/.config/mise/tasks/release-check @@ -16,12 +16,12 @@ from email.parser import BytesParser from pathlib import Path versions = set() -for name in ("morphir", "morphir-tools"): - directory = Path("packages") / name +for folder, name in (("morphir", "morphir"), ("morphir-tools", "morphir-python")): + directory = Path("packages") / folder project = tomllib.loads((directory / "pyproject.toml").read_text())["project"] versions.add(project["version"]) tag = os.environ.get("RELEASE_TAG", "") - if tag and tag != f"v{project['version']}": + if tag and tag not in (f"v{project['version']}", f"python-v{project['version']}"): raise SystemExit(f"{tag} does not match {name} {project['version']}") stem = name.replace("-", "_") + "-" + project["version"] expected = {f"{stem}-py3-none-any.whl", f"{stem}.tar.gz"} @@ -72,15 +72,15 @@ from morphir.ir import format_version from morphir.sdk import basics assert morphir.__version__ == version("morphir") -assert morphir_tools.__version__ == version("morphir-tools") +assert morphir_tools.__version__ == version("morphir-python") assert basics.round(2.5) == 3 assert format_version.V3.major == 3 PY -"$release_dir/venv/bin/morphir-tools" --help +"$release_dir/venv/bin/morphir-python" --help # These two first-party packages are not yet on PyPI. Audit the exact installed # third-party versions, without querying PyPI for the unpublished packages. -uv pip freeze --python "$release_python" --exclude morphir --exclude morphir-tools \ +uv pip freeze --python "$release_python" --exclude morphir --exclude morphir-python \ > "$release_dir/requirements.txt" uvx --from pip-audit==2.10.1 pip-audit --strict --no-deps --disable-pip \ --requirement "$release_dir/requirements.txt" \ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1248bdfd..faf9e141 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -104,10 +104,10 @@ jobs: path: packages/morphir/dist/ if-no-files-found: error - - name: Upload morphir-tools distributions + - name: Upload morphir-python distributions uses: actions/upload-artifact@v6 with: - name: morphir-tools-dist + name: morphir-python-dist path: packages/morphir-tools/dist/ if-no-files-found: error diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1a6ccf7e..54d04cdd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,7 @@ on: push: tags: - 'v*' + - 'python-v*' # Qualification only. Publishing requires a tag push. workflow_dispatch: @@ -41,20 +42,20 @@ jobs: - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 - publish-morphir-tools: - name: Publish morphir-tools to PyPI + publish-morphir-python: + name: Publish morphir-python to PyPI needs: [qualify, publish-morphir] runs-on: ubuntu-latest permissions: id-token: write environment: name: pypi - url: https://pypi.org/project/morphir-tools/ + url: https://pypi.org/project/morphir-python/ steps: - - name: Download morphir-tools dist + - name: Download morphir-python dist uses: actions/download-artifact@v7 with: - name: morphir-tools-dist + name: morphir-python-dist path: dist/ - name: Publish to PyPI @@ -62,7 +63,7 @@ jobs: github-release: name: Create GitHub Release - needs: [publish-morphir, publish-morphir-tools] + needs: [publish-morphir, publish-morphir-python] runs-on: ubuntu-latest permissions: contents: write diff --git a/AGENTS.md b/AGENTS.md index d51188df..b2912fed 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,7 +15,7 @@ morphir-python/ │ └── morphir-tools/ # CLI tools and extensions (depends on morphir) ├── tests/ │ ├── unit/ # Unit tests for morphir package -│ └── tools/ # Unit tests for morphir-tools package +│ └── tools/ # Unit tests for morphir-python package └── features/ # BDD feature tests (behave) ``` @@ -36,8 +36,8 @@ morphir-python/ - Zero CLI or IO dependencies - Should be usable as a standalone library -**`morphir-tools` (CLI/tools)** -- Command-line interface (`morphir-tools`, Typer): `ir inspect`, `ir convert` +**`morphir-python` (CLI/tools, in `packages/morphir-tools/`)** +- Command-line interface (`morphir-python`, Typer): `ir inspect`, `ir convert` - File I/O operations - Code generation backends - Depends on `morphir` core library @@ -266,7 +266,7 @@ mise run coverage # Coverage report When adding dependencies: - **morphir package**: Should have minimal/zero dependencies -- **morphir-tools package**: Can have CLI and IO dependencies +- **morphir-python package**: Can have CLI and IO dependencies - Always pin minimum versions in pyproject.toml ## Error Handling diff --git a/README.md b/README.md index 4510134d..bd9a1e5e 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ This monorepo contains two packages: | Package | Description | PyPI | |---------|-------------|------| | `morphir` | Core library - IR models, the Morphir SDK runtime (`morphir.sdk`), types, and functional domain modeling primitives | [![PyPI](https://img.shields.io/pypi/v/morphir)](https://pypi.org/project/morphir/) | -| `morphir-tools` | CLI tools and extensions for working with Morphir | [![PyPI](https://img.shields.io/pypi/v/morphir-tools)](https://pypi.org/project/morphir-tools/) | +| `morphir-python` | CLI tools and extensions for working with Morphir | [![PyPI](https://img.shields.io/pypi/v/morphir-python)](https://pypi.org/project/morphir-python/) | ## Installation @@ -37,13 +37,13 @@ uv add morphir For the full toolset including CLI commands: ```bash -pip install morphir-tools +pip install morphir-python ``` Or with uv: ```bash -uv add morphir-tools +uv add morphir-python ``` ## Quick Start @@ -65,8 +65,8 @@ v4_library = v3_to_v4(library).value Or from the command line: ```bash -morphir-tools ir inspect morphir-ir.json -morphir-tools ir convert morphir-ir.json --to v4 -o morphir-ir.v4.json +morphir-python ir inspect morphir-ir.json +morphir-python ir convert morphir-ir.json --to v4 -o morphir-ir.v4.json ``` See the [morphir package README](packages/morphir/README.md#morphir-ir-versions) @@ -171,10 +171,11 @@ The initial release is `0.0.1` for both PyPI distributions: | PyPI distribution | Python import | Command | |-------------------|---------------|---------| | `morphir` | `morphir` | None | -| `morphir-tools` | `morphir_tools` | `morphir-tools` | +| `morphir-python` | `morphir_tools` | `morphir-python` | -`morphir-python` is the repository name. The workspace itself is not published. -Both packages require Python 3.14 or later. `morphir-tools` installs `morphir` as +`morphir-python` is both the CLI distribution and repository name. The workspace +itself is not published. +Both packages require Python 3.14 or later. `morphir-python` installs `morphir` as a dependency. Publishing uses [PyPI trusted publishing](https://docs.pypi.org/trusted-publishers/using-a-publisher/), @@ -188,9 +189,9 @@ PyPI, including a pending publisher if the project has not been created yet: | Workflow filename | `release.yml` | | Environment | `pypi` | -The GitHub `pypi` environment permits release tags matching `v*`. After maintainer -approval, push a tag such as `v0.0.1` whose version matches both package metadata -files and their runtime `__version__` values. Update the tools package's minimum +The GitHub `pypi` environment permits release tags matching `v*` or `python-v*`. +After maintainer approval, push a tag such as `python-v0.0.1` whose version matches +both package metadata files and their runtime `__version__` values. Update the tools package's minimum `morphir` dependency and refresh `uv.lock` when changing versions. The release workflow runs the same lint, format, type, coverage and BDD checks as diff --git a/openspec/specs/tools/ir-interchange/spec.md b/openspec/specs/tools/ir-interchange/spec.md index 1e3cc50c..9c8c5b1d 100644 --- a/openspec/specs/tools/ir-interchange/spec.md +++ b/openspec/specs/tools/ir-interchange/spec.md @@ -1,7 +1,7 @@ # tools/ir-interchange Specification ## Purpose -Gives morphir-tools users commands to inspect versioned `morphir-ir.json` files and convert them between format 3 (3.0.x and 3.1.x) and draft IR v4, using the same library readers, codecs, and migrations that Python callers use. +Gives morphir-python users commands to inspect versioned `morphir-ir.json` files and convert them between format 3 (3.0.x and 3.1.x) and draft IR v4, using the same library readers, codecs, and migrations that Python callers use. ## Requirements diff --git a/packages/morphir-tools/README.md b/packages/morphir-tools/README.md index 6fae6017..e9e940d0 100644 --- a/packages/morphir-tools/README.md +++ b/packages/morphir-tools/README.md @@ -1,4 +1,4 @@ -# Morphir Tools +# Morphir Python CLI tools and extensions for [Morphir Python](https://github.com/finos/morphir-python). @@ -12,30 +12,30 @@ This package provides: ## Installation ```bash -pip install morphir-tools +pip install morphir-python ``` Or with uv: ```bash -uv add morphir-tools +uv add morphir-python ``` This will also install the `morphir` core library as a dependency. ## Usage -The `morphir-tools` command inspects and converts versioned `morphir-ir.json` +The `morphir-python` command inspects and converts versioned `morphir-ir.json` files. It reads format 3.0.x (what morphir-elm writes), format 3.1.x (which adds the v3 `Specs` distribution) and draft IR 4.0.x, reads the legacy formats 1 and 2 to upgrade them, and uses the `morphir` library for every read, write, and migration. ```bash -morphir-tools ir inspect morphir-ir.json -morphir-tools ir convert morphir-ir.json --to v4 -o out.json --package-version 1.2.0 -morphir-tools ir convert v4.json --to v3 -o out.json --drop-source-locations -morphir-tools ir convert old-format-1.json --to v3 -o out.json +morphir-python ir inspect morphir-ir.json +morphir-python ir convert morphir-ir.json --to v4 -o out.json --package-version 1.2.0 +morphir-python ir convert v4.json --to v3 -o out.json --drop-source-locations +morphir-python ir convert old-format-1.json --to v3 -o out.json ``` `inspect` prints the normalized format version, the distribution kind diff --git a/packages/morphir-tools/pyproject.toml b/packages/morphir-tools/pyproject.toml index c390f35f..ea519caa 100644 --- a/packages/morphir-tools/pyproject.toml +++ b/packages/morphir-tools/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "morphir-tools" +name = "morphir-python" version = "0.0.1" description = "CLI tools and extensions for Morphir Python" readme = "README.md" @@ -35,7 +35,7 @@ Repository = "https://github.com/finos/morphir-python" Issues = "https://github.com/finos/morphir-python/issues" [project.scripts] -morphir-tools = "morphir_tools.cli:main" +morphir-python = "morphir_tools.cli:main" [build-system] requires = ["hatchling>=1.27"] diff --git a/packages/morphir-tools/src/morphir_tools/__init__.py b/packages/morphir-tools/src/morphir_tools/__init__.py index 31cfe934..7d28926c 100644 --- a/packages/morphir-tools/src/morphir_tools/__init__.py +++ b/packages/morphir-tools/src/morphir_tools/__init__.py @@ -1,4 +1,4 @@ -"""Morphir Tools - CLI tools and extensions for Morphir Python. +"""Morphir Python - CLI tools and extensions for Morphir Python. This package provides command-line tools and utilities for working with Morphir domain models, including code generation and development tools. diff --git a/packages/morphir-tools/src/morphir_tools/cli.py b/packages/morphir-tools/src/morphir_tools/cli.py index f3338177..9a3369d4 100644 --- a/packages/morphir-tools/src/morphir_tools/cli.py +++ b/packages/morphir-tools/src/morphir_tools/cli.py @@ -1,4 +1,4 @@ -"""The ``morphir-tools`` command line. +"""The ``morphir-python`` command line. Every IR operation goes through the `morphir` library: the shared format-version reader, the v3 (3.0.x and 3.1.x) and draft v4 codecs, and `morphir.ir.migrate`. @@ -31,7 +31,7 @@ from morphir.ir.v4.path import Path as Path4 app = typer.Typer( - name="morphir-tools", + name="morphir-python", help="Command-line tools for Morphir IR.", no_args_is_help=True, ) @@ -219,5 +219,5 @@ def convert( def main() -> None: - """Run the ``morphir-tools`` command line.""" + """Run the ``morphir-python`` command line.""" app() diff --git a/packages/morphir/README.md b/packages/morphir/README.md index 22aa052d..76d989ad 100644 --- a/packages/morphir/README.md +++ b/packages/morphir/README.md @@ -225,7 +225,7 @@ type there, which can differ from the declared type (an alias expanded, a type variable instantiated), and the draft codec then fails with `no_v4_spelling`. Opt in to dropping them with `v3_to_v4(..., drop_input_attributes=True)`, `drop_input_attributes(dist)` for any v4 distribution, or -`morphir-tools ir convert --to v4 --drop-input-attributes`. Each input that loses +`morphir-python ir convert --to v4 --drop-input-attributes`. Each input that loses information reports `input_attributes_dropped`; without the opt-in the in-memory migration stays lossless. @@ -381,4 +381,4 @@ Apache-2.0 - see the [LICENSE](../../LICENSE) file for details. ## Related Packages -- [morphir-tools](https://pypi.org/project/morphir-tools/) - CLI tools and extensions +- [morphir-python](https://pypi.org/project/morphir-python/) - CLI tools and extensions diff --git a/packages/morphir/src/morphir/ir/v4/codec.py b/packages/morphir/src/morphir/ir/v4/codec.py index bd670de8..317ebfa4 100644 --- a/packages/morphir/src/morphir/ir/v4/codec.py +++ b/packages/morphir/src/morphir/ir/v4/codec.py @@ -1235,7 +1235,7 @@ def _encode_expression_body(defn: v.Definition, loc: Location) -> Json: f"input {encode_name(name, here)!r} carries attributes other than its " "declared type; ExpressionBody.inputTypes has no attribute slot. " "Drop them with morphir.ir.migrate.drop_input_attributes or " - "`morphir-tools ir convert --drop-input-attributes`", + "`morphir-python ir convert --drop-input-attributes`", ) return { "ExpressionBody": { diff --git a/tests/tools/__init__.py b/tests/tools/__init__.py index 6b0f7375..0f95fb16 100644 --- a/tests/tools/__init__.py +++ b/tests/tools/__init__.py @@ -1 +1 @@ -"""Unit tests for the morphir-tools package.""" +"""Unit tests for the morphir-python package.""" diff --git a/tests/tools/test_cli.py b/tests/tools/test_cli.py index 9914728c..2e8be39f 100644 --- a/tests/tools/test_cli.py +++ b/tests/tools/test_cli.py @@ -1,4 +1,4 @@ -"""Tests for the morphir-tools IR interchange CLI (capability tools/ir-interchange).""" +"""Tests for the morphir-python IR interchange CLI (capability tools/ir-interchange).""" import json from pathlib import Path @@ -39,6 +39,11 @@ def v4_file(tmp_path: Path) -> Path: class TestHelp: + def test_command_name(self) -> None: + result = run("--help") + assert result.exit_code == 0 + assert "morphir-python" in result.output + def test_ir_help(self) -> None: result = run("ir", "--help") assert result.exit_code == 0 @@ -50,7 +55,7 @@ def test_main_entry_point(self, monkeypatch: pytest.MonkeyPatch) -> None: from morphir_tools import cli - monkeypatch.setattr(sys, "argv", ["morphir-tools", "ir", "--help"]) + monkeypatch.setattr(sys, "argv", ["morphir-python", "ir", "--help"]) with pytest.raises(SystemExit) as info: cli.main() assert info.value.code == 0 diff --git a/tests/tools/test_tools.py b/tests/tools/test_tools.py index bc03a626..8ee0e6cc 100644 --- a/tests/tools/test_tools.py +++ b/tests/tools/test_tools.py @@ -1,10 +1,12 @@ -"""Tests for the morphir-tools package.""" +"""Tests for the morphir-python package.""" + +from importlib.metadata import distribution, version import morphir_tools class TestMorphirToolsPackage: - """Tests for the morphir-tools package initialization.""" + """Tests for the morphir-python package initialization.""" def test_version_is_defined(self) -> None: """The package should have a version attribute.""" @@ -14,6 +16,19 @@ def test_version_matches_expected(self, sample_version: str) -> None: """The version should match the expected value.""" assert morphir_tools.__version__ == sample_version + def test_installed_distribution_name(self) -> None: + """The tools import belongs to the morphir-python distribution.""" + assert version("morphir-python") == morphir_tools.__version__ + + def test_installed_console_command(self) -> None: + """The published command is named morphir-python.""" + commands = { + entry.name: entry.value + for entry in distribution("morphir-python").entry_points + if entry.group == "console_scripts" + } + assert commands == {"morphir-python": "morphir_tools.cli:main"} + def test_version_is_string(self) -> None: """The version should be a string.""" assert isinstance(morphir_tools.__version__, str) diff --git a/uv.lock b/uv.lock index 00624e74..2d9cc599 100644 --- a/uv.lock +++ b/uv.lock @@ -5,7 +5,7 @@ requires-python = ">=3.14" [manifest] members = [ "morphir", - "morphir-tools", + "morphir-python", "morphir-workspace", ] @@ -199,7 +199,7 @@ version = "0.0.1" source = { editable = "packages/morphir" } [[package]] -name = "morphir-tools" +name = "morphir-python" version = "0.0.1" source = { editable = "packages/morphir-tools" } dependencies = [ From 6abe5b267df5f7426979651ace82e369010ada6b Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Tue, 27 Oct 2026 08:32:46 -0500 Subject: [PATCH 2/2] build: bundle library and CLI in morphir-python Build one public distribution from the root project, including both source components. Keep the pure core workspace for development and remove the public dependency on an unpublished morphir distribution. Qualify and publish only the bundled wheel and source archive. Verify source inclusion, installation without a separate core wheel, and the morphir-python command before promotion. --- .config/mise/tasks/build | 7 +- .config/mise/tasks/release-check | 37 +++++++---- .github/workflows/ci.yml | 11 +--- .github/workflows/release.yml | 25 +------- AGENTS.md | 9 ++- README.md | 92 ++++++++++----------------- packages/morphir-tools/README.md | 6 +- packages/morphir-tools/pyproject.toml | 45 ------------- packages/morphir/README.md | 7 +- pyproject.toml | 44 +++++++++++-- tests/tools/test_tools.py | 5 ++ uv.lock | 20 ++---- 12 files changed, 129 insertions(+), 179 deletions(-) delete mode 100644 packages/morphir-tools/pyproject.toml diff --git a/.config/mise/tasks/build b/.config/mise/tasks/build index d6d4507c..059c4ada 100755 --- a/.config/mise/tasks/build +++ b/.config/mise/tasks/build @@ -1,11 +1,8 @@ #!/usr/bin/env bash -#MISE description="Build all packages" +#MISE description="Build the bundled morphir-python distribution" set -euo pipefail -echo "Building morphir package..." -uv build packages/morphir --out-dir packages/morphir/dist - echo "Building morphir-python package..." -uv build packages/morphir-tools --out-dir packages/morphir-tools/dist +uv build --out-dir dist echo "Build complete!" diff --git a/.config/mise/tasks/release-check b/.config/mise/tasks/release-check index ab22d69d..3b706c50 100755 --- a/.config/mise/tasks/release-check +++ b/.config/mise/tasks/release-check @@ -15,11 +15,9 @@ import zipfile from email.parser import BytesParser from pathlib import Path -versions = set() -for folder, name in (("morphir", "morphir"), ("morphir-tools", "morphir-python")): - directory = Path("packages") / folder +for folder, name in ((".", "morphir-python"),): + directory = Path(folder) project = tomllib.loads((directory / "pyproject.toml").read_text())["project"] - versions.add(project["version"]) tag = os.environ.get("RELEASE_TAG", "") if tag and tag not in (f"v{project['version']}", f"python-v{project['version']}"): raise SystemExit(f"{tag} does not match {name} {project['version']}") @@ -36,6 +34,12 @@ for folder, name in (("morphir", "morphir"), ("morphir-tools", "morphir-python") metadata = [n for n in archive.namelist() if n.endswith(".dist-info/METADATA")] raw = archive.read(metadata[0]) has_license = any(n.endswith("/licenses/LICENSE") for n in archive.namelist()) + for source in (Path("packages/morphir/src"), Path("packages/morphir-tools/src")): + expected_sources = {str(p.relative_to(source)) for p in source.rglob("*") + if p.is_file() and p.suffix in (".py", ".typed")} + missing = expected_sources - set(archive.namelist()) + if missing: + raise SystemExit(f"Missing bundled sources in {path}: {missing}") else: with tarfile.open(path) as archive: member = archive.extractfile(f"{stem}/PKG-INFO") @@ -48,39 +52,46 @@ for folder, name in (("morphir", "morphir"), ("morphir-tools", "morphir-python") raise SystemExit(f"Incorrect name or version in {path}") if metadata["License-Expression"] != "Apache-2.0" or not has_license: raise SystemExit(f"Missing Apache-2.0 license in {path}") -if len(versions) != 1: - raise SystemExit("Both packages must use the same release version") + if any(req.startswith("morphir") for req in metadata.get_all("Requires-Dist", [])): + raise SystemExit(f"{path} must not depend on a separate Morphir distribution") PY uvx --from twine==7.0.0 twine check --strict \ - packages/morphir/dist/* packages/morphir-tools/dist/* + dist/* uv export --frozen --all-packages --no-default-groups --no-emit-workspace \ --no-hashes --output-file "$release_dir/constraints.txt" uv venv --clear --python 3.14 "$release_dir/venv" release_python="$release_dir/venv/bin/python" uv pip install --python "$release_python" --constraint "$release_dir/constraints.txt" \ - packages/morphir/dist/*.whl packages/morphir-tools/dist/*.whl + dist/*.whl uv pip check --python "$release_python" "$release_python" -I - <<'PY' -from importlib.metadata import version +from importlib.metadata import PackageNotFoundError, version import morphir import morphir_tools from morphir.ir import format_version from morphir.sdk import basics -assert morphir.__version__ == version("morphir") +assert morphir.__version__ == version("morphir-python") assert morphir_tools.__version__ == version("morphir-python") assert basics.round(2.5) == 3 assert format_version.V3.major == 3 +try: + version("morphir") +except PackageNotFoundError: + pass +else: + raise AssertionError("Bundle verification must not install a separate morphir wheel") PY "$release_dir/venv/bin/morphir-python" --help +"$release_dir/venv/bin/morphir-python" ir inspect tests/fixtures/ir/v3/elm-simpleValueTree.json -# These two first-party packages are not yet on PyPI. Audit the exact installed -# third-party versions, without querying PyPI for the unpublished packages. -uv pip freeze --python "$release_python" --exclude morphir --exclude morphir-python \ +# Audit the exact installed third-party versions without querying PyPI for the +# first-party bundle being released. +uv pip freeze --python "$release_python" --exclude morphir-python \ > "$release_dir/requirements.txt" uvx --from pip-audit==2.10.1 pip-audit --strict --no-deps --disable-pip \ --requirement "$release_dir/requirements.txt" \ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index faf9e141..b088057b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ on: workflow_call: inputs: release-tag: - description: Tag that must match both package versions + description: Tag that must match the bundled package version type: string default: '' @@ -97,18 +97,11 @@ jobs: env: RELEASE_TAG: ${{ inputs.release-tag }} - - name: Upload morphir distributions - uses: actions/upload-artifact@v6 - with: - name: morphir-dist - path: packages/morphir/dist/ - if-no-files-found: error - - name: Upload morphir-python distributions uses: actions/upload-artifact@v6 with: name: morphir-python-dist - path: packages/morphir-tools/dist/ + path: dist/ if-no-files-found: error - name: Upload release scan reports diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 54d04cdd..047c3d13 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,29 +22,10 @@ jobs: with: release-tag: ${{ github.ref_type == 'tag' && github.ref_name || '' }} - publish-morphir: - name: Publish morphir to PyPI - if: github.event_name == 'push' && github.repository == 'finos/morphir-python' - needs: qualify - runs-on: ubuntu-latest - permissions: - id-token: write - environment: - name: pypi - url: https://pypi.org/project/morphir/ - steps: - - name: Download morphir dist - uses: actions/download-artifact@v7 - with: - name: morphir-dist - path: dist/ - - - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - publish-morphir-python: name: Publish morphir-python to PyPI - needs: [qualify, publish-morphir] + if: github.event_name == 'push' && github.repository == 'finos/morphir-python' + needs: qualify runs-on: ubuntu-latest permissions: id-token: write @@ -63,7 +44,7 @@ jobs: github-release: name: Create GitHub Release - needs: [publish-morphir, publish-morphir-python] + needs: publish-morphir-python runs-on: ubuntu-latest permissions: contents: write diff --git a/AGENTS.md b/AGENTS.md index b2912fed..e957b365 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,8 +11,8 @@ Morphir Python is a port of Morphir to Python, providing functional domain model ``` morphir-python/ ├── packages/ -│ ├── morphir/ # Core library - IR models, types, pure functions -│ └── morphir-tools/ # CLI tools and extensions (depends on morphir) +│ ├── morphir/ # Core library workspace - IR models, types, pure functions +│ └── morphir-tools/ # CLI source; root morphir-python bundles library and CLI ├── tests/ │ ├── unit/ # Unit tests for morphir package │ └── tools/ # Unit tests for morphir-python package @@ -21,6 +21,11 @@ morphir-python/ ### Package Responsibilities +The root `pyproject.toml` publishes one `morphir-python` distribution containing +both source components. It provides `import morphir`, `import morphir_tools`, and +the `morphir-python` command. The core remains a standalone workspace for development; +the release workflow does not publish or require a separate `morphir` PyPI package. + **`morphir` (core library)** - Morphir IR (Intermediate Representation) models, in two versions: `morphir.ir.v3` (formats 3.0.0 and 3.1.0; morphir-elm is normative for 3.0.0, diff --git a/README.md b/README.md index bd9a1e5e..ac378271 100644 --- a/README.md +++ b/README.md @@ -7,34 +7,9 @@ Python port of [Morphir](https://morphir.finos.org) - a library of tools for bui Morphir enables you to write business logic once and use it across multiple platforms, languages, and runtimes. It provides a strongly-typed intermediate representation (IR) that captures the semantics of your domain model. -## Packages - -This monorepo contains two packages: - -| Package | Description | PyPI | -|---------|-------------|------| -| `morphir` | Core library - IR models, the Morphir SDK runtime (`morphir.sdk`), types, and functional domain modeling primitives | [![PyPI](https://img.shields.io/pypi/v/morphir)](https://pypi.org/project/morphir/) | -| `morphir-python` | CLI tools and extensions for working with Morphir | [![PyPI](https://img.shields.io/pypi/v/morphir-python)](https://pypi.org/project/morphir-python/) | - ## Installation -### Library Only - -If you just need the Morphir library for your Python project: - -```bash -pip install morphir -``` - -Or with uv: - -```bash -uv add morphir -``` - -### With CLI Tools - -For the full toolset including CLI commands: +Install the library, Morphir SDK runtime, and CLI together: ```bash pip install morphir-python @@ -46,6 +21,13 @@ Or with uv: uv add morphir-python ``` +| PyPI distribution | Python imports | Command | +|-------------------|----------------|---------| +| [`morphir-python`](https://pypi.org/project/morphir-python/) | `morphir`, `morphir_tools` | `morphir-python` | + +The distribution includes the core library and tools. It does not require a +separate `morphir` package from PyPI. + ## Quick Start Morphir IR comes in two first-class versions: format 3.0.0 (`morphir.ir.v3`), which @@ -166,21 +148,14 @@ mise run check ## Releases -The initial release is `0.0.1` for both PyPI distributions: - -| PyPI distribution | Python import | Command | -|-------------------|---------------|---------| -| `morphir` | `morphir` | None | -| `morphir-python` | `morphir_tools` | `morphir-python` | - -`morphir-python` is both the CLI distribution and repository name. The workspace -itself is not published. -Both packages require Python 3.14 or later. `morphir-python` installs `morphir` as -a dependency. +The initial PyPI release is `morphir-python==0.0.1`, containing both the `morphir` +library and the `morphir-python` CLI. Python 3.14 or later is required. The root +`pyproject.toml` defines the public distribution. The library workspace under +`packages/morphir/` remains independent of CLI dependencies for development; it +is included in the bundle rather than published separately by this workflow. -Publishing uses [PyPI trusted publishing](https://docs.pypi.org/trusted-publishers/using-a-publisher/), -without a stored PyPI token. Configure a publisher for **each** distribution on -PyPI, including a pending publisher if the project has not been created yet: +Publishing uses [PyPI trusted publishing](https://docs.pypi.org/trusted-publishers/using-a-publisher/) +without a stored PyPI token. Configure the `morphir-python` project on PyPI with: | Publisher field | Value | |-----------------|-------| @@ -191,32 +166,33 @@ PyPI, including a pending publisher if the project has not been created yet: The GitHub `pypi` environment permits release tags matching `v*` or `python-v*`. After maintainer approval, push a tag such as `python-v0.0.1` whose version matches -both package metadata files and their runtime `__version__` values. Update the tools package's minimum -`morphir` dependency and refresh `uv.lock` when changing versions. +root package metadata and the runtime `__version__` values. Refresh `uv.lock` +when changing versions. A new tag records the corrected source without moving +the earlier failed `v0.0.1` tag. The release workflow runs the same lint, format, type, coverage and BDD checks as -PR CI. It builds each package once, validates wheel and source metadata and license -files, installs the wheels in an isolated environment, checks imports and the CLI, -then scans the locked runtime dependency versions with `pip-audit` and -`pip-licenses`. Known vulnerabilities, audit failures, unknown licenses and -licenses outside the exact Apache-2.0, MIT, BSD-2-Clause, BSD-3-Clause and ISC -allowlist block publication. The license allowlist also accepts the corresponding -legacy license classifiers. The CVE scan queries third-party dependencies; -the unpublished first-party packages are excluded from that query. These checks -use package metadata and known vulnerability advisories, not a source-code license -or security review. Scan reports are retained as workflow artifacts. - -Only the two publishing jobs receive `id-token: write`. They publish the checked -artifacts, core library first, and then create a GitHub release. The workflow's -manual **Run workflow** action runs qualification without publishing. To qualify -locally: +PR CI. It builds the bundle once, validates wheel and source metadata and license +files, verifies that every library and CLI source file is in the wheel, installs +only that wheel in an isolated environment, checks imports and the CLI, then +scans the locked runtime dependency versions with `pip-audit` and `pip-licenses`. +Known vulnerabilities, audit failures, unknown licenses and licenses outside the +Apache-2.0, MIT, BSD-2-Clause, BSD-3-Clause and ISC allowlist block publication. +The allowlist also accepts corresponding legacy license classifiers. The CVE +scan queries third-party dependencies, excluding the first-party bundle being +released. These checks use package metadata and known vulnerability advisories; +they do not replace a source-code license or security review. Scan reports are +retained as workflow artifacts. + +Only the publishing job receives `id-token: write`. It publishes the checked +artifacts, then a separate job creates the GitHub release. The workflow's manual +**Run workflow** action runs qualification without publishing. To qualify locally: ```bash mise run install mise run check mise run coverage mise run build -RELEASE_TAG=v0.0.1 mise run release-check +RELEASE_TAG=python-v0.0.1 mise run release-check ``` ## Project Principles diff --git a/packages/morphir-tools/README.md b/packages/morphir-tools/README.md index e9e940d0..66b8a68d 100644 --- a/packages/morphir-tools/README.md +++ b/packages/morphir-tools/README.md @@ -21,7 +21,9 @@ Or with uv: uv add morphir-python ``` -This will also install the `morphir` core library as a dependency. +The `morphir-python` distribution includes the `morphir` core library and SDK +runtime alongside the CLI. It does not depend on a separate `morphir` PyPI +package. Python imports remain `morphir` and `morphir_tools`. ## Usage @@ -56,7 +58,7 @@ format-version rules and the draft v4 status. ## Requirements - Python 3.14+ -- morphir (core library) +- Core library and SDK included ## License diff --git a/packages/morphir-tools/pyproject.toml b/packages/morphir-tools/pyproject.toml deleted file mode 100644 index ea519caa..00000000 --- a/packages/morphir-tools/pyproject.toml +++ /dev/null @@ -1,45 +0,0 @@ -[project] -name = "morphir-python" -version = "0.0.1" -description = "CLI tools and extensions for Morphir Python" -readme = "README.md" -license = "Apache-2.0" -license-files = ["LICENSE"] -requires-python = ">=3.14" -keywords = ["morphir", "cli", "tools", "code-generation", "domain-modeling"] -classifiers = [ - "Development Status :: 3 - Alpha", - "Environment :: Console", - "Intended Audience :: Developers", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.14", - "Topic :: Software Development :: Code Generators", - "Topic :: Software Development :: Libraries", - "Typing :: Typed", -] -authors = [ - { name = "FINOS", email = "morphir@finos.org" } -] -dependencies = [ - "morphir>=0.0.1", - "typer>=0.15", - # Rich uses Pygments; older versions are affected by CVE-2026-4539. - "pygments>=2.20.0", -] - -[project.urls] -Homepage = "https://github.com/finos/morphir-python" -Documentation = "https://morphir.finos.org" -Repository = "https://github.com/finos/morphir-python" -Issues = "https://github.com/finos/morphir-python/issues" - -[project.scripts] -morphir-python = "morphir_tools.cli:main" - -[build-system] -requires = ["hatchling>=1.27"] -build-backend = "hatchling.build" - -[tool.hatch.build.targets.wheel] -packages = ["src/morphir_tools"] diff --git a/packages/morphir/README.md b/packages/morphir/README.md index 76d989ad..233347f0 100644 --- a/packages/morphir/README.md +++ b/packages/morphir/README.md @@ -11,14 +11,17 @@ This is the core library package containing: ## Installation +The core library is included in the `morphir-python` distribution, together with +the SDK runtime and CLI. Python code continues to use `import morphir`. + ```bash -pip install morphir +pip install morphir-python ``` Or with uv: ```bash -uv add morphir +uv add morphir-python ``` ## Usage diff --git a/pyproject.toml b/pyproject.toml index 77e54459..b1d35983 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,45 @@ [project] -name = "morphir-workspace" +name = "morphir-python" version = "0.0.1" -description = "Morphir Python monorepo workspace" +description = "Morphir domain modeling library, SDK runtime, and CLI for Python" requires-python = ">=3.14" readme = "README.md" license = "Apache-2.0" +license-files = ["LICENSE"] +authors = [{ name = "FINOS", email = "morphir@finos.org" }] +keywords = ["morphir", "domain-modeling", "functional", "cli", "ir"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.14", + "Typing :: Typed", +] +dependencies = [ + "typer>=0.15", + # Rich uses Pygments; older versions are affected by CVE-2026-4539. + "pygments>=2.20.0", +] + +[project.urls] +Homepage = "https://github.com/finos/morphir-python" +Documentation = "https://morphir.finos.org" +Repository = "https://github.com/finos/morphir-python" +Issues = "https://github.com/finos/morphir-python/issues" + +[project.scripts] +morphir-python = "morphir_tools.cli:main" + +[build-system] +requires = ["hatchling>=1.27"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["packages/morphir/src/morphir", "packages/morphir-tools/src/morphir_tools"] + +[tool.hatch.build.targets.sdist] +only-include = ["packages", "README.md", "LICENSE", "uv.lock"] [dependency-groups] dev = [ @@ -24,10 +59,7 @@ test = [ managed = true [tool.uv.workspace] -members = ["packages/*"] - -[tool.uv.sources] -morphir = { workspace = true } +members = ["packages/morphir"] # ============================================================================= # Ruff Configuration diff --git a/tests/tools/test_tools.py b/tests/tools/test_tools.py index 8ee0e6cc..c7ffdfc3 100644 --- a/tests/tools/test_tools.py +++ b/tests/tools/test_tools.py @@ -29,6 +29,11 @@ def test_installed_console_command(self) -> None: } assert commands == {"morphir-python": "morphir_tools.cli:main"} + def test_bundle_does_not_require_separate_core(self) -> None: + """Installing the bundle must not query PyPI for an unpublished core.""" + requirements = distribution("morphir-python").requires or [] + assert not any(item.startswith("morphir") for item in requirements) + def test_version_is_string(self) -> None: """The version should be a string.""" assert isinstance(morphir_tools.__version__, str) diff --git a/uv.lock b/uv.lock index 2d9cc599..e57b1709 100644 --- a/uv.lock +++ b/uv.lock @@ -6,7 +6,6 @@ requires-python = ">=3.14" members = [ "morphir", "morphir-python", - "morphir-workspace", ] [[package]] @@ -201,25 +200,12 @@ source = { editable = "packages/morphir" } [[package]] name = "morphir-python" version = "0.0.1" -source = { editable = "packages/morphir-tools" } +source = { editable = "." } dependencies = [ - { name = "morphir" }, { name = "pygments" }, { name = "typer" }, ] -[package.metadata] -requires-dist = [ - { name = "morphir", editable = "packages/morphir" }, - { name = "pygments", specifier = ">=2.20.0" }, - { name = "typer", specifier = ">=0.15" }, -] - -[[package]] -name = "morphir-workspace" -version = "0.0.1" -source = { virtual = "." } - [package.dev-dependencies] dev = [ { name = "mypy" }, @@ -235,6 +221,10 @@ test = [ ] [package.metadata] +requires-dist = [ + { name = "pygments", specifier = ">=2.20.0" }, + { name = "typer", specifier = ">=0.15" }, +] [package.metadata.requires-dev] dev = [