morphir-python 0.0.1__py3-none-any.whl → 0.0.3__py3-none-any.whl

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 (106) hide show
  1. morphir_python-0.0.3.dist-info/METADATA +98 -0
  2. morphir_python-0.0.3.dist-info/RECORD +14 -0
  3. morphir_tools/__init__.py +2 -2
  4. morphir_tools/_output.py +114 -0
  5. morphir_tools/cli.py +176 -2
  6. morphir_tools/python/__init__.py +71 -0
  7. morphir_tools/python/_api.py +215 -0
  8. morphir_tools/python/_bridge.py +84 -0
  9. morphir_tools/python/_types.py +201 -0
  10. morphir_tools/python/diagnostics.py +33 -0
  11. morphir/__init__.py +0 -14
  12. morphir/ir/__init__.py +0 -110
  13. morphir/ir/access_controlled.py +0 -15
  14. morphir/ir/decorations.py +0 -19
  15. morphir/ir/diagnostics.py +0 -105
  16. morphir/ir/distribution.py +0 -21
  17. morphir/ir/document.py +0 -37
  18. morphir/ir/documented.py +0 -11
  19. morphir/ir/format_version.py +0 -483
  20. morphir/ir/fqname.py +0 -9
  21. morphir/ir/json.py +0 -11
  22. morphir/ir/literal.py +0 -25
  23. morphir/ir/meta.py +0 -11
  24. morphir/ir/migrate.py +0 -1267
  25. morphir/ir/module.py +0 -17
  26. morphir/ir/name.py +0 -27
  27. morphir/ir/package.py +0 -15
  28. morphir/ir/path.py +0 -25
  29. morphir/ir/qname.py +0 -9
  30. morphir/ir/ref.py +0 -17
  31. morphir/ir/type.py +0 -37
  32. morphir/ir/type_constraints.py +0 -33
  33. morphir/ir/type_def.py +0 -19
  34. morphir/ir/type_spec.py +0 -25
  35. morphir/ir/v3/__init__.py +0 -64
  36. morphir/ir/v3/_legacy_value.py +0 -201
  37. morphir/ir/v3/access_controlled.py +0 -31
  38. morphir/ir/v3/codec.py +0 -1275
  39. morphir/ir/v3/codec_v1.py +0 -491
  40. morphir/ir/v3/codec_v2.py +0 -187
  41. morphir/ir/v3/distribution.py +0 -42
  42. morphir/ir/v3/documented.py +0 -13
  43. morphir/ir/v3/fqname.py +0 -30
  44. morphir/ir/v3/literal.py +0 -46
  45. morphir/ir/v3/module.py +0 -32
  46. morphir/ir/v3/name.py +0 -42
  47. morphir/ir/v3/package.py +0 -23
  48. morphir/ir/v3/path.py +0 -29
  49. morphir/ir/v3/qname.py +0 -16
  50. morphir/ir/v3/type.py +0 -126
  51. morphir/ir/v3/value.py +0 -235
  52. morphir/ir/v4/__init__.py +0 -160
  53. morphir/ir/v4/access_controlled.py +0 -17
  54. morphir/ir/v4/annotation.py +0 -75
  55. morphir/ir/v4/codec.py +0 -2161
  56. morphir/ir/v4/decorations.py +0 -88
  57. morphir/ir/v4/distribution.py +0 -56
  58. morphir/ir/v4/document.py +0 -69
  59. morphir/ir/v4/documented.py +0 -10
  60. morphir/ir/v4/fqname.py +0 -69
  61. morphir/ir/v4/incompleteness.py +0 -78
  62. morphir/ir/v4/json.py +0 -103
  63. morphir/ir/v4/literal.py +0 -51
  64. morphir/ir/v4/meta.py +0 -27
  65. morphir/ir/v4/module.py +0 -30
  66. morphir/ir/v4/name.py +0 -69
  67. morphir/ir/v4/package.py +0 -19
  68. morphir/ir/v4/path.py +0 -50
  69. morphir/ir/v4/qname.py +0 -34
  70. morphir/ir/v4/ref.py +0 -35
  71. morphir/ir/v4/type.py +0 -140
  72. morphir/ir/v4/type_constraints.py +0 -91
  73. morphir/ir/v4/type_def.py +0 -38
  74. morphir/ir/v4/type_spec.py +0 -64
  75. morphir/ir/v4/value.py +0 -480
  76. morphir/ir/value.py +0 -81
  77. morphir/py.typed +0 -0
  78. morphir/sdk/__init__.py +0 -110
  79. morphir/sdk/_compare.py +0 -187
  80. morphir/sdk/aggregate.py +0 -421
  81. morphir/sdk/basics.py +0 -504
  82. morphir/sdk/char.py +0 -110
  83. morphir/sdk/decimal.py +0 -311
  84. morphir/sdk/dict.py +0 -336
  85. morphir/sdk/instant.py +0 -118
  86. morphir/sdk/int.py +0 -86
  87. morphir/sdk/key.py +0 -522
  88. morphir/sdk/list.py +0 -356
  89. morphir/sdk/local_date.py +0 -436
  90. morphir/sdk/local_time.py +0 -267
  91. morphir/sdk/maybe.py +0 -135
  92. morphir/sdk/number.py +0 -237
  93. morphir/sdk/regex.py +0 -287
  94. morphir/sdk/result.py +0 -162
  95. morphir/sdk/result_list.py +0 -169
  96. morphir/sdk/rule.py +0 -103
  97. morphir/sdk/set.py +0 -214
  98. morphir/sdk/stateful_app.py +0 -49
  99. morphir/sdk/string.py +0 -427
  100. morphir/sdk/tuple.py +0 -43
  101. morphir/sdk/uuid.py +0 -209
  102. morphir_python-0.0.1.dist-info/METADATA +0 -269
  103. morphir_python-0.0.1.dist-info/RECORD +0 -99
  104. {morphir_python-0.0.1.dist-info → morphir_python-0.0.3.dist-info}/WHEEL +0 -0
  105. {morphir_python-0.0.1.dist-info → morphir_python-0.0.3.dist-info}/entry_points.txt +0 -0
  106. {morphir_python-0.0.1.dist-info → morphir_python-0.0.3.dist-info}/licenses/LICENSE +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
