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.
Files changed (112) hide show
  1. {morphir_python-0.0.1 → morphir_python-0.0.3}/.gitignore +6 -0
  2. morphir_python-0.0.3/PKG-INFO +98 -0
  3. {morphir_python-0.0.1/packages/morphir-tools → morphir_python-0.0.3}/README.md +5 -4
  4. {morphir_python-0.0.1/packages/morphir → morphir_python-0.0.3}/pyproject.toml +18 -7
  5. {morphir_python-0.0.1/packages/morphir-tools → morphir_python-0.0.3}/src/morphir_tools/__init__.py +2 -2
  6. morphir_python-0.0.3/src/morphir_tools/_output.py +114 -0
  7. {morphir_python-0.0.1/packages/morphir-tools → morphir_python-0.0.3}/src/morphir_tools/cli.py +176 -2
  8. morphir_python-0.0.3/src/morphir_tools/python/__init__.py +71 -0
  9. morphir_python-0.0.3/src/morphir_tools/python/_api.py +215 -0
  10. morphir_python-0.0.3/src/morphir_tools/python/_bridge.py +84 -0
  11. morphir_python-0.0.3/src/morphir_tools/python/_types.py +201 -0
  12. morphir_python-0.0.3/src/morphir_tools/python/diagnostics.py +33 -0
  13. morphir_python-0.0.1/PKG-INFO +0 -269
  14. morphir_python-0.0.1/README.md +0 -246
  15. morphir_python-0.0.1/packages/morphir/LICENSE +0 -201
  16. morphir_python-0.0.1/packages/morphir/README.md +0 -387
  17. morphir_python-0.0.1/packages/morphir/src/morphir/__init__.py +0 -14
  18. morphir_python-0.0.1/packages/morphir/src/morphir/ir/__init__.py +0 -110
  19. morphir_python-0.0.1/packages/morphir/src/morphir/ir/access_controlled.py +0 -15
  20. morphir_python-0.0.1/packages/morphir/src/morphir/ir/decorations.py +0 -19
  21. morphir_python-0.0.1/packages/morphir/src/morphir/ir/diagnostics.py +0 -105
  22. morphir_python-0.0.1/packages/morphir/src/morphir/ir/distribution.py +0 -21
  23. morphir_python-0.0.1/packages/morphir/src/morphir/ir/document.py +0 -37
  24. morphir_python-0.0.1/packages/morphir/src/morphir/ir/documented.py +0 -11
  25. morphir_python-0.0.1/packages/morphir/src/morphir/ir/format_version.py +0 -483
  26. morphir_python-0.0.1/packages/morphir/src/morphir/ir/fqname.py +0 -9
  27. morphir_python-0.0.1/packages/morphir/src/morphir/ir/json.py +0 -11
  28. morphir_python-0.0.1/packages/morphir/src/morphir/ir/literal.py +0 -25
  29. morphir_python-0.0.1/packages/morphir/src/morphir/ir/meta.py +0 -11
  30. morphir_python-0.0.1/packages/morphir/src/morphir/ir/migrate.py +0 -1267
  31. morphir_python-0.0.1/packages/morphir/src/morphir/ir/module.py +0 -17
  32. morphir_python-0.0.1/packages/morphir/src/morphir/ir/name.py +0 -27
  33. morphir_python-0.0.1/packages/morphir/src/morphir/ir/package.py +0 -15
  34. morphir_python-0.0.1/packages/morphir/src/morphir/ir/path.py +0 -25
  35. morphir_python-0.0.1/packages/morphir/src/morphir/ir/qname.py +0 -9
  36. morphir_python-0.0.1/packages/morphir/src/morphir/ir/ref.py +0 -17
  37. morphir_python-0.0.1/packages/morphir/src/morphir/ir/type.py +0 -37
  38. morphir_python-0.0.1/packages/morphir/src/morphir/ir/type_constraints.py +0 -33
  39. morphir_python-0.0.1/packages/morphir/src/morphir/ir/type_def.py +0 -19
  40. morphir_python-0.0.1/packages/morphir/src/morphir/ir/type_spec.py +0 -25
  41. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v3/__init__.py +0 -64
  42. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v3/_legacy_value.py +0 -201
  43. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v3/access_controlled.py +0 -31
  44. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v3/codec.py +0 -1275
  45. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v3/codec_v1.py +0 -491
  46. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v3/codec_v2.py +0 -187
  47. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v3/distribution.py +0 -42
  48. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v3/documented.py +0 -13
  49. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v3/fqname.py +0 -30
  50. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v3/literal.py +0 -46
  51. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v3/module.py +0 -32
  52. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v3/name.py +0 -42
  53. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v3/package.py +0 -23
  54. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v3/path.py +0 -29
  55. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v3/qname.py +0 -16
  56. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v3/type.py +0 -126
  57. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v3/value.py +0 -235
  58. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/__init__.py +0 -160
  59. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/access_controlled.py +0 -17
  60. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/annotation.py +0 -75
  61. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/codec.py +0 -2161
  62. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/decorations.py +0 -88
  63. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/distribution.py +0 -56
  64. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/document.py +0 -69
  65. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/documented.py +0 -10
  66. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/fqname.py +0 -69
  67. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/incompleteness.py +0 -78
  68. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/json.py +0 -103
  69. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/literal.py +0 -51
  70. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/meta.py +0 -27
  71. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/module.py +0 -30
  72. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/name.py +0 -69
  73. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/package.py +0 -19
  74. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/path.py +0 -50
  75. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/qname.py +0 -34
  76. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/ref.py +0 -35
  77. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/type.py +0 -140
  78. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/type_constraints.py +0 -91
  79. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/type_def.py +0 -38
  80. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/type_spec.py +0 -64
  81. morphir_python-0.0.1/packages/morphir/src/morphir/ir/v4/value.py +0 -480
  82. morphir_python-0.0.1/packages/morphir/src/morphir/ir/value.py +0 -81
  83. morphir_python-0.0.1/packages/morphir/src/morphir/py.typed +0 -0
  84. morphir_python-0.0.1/packages/morphir/src/morphir/sdk/__init__.py +0 -110
  85. morphir_python-0.0.1/packages/morphir/src/morphir/sdk/_compare.py +0 -187
  86. morphir_python-0.0.1/packages/morphir/src/morphir/sdk/aggregate.py +0 -421
  87. morphir_python-0.0.1/packages/morphir/src/morphir/sdk/basics.py +0 -504
  88. morphir_python-0.0.1/packages/morphir/src/morphir/sdk/char.py +0 -110
  89. morphir_python-0.0.1/packages/morphir/src/morphir/sdk/decimal.py +0 -311
  90. morphir_python-0.0.1/packages/morphir/src/morphir/sdk/dict.py +0 -336
  91. morphir_python-0.0.1/packages/morphir/src/morphir/sdk/instant.py +0 -118
  92. morphir_python-0.0.1/packages/morphir/src/morphir/sdk/int.py +0 -86
  93. morphir_python-0.0.1/packages/morphir/src/morphir/sdk/key.py +0 -522
  94. morphir_python-0.0.1/packages/morphir/src/morphir/sdk/list.py +0 -356
  95. morphir_python-0.0.1/packages/morphir/src/morphir/sdk/local_date.py +0 -436
  96. morphir_python-0.0.1/packages/morphir/src/morphir/sdk/local_time.py +0 -267
  97. morphir_python-0.0.1/packages/morphir/src/morphir/sdk/maybe.py +0 -135
  98. morphir_python-0.0.1/packages/morphir/src/morphir/sdk/number.py +0 -237
  99. morphir_python-0.0.1/packages/morphir/src/morphir/sdk/regex.py +0 -287
  100. morphir_python-0.0.1/packages/morphir/src/morphir/sdk/result.py +0 -162
  101. morphir_python-0.0.1/packages/morphir/src/morphir/sdk/result_list.py +0 -169
  102. morphir_python-0.0.1/packages/morphir/src/morphir/sdk/rule.py +0 -103
  103. morphir_python-0.0.1/packages/morphir/src/morphir/sdk/set.py +0 -214
  104. morphir_python-0.0.1/packages/morphir/src/morphir/sdk/stateful_app.py +0 -49
  105. morphir_python-0.0.1/packages/morphir/src/morphir/sdk/string.py +0 -427
  106. morphir_python-0.0.1/packages/morphir/src/morphir/sdk/tuple.py +0 -43
  107. morphir_python-0.0.1/packages/morphir/src/morphir/sdk/uuid.py +0 -209
  108. morphir_python-0.0.1/packages/morphir-tools/LICENSE +0 -201
  109. morphir_python-0.0.1/pyproject.toml +0 -177
  110. morphir_python-0.0.1/uv.lock +0 -527
  111. {morphir_python-0.0.1 → morphir_python-0.0.3}/LICENSE +0 -0
  112. {morphir_python-0.0.1/packages/morphir-tools → morphir_python-0.0.3}/src/morphir_tools/py.typed +0 -0
