morphir-python 0.0.1__tar.gz → 0.0.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {morphir_python-0.0.1 → morphir_python-0.0.3}/.gitignore +6 -0
- morphir_python-0.0.3/PKG-INFO +98 -0
- {morphir_python-0.0.1/packages/morphir-tools → morphir_python-0.0.3}/README.md +5 -4
- {morphir_python-0.0.1/packages/morphir → morphir_python-0.0.3}/pyproject.toml +18 -7
- {morphir_python-0.0.1/packages/morphir-tools → morphir_python-0.0.3}/src/morphir_tools/__init__.py +2 -2
- morphir_python-0.0.3/src/morphir_tools/_output.py +114 -0
- {morphir_python-0.0.1/packages/morphir-tools → morphir_python-0.0.3}/src/morphir_tools/cli.py +176 -2
- morphir_python-0.0.3/src/morphir_tools/python/__init__.py +71 -0
- morphir_python-0.0.3/src/morphir_tools/python/_api.py +215 -0
- morphir_python-0.0.3/src/morphir_tools/python/_bridge.py +84 -0
- morphir_python-0.0.3/src/morphir_tools/python/_types.py +201 -0
- morphir_python-0.0.3/src/morphir_tools/python/diagnostics.py +33 -0
- morphir_python-0.0.1/PKG-INFO +0 -269
- morphir_python-0.0.1/README.md +0 -246
- morphir_python-0.0.1/packages/morphir/LICENSE +0 -201
- morphir_python-0.0.1/packages/morphir/README.md +0 -387
- morphir_python-0.0.1/packages/morphir/src/morphir/__init__.py +0 -14
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/__init__.py +0 -110
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/access_controlled.py +0 -15
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/decorations.py +0 -19
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/diagnostics.py +0 -105
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/distribution.py +0 -21
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/document.py +0 -37
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/documented.py +0 -11
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/format_version.py +0 -483
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/fqname.py +0 -9
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/json.py +0 -11
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/literal.py +0 -25
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/meta.py +0 -11
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/migrate.py +0 -1267
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/module.py +0 -17
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/name.py +0 -27
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/package.py +0 -15
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/path.py +0 -25
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/qname.py +0 -9
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/ref.py +0 -17
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/type.py +0 -37
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/type_constraints.py +0 -33
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/type_def.py +0 -19
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/type_spec.py +0 -25
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v3/__init__.py +0 -64
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v3/_legacy_value.py +0 -201
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v3/access_controlled.py +0 -31
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v3/codec.py +0 -1275
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v3/codec_v1.py +0 -491
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v3/codec_v2.py +0 -187
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v3/distribution.py +0 -42
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v3/documented.py +0 -13
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v3/fqname.py +0 -30
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v3/literal.py +0 -46
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v3/module.py +0 -32
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v3/name.py +0 -42
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v3/package.py +0 -23
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v3/path.py +0 -29
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v3/qname.py +0 -16
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v3/type.py +0 -126
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v3/value.py +0 -235
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/__init__.py +0 -160
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/access_controlled.py +0 -17
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/annotation.py +0 -75
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/codec.py +0 -2161
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/decorations.py +0 -88
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/distribution.py +0 -56
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/document.py +0 -69
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/documented.py +0 -10
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/fqname.py +0 -69
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/incompleteness.py +0 -78
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/json.py +0 -103
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/literal.py +0 -51
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/meta.py +0 -27
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/module.py +0 -30
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/name.py +0 -69
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/package.py +0 -19
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/path.py +0 -50
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/qname.py +0 -34
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/ref.py +0 -35
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/type.py +0 -140
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/type_constraints.py +0 -91
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/type_def.py +0 -38
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/type_spec.py +0 -64
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/value.py +0 -480
- morphir_python-0.0.1/packages/morphir/src/morphir/ir/value.py +0 -81
- morphir_python-0.0.1/packages/morphir/src/morphir/py.typed +0 -0
- morphir_python-0.0.1/packages/morphir/src/morphir/sdk/__init__.py +0 -110
- morphir_python-0.0.1/packages/morphir/src/morphir/sdk/_compare.py +0 -187
- morphir_python-0.0.1/packages/morphir/src/morphir/sdk/aggregate.py +0 -421
- morphir_python-0.0.1/packages/morphir/src/morphir/sdk/basics.py +0 -504
- morphir_python-0.0.1/packages/morphir/src/morphir/sdk/char.py +0 -110
- morphir_python-0.0.1/packages/morphir/src/morphir/sdk/decimal.py +0 -311
- morphir_python-0.0.1/packages/morphir/src/morphir/sdk/dict.py +0 -336
- morphir_python-0.0.1/packages/morphir/src/morphir/sdk/instant.py +0 -118
- morphir_python-0.0.1/packages/morphir/src/morphir/sdk/int.py +0 -86
- morphir_python-0.0.1/packages/morphir/src/morphir/sdk/key.py +0 -522
- morphir_python-0.0.1/packages/morphir/src/morphir/sdk/list.py +0 -356
- morphir_python-0.0.1/packages/morphir/src/morphir/sdk/local_date.py +0 -436
- morphir_python-0.0.1/packages/morphir/src/morphir/sdk/local_time.py +0 -267
- morphir_python-0.0.1/packages/morphir/src/morphir/sdk/maybe.py +0 -135
- morphir_python-0.0.1/packages/morphir/src/morphir/sdk/number.py +0 -237
- morphir_python-0.0.1/packages/morphir/src/morphir/sdk/regex.py +0 -287
- morphir_python-0.0.1/packages/morphir/src/morphir/sdk/result.py +0 -162
- morphir_python-0.0.1/packages/morphir/src/morphir/sdk/result_list.py +0 -169
- morphir_python-0.0.1/packages/morphir/src/morphir/sdk/rule.py +0 -103
- morphir_python-0.0.1/packages/morphir/src/morphir/sdk/set.py +0 -214
- morphir_python-0.0.1/packages/morphir/src/morphir/sdk/stateful_app.py +0 -49
- morphir_python-0.0.1/packages/morphir/src/morphir/sdk/string.py +0 -427
- morphir_python-0.0.1/packages/morphir/src/morphir/sdk/tuple.py +0 -43
- morphir_python-0.0.1/packages/morphir/src/morphir/sdk/uuid.py +0 -209
- morphir_python-0.0.1/packages/morphir-tools/LICENSE +0 -201
- morphir_python-0.0.1/pyproject.toml +0 -177
- morphir_python-0.0.1/uv.lock +0 -527
- {morphir_python-0.0.1 → morphir_python-0.0.3}/LICENSE +0 -0
- {morphir_python-0.0.1/packages/morphir-tools → morphir_python-0.0.3}/src/morphir_tools/py.typed +0 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: morphir-python
|
|
3
|
+
Version: 0.0.3
|
|
4
|
+
Summary: CLI tools and extensions for Morphir Python
|
|
5
|
+
Project-URL: Homepage, https://github.com/finos/morphir-python
|
|
6
|
+
Project-URL: Documentation, https://morphir.finos.org
|
|
7
|
+
Project-URL: Repository, https://github.com/finos/morphir-python
|
|
8
|
+
Project-URL: Issues, https://github.com/finos/morphir-python/issues
|
|
9
|
+
Author-email: FINOS <morphir@finos.org>
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: cli,code-generation,domain-modeling,morphir,tools
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.14
|
|
23
|
+
Requires-Dist: morphir-python-native==0.1.0
|
|
24
|
+
Requires-Dist: morphir>=0.0.2
|
|
25
|
+
Requires-Dist: pygments>=2.20.0
|
|
26
|
+
Requires-Dist: typer>=0.15
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# Morphir Python
|
|
30
|
+
|
|
31
|
+
CLI tools and extensions for [Morphir Python](https://github.com/finos/morphir-python).
|
|
32
|
+
|
|
33
|
+
This package provides:
|
|
34
|
+
|
|
35
|
+
- Command-line interface for Morphir operations
|
|
36
|
+
- Code generation backends
|
|
37
|
+
- File I/O utilities
|
|
38
|
+
- Development tools
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install morphir-python
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Or with uv:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
uv add morphir-python
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The `morphir-python` distribution contains the CLI and depends on the separate
|
|
53
|
+
`morphir` library/SDK distribution. Python imports remain `morphir` and
|
|
54
|
+
`morphir_tools`. Version 0.0.1 was a bundle; for migration to CLI-only 0.0.2, see
|
|
55
|
+
the [repository installation guide](../../README.md#migrating-from-the-bundled-001-release).
|
|
56
|
+
|
|
57
|
+
## Usage
|
|
58
|
+
|
|
59
|
+
The `morphir-python` command inspects and converts versioned `morphir-ir.json`
|
|
60
|
+
files. It reads format 3.0.x (what morphir-elm writes), format 3.1.x (which adds
|
|
61
|
+
the v3 `Specs` distribution) and draft IR 4.0.x, reads the legacy formats 1 and 2
|
|
62
|
+
to upgrade them, and uses the `morphir` library for every read, write, and
|
|
63
|
+
migration.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
morphir-python ir inspect morphir-ir.json
|
|
67
|
+
morphir-python ir convert morphir-ir.json --to v4 -o out.json --package-version 1.2.0
|
|
68
|
+
morphir-python ir convert v4.json --to v3 -o out.json --drop-source-locations
|
|
69
|
+
morphir-python ir convert old-format-1.json --to v3 -o out.json
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`inspect` prints the normalized format version, the distribution kind
|
|
73
|
+
(`Library`, `Specs` or `Application`), and the package name, and labels v4 as
|
|
74
|
+
draft and formats 1 and 2 as legacy. `convert` handles `Library` and `Specs` in
|
|
75
|
+
both directions. It writes a v3 `Library` as format `3` and a v3 `Specs` as
|
|
76
|
+
`"3.1.0"`, the lowest version that holds each. A legacy format 1 or 2 file
|
|
77
|
+
converts to a format 3.0.0 `Library` or draft v4; nothing writes format 1 or 2.
|
|
78
|
+
`convert` writes through a temporary file and renames it only on success, so a
|
|
79
|
+
failed conversion leaves no output file.
|
|
80
|
+
Diagnostics go to standard error as `code at location: message`.
|
|
81
|
+
|
|
82
|
+
Exit codes are 0 for success (also with warnings), 1 for a diagnostic, and 2 for
|
|
83
|
+
a usage error. 3.x releases from 3.2.0 on are rejected; see the
|
|
84
|
+
[morphir package README](../morphir/README.md#morphir-ir-versions) for the
|
|
85
|
+
format-version rules and the draft v4 status.
|
|
86
|
+
|
|
87
|
+
## Requirements
|
|
88
|
+
|
|
89
|
+
- Python 3.14+
|
|
90
|
+
- `morphir>=0.0.2` and `morphir-python-native==0.1.0` (installed as dependencies)
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
Apache-2.0 - see the [LICENSE](../../LICENSE) file for details.
|
|
95
|
+
|
|
96
|
+
## Related Packages
|
|
97
|
+
|
|
98
|
+
- [morphir](https://pypi.org/project/morphir/) - Core library
|
|
@@ -21,9 +21,10 @@ Or with uv:
|
|
|
21
21
|
uv add morphir-python
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
The `morphir-python` distribution
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
The `morphir-python` distribution contains the CLI and depends on the separate
|
|
25
|
+
`morphir` library/SDK distribution. Python imports remain `morphir` and
|
|
26
|
+
`morphir_tools`. Version 0.0.1 was a bundle; for migration to CLI-only 0.0.2, see
|
|
27
|
+
the [repository installation guide](../../README.md#migrating-from-the-bundled-001-release).
|
|
27
28
|
|
|
28
29
|
## Usage
|
|
29
30
|
|
|
@@ -58,7 +59,7 @@ format-version rules and the draft v4 status.
|
|
|
58
59
|
## Requirements
|
|
59
60
|
|
|
60
61
|
- Python 3.14+
|
|
61
|
-
-
|
|
62
|
+
- `morphir>=0.0.2` and `morphir-python-native==0.1.0` (installed as dependencies)
|
|
62
63
|
|
|
63
64
|
## License
|
|
64
65
|
|
|
@@ -1,26 +1,34 @@
|
|
|
1
1
|
[project]
|
|
2
|
-
name = "morphir"
|
|
3
|
-
version = "0.0.
|
|
4
|
-
description = "
|
|
2
|
+
name = "morphir-python"
|
|
3
|
+
version = "0.0.3"
|
|
4
|
+
description = "CLI tools and extensions for Morphir Python"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
7
7
|
license-files = ["LICENSE"]
|
|
8
8
|
requires-python = ">=3.14"
|
|
9
|
-
keywords = ["morphir", "
|
|
9
|
+
keywords = ["morphir", "cli", "tools", "code-generation", "domain-modeling"]
|
|
10
10
|
classifiers = [
|
|
11
11
|
"Development Status :: 3 - Alpha",
|
|
12
|
+
"Environment :: Console",
|
|
12
13
|
"Intended Audience :: Developers",
|
|
13
14
|
"Operating System :: OS Independent",
|
|
14
15
|
"Programming Language :: Python :: 3",
|
|
15
16
|
"Programming Language :: Python :: 3.14",
|
|
17
|
+
"Topic :: Software Development :: Code Generators",
|
|
16
18
|
"Topic :: Software Development :: Libraries",
|
|
17
|
-
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
18
19
|
"Typing :: Typed",
|
|
19
20
|
]
|
|
20
21
|
authors = [
|
|
21
22
|
{ name = "FINOS", email = "morphir@finos.org" }
|
|
22
23
|
]
|
|
23
|
-
dependencies = [
|
|
24
|
+
dependencies = [
|
|
25
|
+
"morphir>=0.0.2",
|
|
26
|
+
# Exact qualified native compiler; checked again when the compiler first loads.
|
|
27
|
+
"morphir-python-native==0.1.0",
|
|
28
|
+
"typer>=0.15",
|
|
29
|
+
# Rich uses Pygments; older versions are affected by CVE-2026-4539.
|
|
30
|
+
"pygments>=2.20.0",
|
|
31
|
+
]
|
|
24
32
|
|
|
25
33
|
[project.urls]
|
|
26
34
|
Homepage = "https://github.com/finos/morphir-python"
|
|
@@ -28,9 +36,12 @@ Documentation = "https://morphir.finos.org"
|
|
|
28
36
|
Repository = "https://github.com/finos/morphir-python"
|
|
29
37
|
Issues = "https://github.com/finos/morphir-python/issues"
|
|
30
38
|
|
|
39
|
+
[project.scripts]
|
|
40
|
+
morphir-python = "morphir_tools.cli:main"
|
|
41
|
+
|
|
31
42
|
[build-system]
|
|
32
43
|
requires = ["hatchling>=1.27"]
|
|
33
44
|
build-backend = "hatchling.build"
|
|
34
45
|
|
|
35
46
|
[tool.hatch.build.targets.wheel]
|
|
36
|
-
packages = ["src/
|
|
47
|
+
packages = ["src/morphir_tools"]
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Safe publication of generated files.
|
|
2
|
+
|
|
3
|
+
Generation writes a complete tree into a new sibling staging directory and only
|
|
4
|
+
then moves it into place, so a failure never leaves partial output or touches
|
|
5
|
+
existing user files.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import shutil
|
|
12
|
+
import tempfile
|
|
13
|
+
from pathlib import Path, PurePosixPath
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from collections.abc import Iterable, Sequence
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def umask() -> int:
|
|
21
|
+
"""Return the process umask without changing it."""
|
|
22
|
+
mask = os.umask(0o022)
|
|
23
|
+
os.umask(mask)
|
|
24
|
+
return mask
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class OutputError(Exception):
|
|
28
|
+
"""Output cannot be published safely; nothing was written."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def validate_artifact_paths(paths: Iterable[str]) -> None:
|
|
32
|
+
"""Check that relative artifact paths stay inside one new directory tree.
|
|
33
|
+
|
|
34
|
+
Each path must be a normalized relative POSIX path. No two paths may name the
|
|
35
|
+
same file on a case-insensitive file system, and no path may be a parent
|
|
36
|
+
directory of another.
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
OutputError: For the first invalid path or collision.
|
|
40
|
+
"""
|
|
41
|
+
seen: dict[str, str] = {}
|
|
42
|
+
directories: dict[str, str] = {}
|
|
43
|
+
for path in paths:
|
|
44
|
+
pure = PurePosixPath(path)
|
|
45
|
+
if (
|
|
46
|
+
not path
|
|
47
|
+
or "\\" in path
|
|
48
|
+
or pure.is_absolute()
|
|
49
|
+
or ":" in pure.parts[0]
|
|
50
|
+
or any(part in {"", ".", ".."} for part in path.split("/"))
|
|
51
|
+
):
|
|
52
|
+
raise OutputError(f"artifact path {path!r} is not a safe relative path")
|
|
53
|
+
key = path.casefold()
|
|
54
|
+
if key in seen:
|
|
55
|
+
raise OutputError(f"artifact paths {seen[key]!r} and {path!r} collide")
|
|
56
|
+
seen[key] = path
|
|
57
|
+
for parent in pure.parents:
|
|
58
|
+
if str(parent) != ".":
|
|
59
|
+
directories.setdefault(str(parent).casefold(), path)
|
|
60
|
+
for key, path in seen.items():
|
|
61
|
+
if key in directories:
|
|
62
|
+
raise OutputError(
|
|
63
|
+
f"artifact path {path!r} is also a directory of {directories[key]!r}"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def check_destination(directory: Path) -> None:
|
|
68
|
+
"""Require an absent directory, or an empty real directory.
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
OutputError: When the destination is a symbolic link, a file or a
|
|
72
|
+
nonempty directory.
|
|
73
|
+
"""
|
|
74
|
+
if directory.is_symlink():
|
|
75
|
+
raise OutputError(f"output directory {directory} is a symbolic link")
|
|
76
|
+
if directory.exists():
|
|
77
|
+
if not directory.is_dir():
|
|
78
|
+
raise OutputError(f"output path {directory} is not a directory")
|
|
79
|
+
if any(directory.iterdir()):
|
|
80
|
+
raise OutputError(f"output directory {directory} is not empty")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def publish_tree(directory: Path, files: Sequence[tuple[str, str]]) -> None:
|
|
84
|
+
"""Write ``files`` to a staging directory, then move it to ``directory``.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
directory: The absent or empty destination.
|
|
88
|
+
files: Relative POSIX paths and their text, already validated.
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
OutputError: When the destination is unsuitable.
|
|
92
|
+
OSError: When writing or moving fails; the staging tree is removed and
|
|
93
|
+
the destination is unchanged.
|
|
94
|
+
"""
|
|
95
|
+
check_destination(directory)
|
|
96
|
+
parent = directory.parent
|
|
97
|
+
parent.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
# mkdtemp creates 0700; publish with the existing directory's mode, or the
|
|
99
|
+
# mode an ordinary mkdir would give under the user's umask.
|
|
100
|
+
mode = directory.stat().st_mode & 0o7777 if directory.exists() else 0o777 & ~umask()
|
|
101
|
+
staging = Path(tempfile.mkdtemp(prefix=f".{directory.name}.", dir=parent))
|
|
102
|
+
try:
|
|
103
|
+
staging.chmod(mode)
|
|
104
|
+
for relative, text in files:
|
|
105
|
+
target = staging.joinpath(*relative.split("/"))
|
|
106
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
target.write_text(text, encoding="utf-8")
|
|
108
|
+
check_destination(directory)
|
|
109
|
+
if directory.exists():
|
|
110
|
+
directory.rmdir()
|
|
111
|
+
os.replace(staging, directory)
|
|
112
|
+
except BaseException:
|
|
113
|
+
shutil.rmtree(staging, ignore_errors=True)
|
|
114
|
+
raise
|
{morphir_python-0.0.1/packages/morphir-tools → morphir_python-0.0.3}/src/morphir_tools/cli.py
RENAMED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Every IR operation goes through the `morphir` library: the shared format-version
|
|
4
4
|
reader, the v3 (3.0.x and 3.1.x) and draft v4 codecs, and `morphir.ir.migrate`.
|
|
5
|
-
|
|
5
|
+
``compile`` and ``generate`` go through the public `morphir_tools.python` API,
|
|
6
|
+
which loads the native compiler only when one of them runs. The CLI adds file
|
|
7
|
+
handling and reporting only.
|
|
6
8
|
|
|
7
9
|
Exit codes: 0 for success (including success with warnings), 1 when the library
|
|
8
10
|
reports a diagnostic, 2 for a usage error.
|
|
@@ -14,7 +16,7 @@ import os
|
|
|
14
16
|
import tempfile
|
|
15
17
|
from enum import Enum
|
|
16
18
|
from pathlib import Path
|
|
17
|
-
from typing import Annotated
|
|
19
|
+
from typing import TYPE_CHECKING, Annotated
|
|
18
20
|
|
|
19
21
|
import typer
|
|
20
22
|
|
|
@@ -30,6 +32,13 @@ from morphir.ir.v4 import distribution as v4_dist
|
|
|
30
32
|
from morphir.ir.v4.name import Name as Name4
|
|
31
33
|
from morphir.ir.v4.path import Path as Path4
|
|
32
34
|
|
|
35
|
+
from ._output import OutputError, publish_tree, umask, validate_artifact_paths
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from .python import CompilerDiagnostic
|
|
39
|
+
|
|
40
|
+
__all__ = ["OutputError", "app", "main", "validate_artifact_paths"]
|
|
41
|
+
|
|
33
42
|
app = typer.Typer(
|
|
34
43
|
name="morphir-python",
|
|
35
44
|
help="Command-line tools for Morphir IR.",
|
|
@@ -63,6 +72,9 @@ def _read(path: Path) -> str:
|
|
|
63
72
|
except OSError as error:
|
|
64
73
|
typer.echo(f"error: cannot read {path}: {error.strerror}", err=True)
|
|
65
74
|
raise typer.Exit(1) from None
|
|
75
|
+
except UnicodeDecodeError:
|
|
76
|
+
typer.echo(f"error: cannot read {path}: the file is not UTF-8", err=True)
|
|
77
|
+
raise typer.Exit(1) from None
|
|
66
78
|
|
|
67
79
|
|
|
68
80
|
def _write_atomically(target: Path, text: str) -> None:
|
|
@@ -71,6 +83,9 @@ def _write_atomically(target: Path, text: str) -> None:
|
|
|
71
83
|
prefix=f".{target.name}.", suffix=".tmp", dir=directory
|
|
72
84
|
)
|
|
73
85
|
try:
|
|
86
|
+
# mkstemp creates 0600; keep an existing file's mode, or use the umask.
|
|
87
|
+
mode = target.stat().st_mode & 0o7777 if target.exists() else 0o666 & ~umask()
|
|
88
|
+
os.chmod(fd, mode)
|
|
74
89
|
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
75
90
|
handle.write(text)
|
|
76
91
|
Path(temp_name).replace(target)
|
|
@@ -218,6 +233,165 @@ def convert(
|
|
|
218
233
|
typer.echo(f"wrote {output} (format {to.value})")
|
|
219
234
|
|
|
220
235
|
|
|
236
|
+
def _report_compiler(diagnostics: tuple[CompilerDiagnostic, ...]) -> None:
|
|
237
|
+
for diagnostic in diagnostics:
|
|
238
|
+
typer.echo(f"{diagnostic.severity.value}: {diagnostic.render()}", err=True)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _fail(message: str) -> typer.Exit:
|
|
242
|
+
typer.echo(f"error: {message}", err=True)
|
|
243
|
+
return typer.Exit(1)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _discover(source: Path) -> tuple[Path, list[Path]]:
|
|
247
|
+
"""Return the source root and its Python files, sorted, without following links."""
|
|
248
|
+
if source.is_dir() and not source.is_symlink():
|
|
249
|
+
files: list[Path] = []
|
|
250
|
+
for directory, names, filenames in os.walk(source, followlinks=False):
|
|
251
|
+
names[:] = sorted(
|
|
252
|
+
name
|
|
253
|
+
for name in names
|
|
254
|
+
if not name.startswith(".") and name != "__pycache__"
|
|
255
|
+
)
|
|
256
|
+
files.extend(
|
|
257
|
+
Path(directory) / name
|
|
258
|
+
for name in sorted(filenames)
|
|
259
|
+
if name.endswith(".py") and not (Path(directory) / name).is_symlink()
|
|
260
|
+
)
|
|
261
|
+
if not files:
|
|
262
|
+
raise _fail(f"no Python source files under {source}")
|
|
263
|
+
return source, files
|
|
264
|
+
return source.parent, [source]
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@app.command("compile")
|
|
268
|
+
def compile_command(
|
|
269
|
+
source: Annotated[
|
|
270
|
+
Path, typer.Argument(help="A Python file, or a directory of Python files.")
|
|
271
|
+
],
|
|
272
|
+
package: Annotated[
|
|
273
|
+
str, typer.Option(help="Morphir package name, such as acme/example.")
|
|
274
|
+
],
|
|
275
|
+
output: Annotated[
|
|
276
|
+
Path, typer.Option("--output", "-o", help="Where to write the IR document.")
|
|
277
|
+
],
|
|
278
|
+
ir_version: Annotated[
|
|
279
|
+
Target, typer.Option("--ir-version", help="The IR version to write.")
|
|
280
|
+
] = Target.V3,
|
|
281
|
+
expose: Annotated[
|
|
282
|
+
list[str] | None,
|
|
283
|
+
typer.Option(
|
|
284
|
+
"--expose",
|
|
285
|
+
help="Module to make public, such as acme/models. Repeat for more. "
|
|
286
|
+
"Without it every module is public.",
|
|
287
|
+
),
|
|
288
|
+
] = None,
|
|
289
|
+
) -> None:
|
|
290
|
+
"""Compile supported Python source to a versioned Morphir IR Library.
|
|
291
|
+
|
|
292
|
+
A directory is compiled as one package; a single file uses its parent
|
|
293
|
+
directory as the source root. Sources are parsed, never imported or run.
|
|
294
|
+
The output is written to a temporary file and renamed only on success, so a
|
|
295
|
+
failed compilation leaves an existing output file unchanged.
|
|
296
|
+
"""
|
|
297
|
+
from .python import ( # Loads the compiler on demand.
|
|
298
|
+
CompiledV3,
|
|
299
|
+
CompileFailure,
|
|
300
|
+
IrVersion,
|
|
301
|
+
NativeCompilerError,
|
|
302
|
+
NativeCompilerUnavailable,
|
|
303
|
+
SourceDocument,
|
|
304
|
+
compile_sources,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
root, files = _discover(source)
|
|
308
|
+
documents = [
|
|
309
|
+
SourceDocument(path.relative_to(root).as_posix(), _read(path)) for path in files
|
|
310
|
+
]
|
|
311
|
+
version = IrVersion.V3 if ir_version is Target.V3 else IrVersion.V4
|
|
312
|
+
try:
|
|
313
|
+
result = compile_sources(
|
|
314
|
+
documents,
|
|
315
|
+
package=package,
|
|
316
|
+
exposed_modules=expose,
|
|
317
|
+
ir_version=version,
|
|
318
|
+
)
|
|
319
|
+
except (ValueError, NativeCompilerUnavailable) as error:
|
|
320
|
+
raise _fail(str(error)) from None
|
|
321
|
+
except NativeCompilerError as error:
|
|
322
|
+
raise _fail(f"internal compiler error: {error}") from None
|
|
323
|
+
if isinstance(result, CompileFailure):
|
|
324
|
+
_report_compiler(result.diagnostics)
|
|
325
|
+
raise typer.Exit(1)
|
|
326
|
+
if isinstance(result, CompiledV3):
|
|
327
|
+
document = v3_codec.encode_versioned_distribution(result.distribution)
|
|
328
|
+
else:
|
|
329
|
+
document = v4_codec.encode_versioned_distribution(result.distribution)
|
|
330
|
+
try:
|
|
331
|
+
_write_atomically(output, fv.dump_json(document))
|
|
332
|
+
except OSError as error:
|
|
333
|
+
raise _fail(f"cannot write {output}: {error.strerror}") from None
|
|
334
|
+
_report_compiler(result.warnings)
|
|
335
|
+
typer.echo(f"wrote {output} (format {ir_version.value})")
|
|
336
|
+
if ir_version is Target.V4:
|
|
337
|
+
typer.echo(f"note: {v4_codec.DRAFT_NOTICE}")
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
@app.command("generate")
|
|
341
|
+
def generate_command(
|
|
342
|
+
file: Annotated[Path, typer.Argument(help="A versioned morphir-ir.json file.")],
|
|
343
|
+
output_dir: Annotated[
|
|
344
|
+
Path,
|
|
345
|
+
typer.Option(
|
|
346
|
+
"--output-dir", help="A new or empty directory for the Python modules."
|
|
347
|
+
),
|
|
348
|
+
],
|
|
349
|
+
) -> None:
|
|
350
|
+
"""Generate Python modules from a format 3.0 or draft v4 Library.
|
|
351
|
+
|
|
352
|
+
Module paths are preserved under the output directory. All files are written
|
|
353
|
+
to a staging directory first and published together, so a failure leaves no
|
|
354
|
+
partial output. Legacy formats 1 and 2 must be upgraded with ir convert first.
|
|
355
|
+
"""
|
|
356
|
+
from .python import ( # Loads the compiler on demand.
|
|
357
|
+
GenerationFailure,
|
|
358
|
+
NativeCompilerError,
|
|
359
|
+
NativeCompilerUnavailable,
|
|
360
|
+
generate_python,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
text = _read(file)
|
|
364
|
+
try:
|
|
365
|
+
version, _ = fv.read_document(text)
|
|
366
|
+
model: object = (
|
|
367
|
+
v4_codec.decode_versioned_distribution(text)
|
|
368
|
+
if _is_v4(version)
|
|
369
|
+
else v3_codec.decode_versioned_distribution(text)
|
|
370
|
+
)
|
|
371
|
+
except DiagnosticError as error:
|
|
372
|
+
_report(error.diagnostics)
|
|
373
|
+
raise typer.Exit(1) from None
|
|
374
|
+
try:
|
|
375
|
+
result = generate_python(model)
|
|
376
|
+
except NativeCompilerUnavailable as error:
|
|
377
|
+
raise _fail(str(error)) from None
|
|
378
|
+
except NativeCompilerError as error:
|
|
379
|
+
raise _fail(f"internal compiler error: {error}") from None
|
|
380
|
+
if isinstance(result, GenerationFailure):
|
|
381
|
+
_report_compiler(result.diagnostics)
|
|
382
|
+
raise typer.Exit(1)
|
|
383
|
+
files = [(artifact.path, artifact.text) for artifact in result.artifacts]
|
|
384
|
+
try:
|
|
385
|
+
validate_artifact_paths(path for path, _ in files)
|
|
386
|
+
publish_tree(output_dir, files)
|
|
387
|
+
except OutputError as error:
|
|
388
|
+
raise _fail(str(error)) from None
|
|
389
|
+
except OSError as error:
|
|
390
|
+
raise _fail(f"cannot write {output_dir}: {error.strerror}") from None
|
|
391
|
+
_report_compiler(result.warnings)
|
|
392
|
+
typer.echo(f"wrote {len(files)} modules to {output_dir}")
|
|
393
|
+
|
|
394
|
+
|
|
221
395
|
def main() -> None:
|
|
222
396
|
"""Run the ``morphir-python`` command line."""
|
|
223
397
|
app()
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
r"""Compile Python to Morphir IR and generate Python from Morphir IR.
|
|
2
|
+
|
|
3
|
+
This is the typed, in-process API behind ``morphir-python compile`` and
|
|
4
|
+
``morphir-python generate``. A native compiler does the work; callers need
|
|
5
|
+
neither Rust nor a separate Morphir executable, and the native compiler loads
|
|
6
|
+
only when one of these functions is first called.
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
>>> from morphir_tools.python import SourceDocument, compile_sources
|
|
10
|
+
>>> from morphir_tools.python import CompiledV3, generate_python
|
|
11
|
+
>>> source = SourceDocument(
|
|
12
|
+
... "rules.py",
|
|
13
|
+
... "def choose(flag: bool, a: int, b: int) -> int:\n"
|
|
14
|
+
... " return a if flag else b\n",
|
|
15
|
+
... )
|
|
16
|
+
>>> compiled = compile_sources([source], package="acme/example")
|
|
17
|
+
>>> isinstance(compiled, CompiledV3)
|
|
18
|
+
True
|
|
19
|
+
>>> generated = generate_python(compiled.distribution)
|
|
20
|
+
>>> [artifact.path for artifact in generated.artifacts]
|
|
21
|
+
['rules.py']
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from ._api import compile_sources, generate_python, native_compiler
|
|
25
|
+
from ._bridge import EXPECTED_BRIDGE_API, EXPECTED_NATIVE_VERSION
|
|
26
|
+
from ._types import (
|
|
27
|
+
CompiledV3,
|
|
28
|
+
CompiledV4,
|
|
29
|
+
CompileFailure,
|
|
30
|
+
CompilerDiagnostic,
|
|
31
|
+
CompileResult,
|
|
32
|
+
Generated,
|
|
33
|
+
GeneratedArtifact,
|
|
34
|
+
GenerateResult,
|
|
35
|
+
GenerationFailure,
|
|
36
|
+
IrVersion,
|
|
37
|
+
LibraryModel,
|
|
38
|
+
NativeCompilerError,
|
|
39
|
+
NativeCompilerInfo,
|
|
40
|
+
NativeCompilerUnavailable,
|
|
41
|
+
Severity,
|
|
42
|
+
SourceDocument,
|
|
43
|
+
SourceLocation,
|
|
44
|
+
SourcePosition,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"EXPECTED_BRIDGE_API",
|
|
49
|
+
"EXPECTED_NATIVE_VERSION",
|
|
50
|
+
"CompileFailure",
|
|
51
|
+
"CompileResult",
|
|
52
|
+
"CompiledV3",
|
|
53
|
+
"CompiledV4",
|
|
54
|
+
"CompilerDiagnostic",
|
|
55
|
+
"GenerateResult",
|
|
56
|
+
"Generated",
|
|
57
|
+
"GeneratedArtifact",
|
|
58
|
+
"GenerationFailure",
|
|
59
|
+
"IrVersion",
|
|
60
|
+
"LibraryModel",
|
|
61
|
+
"NativeCompilerError",
|
|
62
|
+
"NativeCompilerInfo",
|
|
63
|
+
"NativeCompilerUnavailable",
|
|
64
|
+
"Severity",
|
|
65
|
+
"SourceDocument",
|
|
66
|
+
"SourceLocation",
|
|
67
|
+
"SourcePosition",
|
|
68
|
+
"compile_sources",
|
|
69
|
+
"generate_python",
|
|
70
|
+
"native_compiler",
|
|
71
|
+
]
|