@@ -0,0 +1,14 @@
1
+ morphir_tools/__init__.py,sha256=s0PyppW1YjLdjPHMZbu4KNEPRQYhAj7hHresVUTSyY0,348
2
+ morphir_tools/_output.py,sha256=6mWIWg0vjM8pxcnRdegbGix_yqwClpR0gX2cqrdOcKo,4004
3
+ morphir_tools/cli.py,sha256=DZiWPlJp8CNj4zLK7fGOrPy7LuuznRFo-BngrK85fjw,14356
4
+ morphir_tools/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ morphir_tools/python/__init__.py,sha256=5ufqFJLyatrEDxtS2zaL1j4yeKp_6IehQFIpL14wTFg,1995
6
+ morphir_tools/python/_api.py,sha256=Wfm8MBGryA1QCB97FFPiQB3bYpkea9emRrZrngc4NBg,7716
7
+ morphir_tools/python/_bridge.py,sha256=VN86T8tlgx7maoEwXKAkxR4nD7tbs-Fux_rUmYjQ4zo,2822
8
+ morphir_tools/python/_types.py,sha256=9uQW1UeqRmp4tdJIdpDF6iLUDA2BRoV6y-5FtzjgIcs,5330
9
+ morphir_tools/python/diagnostics.py,sha256=C3EOuOFwYL-K992L_kKuMlBwTwi7muP64mOCak8EG3c,1292
10
+ morphir_python-0.0.3.dist-info/METADATA,sha256=d9jXyZt_jaLG95I5vGiB1gi6aP-SHJL6myEpBKFoTiw,3614
11
+ morphir_python-0.0.3.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
12
+ morphir_python-0.0.3.dist-info/entry_points.txt,sha256=ig7-ByPyqCN0S13AuOguSP9qT1npyySP6hVL32naLxA,58
13
+ morphir_python-0.0.3.dist-info/licenses/LICENSE,sha256=tAkwu8-AdEyGxGoSvJ2gVmQdcicWw3j1ZZueVV74M-E,11357
14
+ morphir_python-0.0.3.dist-info/RECORD,,
morphir_tools/__init__.py CHANGED
@@ -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
morphir_tools/cli.py CHANGED
@@ -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
+ ]