@@ -73,3 +73,9 @@ yarn-error.log*
73
73
 
74
74
  # behave writes the pretty formatter to this file when its output is piped
75
75
  pretty.output
76
+
77
+ # Rust
78
+ rust/target/
79
+
80
+ # Extension bundles built by mise run extension:bundle
81
+ .morphir/
@@ -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 includes the `morphir` core library and SDK
25
- runtime alongside the CLI. It does not depend on a separate `morphir` PyPI
26
- package. Python imports remain `morphir` and `morphir_tools`.
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
- - Core library and SDK included
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.1"
4
- description = "Python port of Morphir - functional domain modeling for the enterprise"
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", "domain-modeling", "functional", "dsl", "type-safe", "ir"]
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/morphir"]
47
+ packages = ["src/morphir_tools"]
@@ -6,8 +6,8 @@ Morphir domain models, including code generation and development tools.
6
6
  Example:
7
7
  >>> import morphir_tools
8
8
  >>> morphir_tools.__version__
9
- '0.0.1'
9
+ '0.0.3'
10
10
  """
11
11
 
12
- __version__ = "0.0.1"
12
+ __version__ = "0.0.3"
13
13
  __all__ = ["__version__"]
@@ -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
@@ -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
- The CLI adds file handling and reporting only.
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
+ ]