morphir-python 0.0.1__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.
- morphir/__init__.py +14 -0
- morphir/ir/__init__.py +110 -0
- morphir/ir/access_controlled.py +15 -0
- morphir/ir/decorations.py +19 -0
- morphir/ir/diagnostics.py +105 -0
- morphir/ir/distribution.py +21 -0
- morphir/ir/document.py +37 -0
- morphir/ir/documented.py +11 -0
- morphir/ir/format_version.py +483 -0
- morphir/ir/fqname.py +9 -0
- morphir/ir/json.py +11 -0
- morphir/ir/literal.py +25 -0
- morphir/ir/meta.py +11 -0
- morphir/ir/migrate.py +1267 -0
- morphir/ir/module.py +17 -0
- morphir/ir/name.py +27 -0
- morphir/ir/package.py +15 -0
- morphir/ir/path.py +25 -0
- morphir/ir/qname.py +9 -0
- morphir/ir/ref.py +17 -0
- morphir/ir/type.py +37 -0
- morphir/ir/type_constraints.py +33 -0
- morphir/ir/type_def.py +19 -0
- morphir/ir/type_spec.py +25 -0
- morphir/ir/v3/__init__.py +64 -0
- morphir/ir/v3/_legacy_value.py +201 -0
- morphir/ir/v3/access_controlled.py +31 -0
- morphir/ir/v3/codec.py +1275 -0
- morphir/ir/v3/codec_v1.py +491 -0
- morphir/ir/v3/codec_v2.py +187 -0
- morphir/ir/v3/distribution.py +42 -0
- morphir/ir/v3/documented.py +13 -0
- morphir/ir/v3/fqname.py +30 -0
- morphir/ir/v3/literal.py +46 -0
- morphir/ir/v3/module.py +32 -0
- morphir/ir/v3/name.py +42 -0
- morphir/ir/v3/package.py +23 -0
- morphir/ir/v3/path.py +29 -0
- morphir/ir/v3/qname.py +16 -0
- morphir/ir/v3/type.py +126 -0
- morphir/ir/v3/value.py +235 -0
- morphir/ir/v4/__init__.py +160 -0
- morphir/ir/v4/access_controlled.py +17 -0
- morphir/ir/v4/annotation.py +75 -0
- morphir/ir/v4/codec.py +2161 -0
- morphir/ir/v4/decorations.py +88 -0
- morphir/ir/v4/distribution.py +56 -0
- morphir/ir/v4/document.py +69 -0
- morphir/ir/v4/documented.py +10 -0
- morphir/ir/v4/fqname.py +69 -0
- morphir/ir/v4/incompleteness.py +78 -0
- morphir/ir/v4/json.py +103 -0
- morphir/ir/v4/literal.py +51 -0
- morphir/ir/v4/meta.py +27 -0
- morphir/ir/v4/module.py +30 -0
- morphir/ir/v4/name.py +69 -0
- morphir/ir/v4/package.py +19 -0
- morphir/ir/v4/path.py +50 -0
- morphir/ir/v4/qname.py +34 -0
- morphir/ir/v4/ref.py +35 -0
- morphir/ir/v4/type.py +140 -0
- morphir/ir/v4/type_constraints.py +91 -0
- morphir/ir/v4/type_def.py +38 -0
- morphir/ir/v4/type_spec.py +64 -0
- morphir/ir/v4/value.py +480 -0
- morphir/ir/value.py +81 -0
- morphir/py.typed +0 -0
- morphir/sdk/__init__.py +110 -0
- morphir/sdk/_compare.py +187 -0
- morphir/sdk/aggregate.py +421 -0
- morphir/sdk/basics.py +504 -0
- morphir/sdk/char.py +110 -0
- morphir/sdk/decimal.py +311 -0
- morphir/sdk/dict.py +336 -0
- morphir/sdk/instant.py +118 -0
- morphir/sdk/int.py +86 -0
- morphir/sdk/key.py +522 -0
- morphir/sdk/list.py +356 -0
- morphir/sdk/local_date.py +436 -0
- morphir/sdk/local_time.py +267 -0
- morphir/sdk/maybe.py +135 -0
- morphir/sdk/number.py +237 -0
- morphir/sdk/regex.py +287 -0
- morphir/sdk/result.py +162 -0
- morphir/sdk/result_list.py +169 -0
- morphir/sdk/rule.py +103 -0
- morphir/sdk/set.py +214 -0
- morphir/sdk/stateful_app.py +49 -0
- morphir/sdk/string.py +427 -0
- morphir/sdk/tuple.py +43 -0
- morphir/sdk/uuid.py +209 -0
- morphir_python-0.0.1.dist-info/METADATA +269 -0
- morphir_python-0.0.1.dist-info/RECORD +99 -0
- morphir_python-0.0.1.dist-info/WHEEL +4 -0
- morphir_python-0.0.1.dist-info/entry_points.txt +2 -0
- morphir_python-0.0.1.dist-info/licenses/LICENSE +201 -0
- morphir_tools/__init__.py +13 -0
- morphir_tools/cli.py +223 -0
- morphir_tools/py.typed +0 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass(frozen=True)
|
|
6
|
+
class SchemaRef:
|
|
7
|
+
display_name: str
|
|
8
|
+
local_path: str
|
|
9
|
+
entry_point: str # FQName as string for simplicity in meta structures
|
|
10
|
+
description: str | None = None
|
|
11
|
+
remote_ref: str | None = None
|
|
12
|
+
cached_at: str | None = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class DecorationFormat:
|
|
17
|
+
format_version: str
|
|
18
|
+
schema_registry: dict[str, SchemaRef] = field(default_factory=dict)
|
|
19
|
+
layers: list[str] = field(default_factory=list)
|
|
20
|
+
layer_priority: dict[str, int] = field(default_factory=dict)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class LayerManifest:
|
|
25
|
+
format_version: str
|
|
26
|
+
layer: str
|
|
27
|
+
display_name: str
|
|
28
|
+
priority: int
|
|
29
|
+
created_at: str
|
|
30
|
+
updated_at: str
|
|
31
|
+
description: str | None = None
|
|
32
|
+
decoration_types: list[str] = field(default_factory=list)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class DecorationValuesFile:
|
|
37
|
+
format_version: str
|
|
38
|
+
decoration_type: str
|
|
39
|
+
layer: str
|
|
40
|
+
values: dict[str, Any] = field(default_factory=dict) # Key is FQName string
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def deep_merge(base: Any, override: Any) -> Any:
|
|
44
|
+
"""Deep merge two values.
|
|
45
|
+
|
|
46
|
+
- If both are dicts, merge recursively.
|
|
47
|
+
- If both are lists, concatenate (override extends base).
|
|
48
|
+
- Otherwise, override wins.
|
|
49
|
+
"""
|
|
50
|
+
if isinstance(base, dict) and isinstance(override, dict):
|
|
51
|
+
merged = base.copy()
|
|
52
|
+
for k, v in override.items():
|
|
53
|
+
if k in merged:
|
|
54
|
+
merged[k] = deep_merge(merged[k], v)
|
|
55
|
+
else:
|
|
56
|
+
merged[k] = v
|
|
57
|
+
return merged
|
|
58
|
+
elif isinstance(base, list) and isinstance(override, list):
|
|
59
|
+
return base + override
|
|
60
|
+
else:
|
|
61
|
+
return override
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def merge_decoration_values(layers: list[tuple[int, dict[str, Any]]]) -> dict[str, Any]:
|
|
65
|
+
"""Merge decoration values from multiple layers based on priority.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
layers: List of (priority, values_dict) tuples.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Merged dictionary of decoration values.
|
|
72
|
+
"""
|
|
73
|
+
# Sort by priority (ascending) -> processed in order, so higher priority overrides later
|
|
74
|
+
# Wait, simple override logic usually means last one wins.
|
|
75
|
+
# If priority 0 is base, and 100 is override, then 0 should be processed first, then 100 merged on top.
|
|
76
|
+
# So ascending sort is correct for standard "last write wins" merge logic.
|
|
77
|
+
sorted_layers = sorted(layers, key=lambda x: x[0])
|
|
78
|
+
|
|
79
|
+
merged: dict[str, Any] = {}
|
|
80
|
+
|
|
81
|
+
for _, values in sorted_layers:
|
|
82
|
+
for key, value in values.items():
|
|
83
|
+
if key in merged:
|
|
84
|
+
merged[key] = deep_merge(merged[key], value)
|
|
85
|
+
else:
|
|
86
|
+
merged[key] = value
|
|
87
|
+
|
|
88
|
+
return merged
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Union
|
|
4
|
+
|
|
5
|
+
from .documented import Documented
|
|
6
|
+
from .fqname import FQName
|
|
7
|
+
from .name import Name
|
|
8
|
+
from .package import Definition as PackageDefinition
|
|
9
|
+
from .package import Specification as PackageSpecification
|
|
10
|
+
from .path import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class PackageInfo:
|
|
15
|
+
name: Path
|
|
16
|
+
version: str | None = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class LibraryDistribution:
|
|
21
|
+
package: PackageInfo
|
|
22
|
+
definition: PackageDefinition
|
|
23
|
+
dependencies: dict[Path, PackageSpecification] = field(default_factory=dict)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class SpecsDistribution:
|
|
28
|
+
package: PackageInfo
|
|
29
|
+
specification: PackageSpecification
|
|
30
|
+
dependencies: dict[Path, PackageSpecification] = field(default_factory=dict)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class EntryPointKind(Enum):
|
|
34
|
+
Main = "Main"
|
|
35
|
+
Command = "Command"
|
|
36
|
+
Handler = "Handler"
|
|
37
|
+
Job = "Job"
|
|
38
|
+
Policy = "Policy"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class EntryPoint:
|
|
43
|
+
target: FQName
|
|
44
|
+
kind: EntryPointKind
|
|
45
|
+
doc: Documented[str] | None = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class ApplicationDistribution:
|
|
50
|
+
package: PackageInfo
|
|
51
|
+
definition: PackageDefinition
|
|
52
|
+
dependencies: dict[Path, PackageDefinition] = field(default_factory=dict)
|
|
53
|
+
entry_points: dict[Name, EntryPoint] = field(default_factory=dict)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
Distribution = Union[LibraryDistribution, SpecsDistribution, ApplicationDistribution]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Union
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass(frozen=True)
|
|
6
|
+
class DocNull:
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class DocBool:
|
|
12
|
+
value: bool
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class DocInt:
|
|
17
|
+
value: int
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class DocFloat:
|
|
22
|
+
value: float
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class DocString:
|
|
27
|
+
value: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Forward references for recursive types
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class DocArray:
|
|
33
|
+
elements: list[Document]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class DocObject:
|
|
38
|
+
fields: dict[str, Document]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
Document = Union[DocNull, DocBool, DocInt, DocFloat, DocString, DocArray, DocObject]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def null() -> Document:
|
|
45
|
+
return DocNull()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def bool_(value: bool) -> Document:
|
|
49
|
+
return DocBool(value)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def int_(value: int) -> Document:
|
|
53
|
+
return DocInt(value)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def float_(value: float) -> Document:
|
|
57
|
+
return DocFloat(value)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def string(value: str) -> Document:
|
|
61
|
+
return DocString(value)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def array(elements: list[Document]) -> Document:
|
|
65
|
+
return DocArray(elements)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def object_(fields: dict[str, Document]) -> Document:
|
|
69
|
+
return DocObject(fields)
|
morphir/ir/v4/fqname.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from . import name, path
|
|
4
|
+
from .name import Name
|
|
5
|
+
from .path import Path
|
|
6
|
+
from .qname import QName
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class FQName:
|
|
11
|
+
package_path: Path
|
|
12
|
+
module_path: Path
|
|
13
|
+
local_name: Name
|
|
14
|
+
|
|
15
|
+
@staticmethod
|
|
16
|
+
def from_qname(package_path: Path, qn: QName) -> FQName:
|
|
17
|
+
return FQName(package_path, qn.module_path, qn.local_name)
|
|
18
|
+
|
|
19
|
+
@staticmethod
|
|
20
|
+
def from_tuple(t: tuple[Path, Path, Name]) -> FQName:
|
|
21
|
+
return FQName(t[0], t[1], t[2])
|
|
22
|
+
|
|
23
|
+
def to_tuple(self) -> tuple[Path, Path, Name]:
|
|
24
|
+
return (self.package_path, self.module_path, self.local_name)
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def fqn(package_str: str, module_str: str, local_str: str) -> FQName:
|
|
28
|
+
return FQName(
|
|
29
|
+
path.from_string(package_str),
|
|
30
|
+
path.from_string(module_str),
|
|
31
|
+
name.from_string(local_str),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def from_string(s: str) -> FQName:
|
|
36
|
+
# Parse canonical format: PackagePath:ModulePath#LocalName
|
|
37
|
+
import re
|
|
38
|
+
|
|
39
|
+
match = re.search(r"^([^:]+):([^#]+)#(.+)$", s)
|
|
40
|
+
if match:
|
|
41
|
+
pkg_str, mod_str, local_str = match.groups()
|
|
42
|
+
return FQName(
|
|
43
|
+
path.from_string(pkg_str),
|
|
44
|
+
path.from_string(mod_str),
|
|
45
|
+
name.from_string(local_str),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Legacy/Fallback: Package.Module.Local or Package:Module:Local
|
|
49
|
+
# Attempt minimal parsing if canonical fails
|
|
50
|
+
# Assuming last part after separator is local name
|
|
51
|
+
if "#" not in s:
|
|
52
|
+
# if no hash, maybe using colon?
|
|
53
|
+
parts = s.split(":")
|
|
54
|
+
if len(parts) == 3:
|
|
55
|
+
return FQName(
|
|
56
|
+
path.from_string(parts[0]),
|
|
57
|
+
path.from_string(parts[1]),
|
|
58
|
+
name.from_string(parts[2]),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Return empty/default if totally unparseable
|
|
62
|
+
return FQName(path.from_string(""), path.from_string(""), name.from_string(s))
|
|
63
|
+
|
|
64
|
+
def to_string(self) -> str:
|
|
65
|
+
# Canonical: PackagePath:ModulePath#LocalName
|
|
66
|
+
package_str = path.to_string(self.package_path) # defaults to /
|
|
67
|
+
module_str = path.to_string(self.module_path)
|
|
68
|
+
local_str = name.to_kebab_case(self.local_name)
|
|
69
|
+
return f"{package_str}:{module_str}#{local_str}"
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Incompleteness: why a type or value definition is not complete yet.
|
|
2
|
+
|
|
3
|
+
Follows the finos/morphir v4 schema (``Incompleteness``, ``HoleReason``) at the
|
|
4
|
+
pinned commit and the morphir-rust v4 model. A hole names its reason; a draft is
|
|
5
|
+
work in progress and names none.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Union
|
|
12
|
+
|
|
13
|
+
from .fqname import FQName
|
|
14
|
+
from .type import Type
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class UnresolvedReference:
|
|
19
|
+
"""A reference that could not be resolved.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
target: The name that did not resolve.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
target: FQName
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class DeletedDuringRefactor:
|
|
30
|
+
"""A reference removed by a refactoring.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
tx_id: The transaction id of the refactoring (JSON member ``tx-id``).
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
tx_id: str
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class TypeMismatch:
|
|
41
|
+
"""Type checking failed.
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
expected: A description of the expected type.
|
|
45
|
+
found: A description of the type found.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
expected: str
|
|
49
|
+
found: str
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
HoleReason = Union[UnresolvedReference, DeletedDuringRefactor, TypeMismatch]
|
|
53
|
+
"""Why a hole exists. ``Draft`` is an incompleteness, not a hole reason."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class HoleIncompleteness:
|
|
58
|
+
"""A definition with a hole.
|
|
59
|
+
|
|
60
|
+
Attributes:
|
|
61
|
+
reason: Why the hole exists.
|
|
62
|
+
partial_body: The type the author had written when the definition stopped
|
|
63
|
+
being complete, if any. The schema gives this member the type
|
|
64
|
+
``Type``; an incomplete value keeps its partial expression on
|
|
65
|
+
`IncompleteBody.partial_body` instead.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
reason: HoleReason
|
|
69
|
+
partial_body: Type | None = None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
class DraftIncompleteness:
|
|
74
|
+
"""A definition the author marked as work in progress."""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
Incompleteness = Union[HoleIncompleteness, DraftIncompleteness]
|
|
78
|
+
"""Why a definition is incomplete: a hole or a draft."""
|
morphir/ir/v4/json.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from dataclasses import fields, is_dataclass
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from .decorations import (
|
|
5
|
+
DecorationFormat,
|
|
6
|
+
DecorationValuesFile,
|
|
7
|
+
LayerManifest,
|
|
8
|
+
SchemaRef,
|
|
9
|
+
)
|
|
10
|
+
from .distribution import PackageInfo
|
|
11
|
+
from .document import DocArray, DocBool, DocFloat, DocInt, DocNull, DocObject, DocString
|
|
12
|
+
from .fqname import FQName
|
|
13
|
+
from .literal import (
|
|
14
|
+
BoolLiteral,
|
|
15
|
+
CharLiteral,
|
|
16
|
+
DecimalLiteral,
|
|
17
|
+
DocumentLiteral,
|
|
18
|
+
FloatLiteral,
|
|
19
|
+
IntegerLiteral,
|
|
20
|
+
StringLiteral,
|
|
21
|
+
)
|
|
22
|
+
from .name import Name
|
|
23
|
+
from .name import to_string as name_to_string
|
|
24
|
+
from .path import Path
|
|
25
|
+
from .path import to_string as path_to_string
|
|
26
|
+
|
|
27
|
+
# Placeholder for full implementation.
|
|
28
|
+
# This will eventually contain robust encoders/decoders for all IR types.
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class MorphirJSONEncoder:
|
|
32
|
+
def encode(self, obj: Any) -> Any:
|
|
33
|
+
if isinstance(obj, Name):
|
|
34
|
+
return name_to_string(obj)
|
|
35
|
+
if isinstance(obj, Path):
|
|
36
|
+
return path_to_string(obj)
|
|
37
|
+
if isinstance(obj, FQName):
|
|
38
|
+
return obj.to_string()
|
|
39
|
+
if isinstance(obj, PackageInfo):
|
|
40
|
+
return {"name": path_to_string(obj.name), "version": obj.version}
|
|
41
|
+
|
|
42
|
+
# Decorations Encoding (Basic Dataclass Support handles these mostly, but explicit checks help)
|
|
43
|
+
if is_dataclass(obj) and isinstance(
|
|
44
|
+
obj, (DecorationFormat, LayerManifest, DecorationValuesFile, SchemaRef)
|
|
45
|
+
):
|
|
46
|
+
# Use standard dataclass conversion but recursively encode values
|
|
47
|
+
return {f.name: self.encode(getattr(obj, f.name)) for f in fields(obj)}
|
|
48
|
+
|
|
49
|
+
# Document Encoding
|
|
50
|
+
if isinstance(obj, DocNull):
|
|
51
|
+
return {"DocNull": {}}
|
|
52
|
+
if isinstance(obj, DocBool):
|
|
53
|
+
return {"DocBool": obj.value}
|
|
54
|
+
if isinstance(obj, DocInt):
|
|
55
|
+
return {"DocInt": obj.value}
|
|
56
|
+
if isinstance(obj, DocFloat):
|
|
57
|
+
return {"DocFloat": obj.value}
|
|
58
|
+
if isinstance(obj, DocString):
|
|
59
|
+
return {"DocString": obj.value}
|
|
60
|
+
if isinstance(obj, DocArray):
|
|
61
|
+
return {"DocArray": [self.encode(elem) for elem in obj.elements]}
|
|
62
|
+
if isinstance(obj, DocObject):
|
|
63
|
+
return {"DocObject": {k: self.encode(v) for k, v in obj.fields.items()}}
|
|
64
|
+
|
|
65
|
+
# Literal Encoding
|
|
66
|
+
if isinstance(
|
|
67
|
+
obj,
|
|
68
|
+
(
|
|
69
|
+
BoolLiteral,
|
|
70
|
+
CharLiteral,
|
|
71
|
+
StringLiteral,
|
|
72
|
+
IntegerLiteral,
|
|
73
|
+
FloatLiteral,
|
|
74
|
+
DecimalLiteral,
|
|
75
|
+
),
|
|
76
|
+
):
|
|
77
|
+
# { "IntegerLiteral": { "value": 42 } }
|
|
78
|
+
type_name = type(obj).__name__
|
|
79
|
+
return {
|
|
80
|
+
type_name: {
|
|
81
|
+
"value": obj.value
|
|
82
|
+
if not isinstance(obj, DecimalLiteral)
|
|
83
|
+
else str(obj.value)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if isinstance(obj, DocumentLiteral):
|
|
87
|
+
# DocumentLiteral wraps a Document
|
|
88
|
+
return {"DocumentLiteral": {"value": self.encode(obj.value)}}
|
|
89
|
+
if is_dataclass(obj):
|
|
90
|
+
# Generic fallback for simple dataclasses
|
|
91
|
+
# Real implementation needs to handle tagged unions (Value, Type, Pattern) specifically
|
|
92
|
+
return {f.name: self.encode(getattr(obj, f.name)) for f in fields(obj)}
|
|
93
|
+
|
|
94
|
+
if isinstance(obj, list):
|
|
95
|
+
return [self.encode(item) for item in obj]
|
|
96
|
+
if isinstance(obj, dict):
|
|
97
|
+
return {k: self.encode(v) for k, v in obj.items()}
|
|
98
|
+
|
|
99
|
+
return obj
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def encode(obj: Any) -> Any:
|
|
103
|
+
return MorphirJSONEncoder().encode(obj)
|
morphir/ir/v4/literal.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
from typing import Union
|
|
4
|
+
|
|
5
|
+
from .document import Document
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class BoolLiteral:
|
|
10
|
+
value: bool
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class CharLiteral:
|
|
15
|
+
value: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class StringLiteral:
|
|
20
|
+
value: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class IntegerLiteral:
|
|
25
|
+
value: int
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class FloatLiteral:
|
|
30
|
+
value: float
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class DecimalLiteral:
|
|
35
|
+
value: Decimal
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class DocumentLiteral:
|
|
40
|
+
value: Document
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
Literal = Union[
|
|
44
|
+
BoolLiteral,
|
|
45
|
+
CharLiteral,
|
|
46
|
+
StringLiteral,
|
|
47
|
+
IntegerLiteral,
|
|
48
|
+
FloatLiteral,
|
|
49
|
+
DecimalLiteral,
|
|
50
|
+
DocumentLiteral,
|
|
51
|
+
]
|
morphir/ir/v4/meta.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass(frozen=True)
|
|
6
|
+
class SourceRange:
|
|
7
|
+
start: tuple[int, int]
|
|
8
|
+
end: tuple[int, int]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class FileMeta:
|
|
13
|
+
# Provenance
|
|
14
|
+
source: str | None = None
|
|
15
|
+
source_range: SourceRange | None = None
|
|
16
|
+
compiler: str | None = None
|
|
17
|
+
generated: str | None = None # ISO 8601
|
|
18
|
+
checksum: str | None = None
|
|
19
|
+
|
|
20
|
+
# Tooling
|
|
21
|
+
edited_by: str | None = None
|
|
22
|
+
edited_at: str | None = None # ISO 8601
|
|
23
|
+
locked: bool | None = None
|
|
24
|
+
is_generated: bool | None = None
|
|
25
|
+
|
|
26
|
+
# Extensions
|
|
27
|
+
extensions: dict[str, Any] = field(default_factory=dict)
|
morphir/ir/v4/module.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
|
|
3
|
+
from . import type_def, type_spec, value
|
|
4
|
+
from .access_controlled import AccessControlled
|
|
5
|
+
from .annotation import Annotation
|
|
6
|
+
from .documented import Documented
|
|
7
|
+
from .name import Name
|
|
8
|
+
from .path import Path
|
|
9
|
+
|
|
10
|
+
ModuleName = Path
|
|
11
|
+
QualifiedModuleName = tuple[Path, Path]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class Specification:
|
|
16
|
+
types: dict[Name, Documented[type_spec.Specification]] = field(default_factory=dict)
|
|
17
|
+
values: dict[Name, Documented[value.Specification]] = field(default_factory=dict)
|
|
18
|
+
doc: str | None = None
|
|
19
|
+
annotations: list[Annotation] = field(default_factory=list)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class Definition:
|
|
24
|
+
types: dict[Name, AccessControlled[Documented[type_def.Definition]]] = field(
|
|
25
|
+
default_factory=dict
|
|
26
|
+
)
|
|
27
|
+
values: dict[Name, AccessControlled[Documented[value.ValueDefinitionBody]]] = field(
|
|
28
|
+
default_factory=dict
|
|
29
|
+
)
|
|
30
|
+
doc: str | None = None
|
morphir/ir/v4/name.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Name(tuple[str, ...]):
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def from_list(words: list[str]) -> Name:
|
|
9
|
+
return Name(tuple(word.lower() for word in words))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def to_list(name: Name) -> list[str]:
|
|
13
|
+
return list(name)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def from_string(s: str) -> Name:
|
|
17
|
+
# Split by common delimiters including those used in FQName (colon, hash)
|
|
18
|
+
# IR v4 canonical is kebab-case (hyphens).
|
|
19
|
+
# Also handle underscores (snake_case), dots (paths), spaces, colons, hashes.
|
|
20
|
+
words = re.split(r"[_\-\s\.:#]", s)
|
|
21
|
+
# Filter empty strings
|
|
22
|
+
words = [w for w in words if w]
|
|
23
|
+
|
|
24
|
+
result = []
|
|
25
|
+
for word in words:
|
|
26
|
+
# Split camelCase / PascalCase / Acronyms
|
|
27
|
+
# Regex explanation:
|
|
28
|
+
# 1. [A-Z]+(?=[A-Z][a-z]) : Acronym followed by Capitalized word (e.g. JSON in JSONResponse)
|
|
29
|
+
# 2. [A-Z][a-z0-9]+ : Capitalized word with at least one lowercase/digit (e.g. Response)
|
|
30
|
+
# 3. [A-Z]+ : Acronym at end or isolated (e.g. SDK, ID in MakeID)
|
|
31
|
+
# 4. [a-z][a-z0-9]* : Lowercase word
|
|
32
|
+
# 5. [0-9]+ : Numbers
|
|
33
|
+
parts = re.findall(
|
|
34
|
+
r"[A-Z]+(?=[A-Z][a-z])|[A-Z][a-z0-9]+|[A-Z]+|[a-z][a-z0-9]*|[0-9]+", word
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if not parts:
|
|
38
|
+
parts = [word] # fallback
|
|
39
|
+
|
|
40
|
+
for part in parts:
|
|
41
|
+
result.append(part.lower())
|
|
42
|
+
|
|
43
|
+
return Name(tuple(result))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def to_title_case(name: Name) -> str:
|
|
47
|
+
return "".join(word.capitalize() for word in name)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def to_camel_case(name: Name) -> str:
|
|
51
|
+
if not name:
|
|
52
|
+
return ""
|
|
53
|
+
return name[0] + "".join(word.capitalize() for word in name[1:])
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def to_snake_case(name: Name) -> str:
|
|
57
|
+
return "_".join(name)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def to_kebab_case(name: Name) -> str:
|
|
61
|
+
return "-".join(name)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def to_string(name: Name) -> str:
|
|
65
|
+
return to_kebab_case(name)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def to_human_words(name: Name) -> list[str]:
|
|
69
|
+
return list(name)
|
morphir/ir/v4/package.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
|
|
3
|
+
from . import module
|
|
4
|
+
from .access_controlled import AccessControlled
|
|
5
|
+
from .path import Path
|
|
6
|
+
|
|
7
|
+
PackageName = Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class Specification:
|
|
12
|
+
modules: dict[module.ModuleName, module.Specification] = field(default_factory=dict)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class Definition:
|
|
17
|
+
modules: dict[module.ModuleName, AccessControlled[module.Definition]] = field(
|
|
18
|
+
default_factory=dict
|
|
19
|
+
)
|