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.
Files changed (99) hide show
  1. morphir/__init__.py +14 -0
  2. morphir/ir/__init__.py +110 -0
  3. morphir/ir/access_controlled.py +15 -0
  4. morphir/ir/decorations.py +19 -0
  5. morphir/ir/diagnostics.py +105 -0
  6. morphir/ir/distribution.py +21 -0
  7. morphir/ir/document.py +37 -0
  8. morphir/ir/documented.py +11 -0
  9. morphir/ir/format_version.py +483 -0
  10. morphir/ir/fqname.py +9 -0
  11. morphir/ir/json.py +11 -0
  12. morphir/ir/literal.py +25 -0
  13. morphir/ir/meta.py +11 -0
  14. morphir/ir/migrate.py +1267 -0
  15. morphir/ir/module.py +17 -0
  16. morphir/ir/name.py +27 -0
  17. morphir/ir/package.py +15 -0
  18. morphir/ir/path.py +25 -0
  19. morphir/ir/qname.py +9 -0
  20. morphir/ir/ref.py +17 -0
  21. morphir/ir/type.py +37 -0
  22. morphir/ir/type_constraints.py +33 -0
  23. morphir/ir/type_def.py +19 -0
  24. morphir/ir/type_spec.py +25 -0
  25. morphir/ir/v3/__init__.py +64 -0
  26. morphir/ir/v3/_legacy_value.py +201 -0
  27. morphir/ir/v3/access_controlled.py +31 -0
  28. morphir/ir/v3/codec.py +1275 -0
  29. morphir/ir/v3/codec_v1.py +491 -0
  30. morphir/ir/v3/codec_v2.py +187 -0
  31. morphir/ir/v3/distribution.py +42 -0
  32. morphir/ir/v3/documented.py +13 -0
  33. morphir/ir/v3/fqname.py +30 -0
  34. morphir/ir/v3/literal.py +46 -0
  35. morphir/ir/v3/module.py +32 -0
  36. morphir/ir/v3/name.py +42 -0
  37. morphir/ir/v3/package.py +23 -0
  38. morphir/ir/v3/path.py +29 -0
  39. morphir/ir/v3/qname.py +16 -0
  40. morphir/ir/v3/type.py +126 -0
  41. morphir/ir/v3/value.py +235 -0
  42. morphir/ir/v4/__init__.py +160 -0
  43. morphir/ir/v4/access_controlled.py +17 -0
  44. morphir/ir/v4/annotation.py +75 -0
  45. morphir/ir/v4/codec.py +2161 -0
  46. morphir/ir/v4/decorations.py +88 -0
  47. morphir/ir/v4/distribution.py +56 -0
  48. morphir/ir/v4/document.py +69 -0
  49. morphir/ir/v4/documented.py +10 -0
  50. morphir/ir/v4/fqname.py +69 -0
  51. morphir/ir/v4/incompleteness.py +78 -0
  52. morphir/ir/v4/json.py +103 -0
  53. morphir/ir/v4/literal.py +51 -0
  54. morphir/ir/v4/meta.py +27 -0
  55. morphir/ir/v4/module.py +30 -0
  56. morphir/ir/v4/name.py +69 -0
  57. morphir/ir/v4/package.py +19 -0
  58. morphir/ir/v4/path.py +50 -0
  59. morphir/ir/v4/qname.py +34 -0
  60. morphir/ir/v4/ref.py +35 -0
  61. morphir/ir/v4/type.py +140 -0
  62. morphir/ir/v4/type_constraints.py +91 -0
  63. morphir/ir/v4/type_def.py +38 -0
  64. morphir/ir/v4/type_spec.py +64 -0
  65. morphir/ir/v4/value.py +480 -0
  66. morphir/ir/value.py +81 -0
  67. morphir/py.typed +0 -0
  68. morphir/sdk/__init__.py +110 -0
  69. morphir/sdk/_compare.py +187 -0
  70. morphir/sdk/aggregate.py +421 -0
  71. morphir/sdk/basics.py +504 -0
  72. morphir/sdk/char.py +110 -0
  73. morphir/sdk/decimal.py +311 -0
  74. morphir/sdk/dict.py +336 -0
  75. morphir/sdk/instant.py +118 -0
  76. morphir/sdk/int.py +86 -0
  77. morphir/sdk/key.py +522 -0
  78. morphir/sdk/list.py +356 -0
  79. morphir/sdk/local_date.py +436 -0
  80. morphir/sdk/local_time.py +267 -0
  81. morphir/sdk/maybe.py +135 -0
  82. morphir/sdk/number.py +237 -0
  83. morphir/sdk/regex.py +287 -0
  84. morphir/sdk/result.py +162 -0
  85. morphir/sdk/result_list.py +169 -0
  86. morphir/sdk/rule.py +103 -0
  87. morphir/sdk/set.py +214 -0
  88. morphir/sdk/stateful_app.py +49 -0
  89. morphir/sdk/string.py +427 -0
  90. morphir/sdk/tuple.py +43 -0
  91. morphir/sdk/uuid.py +209 -0
  92. morphir_python-0.0.1.dist-info/METADATA +269 -0
  93. morphir_python-0.0.1.dist-info/RECORD +99 -0
  94. morphir_python-0.0.1.dist-info/WHEEL +4 -0
  95. morphir_python-0.0.1.dist-info/entry_points.txt +2 -0
  96. morphir_python-0.0.1.dist-info/licenses/LICENSE +201 -0
  97. morphir_tools/__init__.py +13 -0
  98. morphir_tools/cli.py +223 -0
  99. 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)
@@ -0,0 +1,10 @@
1
+ from dataclasses import dataclass
2
+ from typing import Generic, TypeVar
3
+
4
+ T = TypeVar("T")
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class Documented(Generic[T]):
9
+ doc: str | None
10
+ value: T
@@ -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)
@@ -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)
@@ -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)
@@ -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
+ )