Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions .config/mise/tasks/build
Original file line number Diff line number Diff line change
@@ -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-tools package..."
uv build packages/morphir-tools --out-dir packages/morphir-tools/dist
echo "Building morphir-python package..."
uv build --out-dir dist

echo "Build complete!"
43 changes: 27 additions & 16 deletions .config/mise/tasks/release-check
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,11 @@ import zipfile
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-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 != 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"}
Expand All @@ -36,6 +34,12 @@ for name in ("morphir", "morphir-tools"):
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")
Expand All @@ -48,39 +52,46 @@ for name in ("morphir", "morphir-tools"):
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_tools.__version__ == version("morphir-tools")
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-tools" --help
"$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-tools \
# 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" \
Expand Down
15 changes: 4 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: ''

Expand Down Expand Up @@ -97,18 +97,11 @@ jobs:
env:
RELEASE_TAG: ${{ inputs.release-tag }}

- name: Upload morphir distributions
- name: Upload morphir-python distributions
uses: actions/upload-artifact@v6
with:
name: morphir-dist
path: packages/morphir/dist/
if-no-files-found: error

- name: Upload morphir-tools distributions
uses: actions/upload-artifact@v6
with:
name: morphir-tools-dist
path: packages/morphir-tools/dist/
name: morphir-python-dist
path: dist/
if-no-files-found: error

- name: Upload release scan reports
Expand Down
32 changes: 7 additions & 25 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
tags:
- 'v*'
- 'python-v*'
# Qualification only. Publishing requires a tag push.
workflow_dispatch:

Expand All @@ -21,48 +22,29 @@ jobs:
with:
release-tag: ${{ github.ref_type == 'tag' && github.ref_name || '' }}

publish-morphir:
name: Publish morphir to PyPI
publish-morphir-python:
name: Publish morphir-python 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/
url: https://pypi.org/project/morphir-python/
steps:
- name: Download morphir dist
- name: Download morphir-python 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-tools:
name: Publish morphir-tools to PyPI
needs: [qualify, publish-morphir]
runs-on: ubuntu-latest
permissions:
id-token: write
environment:
name: pypi
url: https://pypi.org/project/morphir-tools/
steps:
- name: Download morphir-tools dist
uses: actions/download-artifact@v7
with:
name: morphir-tools-dist
name: morphir-python-dist
path: dist/

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1

github-release:
name: Create GitHub Release
needs: [publish-morphir, publish-morphir-tools]
needs: publish-morphir-python
runs-on: ubuntu-latest
permissions:
contents: write
Expand Down
17 changes: 11 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,21 @@ 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-tools package
│ └── tools/ # Unit tests for morphir-python package
└── features/ # BDD feature tests (behave)
```

### 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,
Expand All @@ -36,8 +41,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
Expand Down Expand Up @@ -266,7 +271,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
Expand Down
99 changes: 38 additions & 61 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,44 +7,26 @@ 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-tools` | CLI tools and extensions for working with Morphir | [![PyPI](https://img.shields.io/pypi/v/morphir-tools)](https://pypi.org/project/morphir-tools/) |

## Installation

### Library Only

If you just need the Morphir library for your Python project:
Install the library, Morphir SDK runtime, and CLI together:

```bash
pip install morphir
pip install morphir-python
```

Or with uv:

```bash
uv add morphir
```

### With CLI Tools

For the full toolset including CLI commands:

```bash
pip install morphir-tools
uv add morphir-python
```

Or with uv:
| PyPI distribution | Python imports | Command |
|-------------------|----------------|---------|
| [`morphir-python`](https://pypi.org/project/morphir-python/) | `morphir`, `morphir_tools` | `morphir-python` |

```bash
uv add morphir-tools
```
The distribution includes the core library and tools. It does not require a
separate `morphir` package from PyPI.

## Quick Start

Expand All @@ -65,8 +47,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)
Expand Down Expand Up @@ -166,20 +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-tools` | `morphir_tools` | `morphir-tools` |

`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
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 |
|-----------------|-------|
Expand All @@ -188,34 +164,35 @@ 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
`morphir` dependency and refresh `uv.lock` when changing versions.
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
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
Expand Down
2 changes: 1 addition & 1 deletion openspec/specs/tools/ir-interchange/spec.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
Loading
Loading