modelable 1.0.0__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.

Potentially problematic release.


This version of modelable might be problematic. Click here for more details.

Files changed (122) hide show
  1. modelable/__init__.py +1 -0
  2. modelable/__main__.py +3 -0
  3. modelable/_pydantic_py314_compat.py +31 -0
  4. modelable/cli.py +41 -0
  5. modelable/commands/__init__.py +1 -0
  6. modelable/commands/apicurio.py +84 -0
  7. modelable/commands/codegen.py +241 -0
  8. modelable/commands/common.py +43 -0
  9. modelable/commands/compile.py +237 -0
  10. modelable/commands/create.py +164 -0
  11. modelable/commands/diff.py +82 -0
  12. modelable/commands/graph.py +53 -0
  13. modelable/commands/llm.py +564 -0
  14. modelable/commands/lsp.py +15 -0
  15. modelable/commands/runtime.py +37 -0
  16. modelable/commands/scenario.py +104 -0
  17. modelable/commands/spec.py +197 -0
  18. modelable/commands/workspace.py +240 -0
  19. modelable/compat/__init__.py +11 -0
  20. modelable/compat/checker.py +179 -0
  21. modelable/compat/diff.py +169 -0
  22. modelable/compiler/__init__.py +3 -0
  23. modelable/compiler/compiler.py +19 -0
  24. modelable/compiler/workspace.py +346 -0
  25. modelable/diagnostics/__init__.py +3 -0
  26. modelable/diagnostics/model.py +27 -0
  27. modelable/emitters/__init__.py +0 -0
  28. modelable/emitters/base.py +22 -0
  29. modelable/emitters/csharp.py +245 -0
  30. modelable/emitters/dbt_yaml.py +290 -0
  31. modelable/emitters/diagnostics.py +25 -0
  32. modelable/emitters/fhir.py +694 -0
  33. modelable/emitters/fhir_validator.py +36 -0
  34. modelable/emitters/go.py +334 -0
  35. modelable/emitters/java.py +264 -0
  36. modelable/emitters/json_schema.py +458 -0
  37. modelable/emitters/markdown.py +252 -0
  38. modelable/emitters/odcs.py +355 -0
  39. modelable/emitters/openlineage.py +315 -0
  40. modelable/emitters/openmetadata.py +258 -0
  41. modelable/emitters/python.py +282 -0
  42. modelable/emitters/rust.py +643 -0
  43. modelable/emitters/shapes.py +261 -0
  44. modelable/emitters/sql.py +266 -0
  45. modelable/emitters/targets.py +141 -0
  46. modelable/emitters/typescript.py +352 -0
  47. modelable/expressions/__init__.py +0 -0
  48. modelable/expressions/cel.py +547 -0
  49. modelable/governance/__init__.py +3 -0
  50. modelable/governance/checker.py +271 -0
  51. modelable/governance/por.py +46 -0
  52. modelable/grammar/__init__.py +1 -0
  53. modelable/grammar/modelable.lark +257 -0
  54. modelable/graph/__init__.py +5 -0
  55. modelable/graph/export.py +442 -0
  56. modelable/llm/__init__.py +43 -0
  57. modelable/llm/chat.py +255 -0
  58. modelable/llm/config.py +87 -0
  59. modelable/llm/context.py +194 -0
  60. modelable/llm/engine.py +976 -0
  61. modelable/llm/importers.py +1077 -0
  62. modelable/llm/provenance.py +84 -0
  63. modelable/llm/providers.py +182 -0
  64. modelable/llm/qa.py +126 -0
  65. modelable/llm/recommendations.py +33 -0
  66. modelable/llm/redaction.py +19 -0
  67. modelable/llm/render.py +279 -0
  68. modelable/llm/update_plan.py +101 -0
  69. modelable/llm/validation_help.py +10 -0
  70. modelable/lsp/__init__.py +3 -0
  71. modelable/lsp/__main__.py +4 -0
  72. modelable/lsp/code_actions.py +210 -0
  73. modelable/lsp/completion.py +480 -0
  74. modelable/lsp/definition.py +343 -0
  75. modelable/lsp/diagnostics.py +31 -0
  76. modelable/lsp/document_symbols.py +197 -0
  77. modelable/lsp/federation.py +261 -0
  78. modelable/lsp/folding.py +33 -0
  79. modelable/lsp/formatting.py +64 -0
  80. modelable/lsp/highlight.py +30 -0
  81. modelable/lsp/hover.py +370 -0
  82. modelable/lsp/inlay_hints.py +158 -0
  83. modelable/lsp/references.py +511 -0
  84. modelable/lsp/rename.py +564 -0
  85. modelable/lsp/semantic_tokens.py +412 -0
  86. modelable/lsp/server.py +370 -0
  87. modelable/lsp/workspace.py +83 -0
  88. modelable/lsp/workspace_symbols.py +104 -0
  89. modelable/parser/__init__.py +94 -0
  90. modelable/parser/ir.py +451 -0
  91. modelable/parser/parse.py +47 -0
  92. modelable/parser/transformer.py +798 -0
  93. modelable/parser/wire.py +68 -0
  94. modelable/planner/__init__.py +0 -0
  95. modelable/planner/lineage.py +91 -0
  96. modelable/planner/planner.py +134 -0
  97. modelable/planner/plans.py +122 -0
  98. modelable/py.typed +0 -0
  99. modelable/registry/__init__.py +9 -0
  100. modelable/registry/apicurio.py +166 -0
  101. modelable/registry/base.py +18 -0
  102. modelable/registry/factory.py +18 -0
  103. modelable/registry/index.py +419 -0
  104. modelable/registry/local.py +26 -0
  105. modelable/registry/oci.py +22 -0
  106. modelable/registry/resolver.py +213 -0
  107. modelable/registry/schema.sql +119 -0
  108. modelable/registry/signature.py +26 -0
  109. modelable/release.py +125 -0
  110. modelable/runtime/__init__.py +5 -0
  111. modelable/runtime/adapter/__init__.py +17 -0
  112. modelable/runtime/adapter/base.py +18 -0
  113. modelable/runtime/adapter/postgres.py +82 -0
  114. modelable/specs/__init__.py +23 -0
  115. modelable/specs/tracking.py +220 -0
  116. modelable/validation/__init__.py +3 -0
  117. modelable/validation/semantic.py +659 -0
  118. modelable-1.0.0.dist-info/METADATA +61 -0
  119. modelable-1.0.0.dist-info/RECORD +122 -0
  120. modelable-1.0.0.dist-info/WHEEL +4 -0
  121. modelable-1.0.0.dist-info/entry_points.txt +2 -0
  122. modelable-1.0.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,252 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from modelable.compiler.workspace import Workspace
6
+ from modelable.emitters.base import EmittedArtifact, compute_content_hash
7
+ from modelable.parser.ir import (
8
+ AnnClassification,
9
+ AnnDeprecated,
10
+ AnnKey,
11
+ AnnOwner,
12
+ AnnPii,
13
+ AnnServer,
14
+ AnnWire,
15
+ ArrayType,
16
+ ComputedMapping,
17
+ DecimalType,
18
+ DirectMapping,
19
+ DomainDef,
20
+ EnumType,
21
+ FieldDef,
22
+ MapType,
23
+ ModelVersion,
24
+ NamedType,
25
+ ObjectType,
26
+ PrimitiveType,
27
+ ProjectionField,
28
+ ProjectionVersion,
29
+ RefType,
30
+ VersionExact,
31
+ VersionMin,
32
+ VersionPinned,
33
+ VersionRange,
34
+ )
35
+ from modelable.parser.wire import render_wire_annotation
36
+
37
+
38
+ def emit_markdown(workspace: Workspace, out_dir: Path) -> list[EmittedArtifact]:
39
+ """Emit Markdown documentation for every model and projection version."""
40
+ artifacts: list[EmittedArtifact] = []
41
+ for domain in workspace.mdl.domains:
42
+ for model_name, versions in domain.models.items():
43
+ for version in versions:
44
+ artifacts.append(_emit_model(domain, model_name, version, out_dir))
45
+ for projection_name, versions in domain.projections.items():
46
+ for version in versions:
47
+ artifacts.append(_emit_projection(domain, projection_name, version, out_dir))
48
+ return artifacts
49
+
50
+
51
+ def _artifact_id(domain: str, name: str, version: int) -> str:
52
+ return f"{domain}.{name}.v{version}"
53
+
54
+
55
+ def _emit_model(domain: DomainDef, model_name: str, version: ModelVersion, out_dir: Path) -> EmittedArtifact:
56
+ artifact_id = _artifact_id(domain.name, model_name, version.version)
57
+ lines: list[str] = []
58
+
59
+ lines.append(f"# {model_name} v{version.version}")
60
+ lines.append("")
61
+ lines.append(f"**Domain:** {domain.name} ")
62
+ lines.append(f"**Name:** {model_name} ")
63
+ lines.append(f"**Version:** {version.version} ")
64
+ lines.append(f"**Artifact ID:** {artifact_id} ")
65
+ lines.append(f"**Artifact:** {artifact_id}.md ")
66
+ if domain.owner:
67
+ lines.append(f"**Owner:** {domain.owner} ")
68
+ if domain.contact:
69
+ lines.append(f"**Contact:** {domain.contact} ")
70
+ if domain.description:
71
+ lines.append(f"**Description:** {domain.description} ")
72
+ lines.append(f"**Kind:** {version.model_kind.value} ")
73
+ lines.append(f"**Change kind:** {version.change_kind.value} ")
74
+ lines.append("")
75
+
76
+ lines.append("## Fields")
77
+ lines.append("")
78
+ lines.append("| Field | Type | Required | Default | Annotations | Classification |")
79
+ lines.append("|---|---|---|---|---|---|")
80
+ for field in version.fields:
81
+ required = "yes" if not field.optional else "no"
82
+ default = field.default if field.default is not None else "—"
83
+ ann_str = _format_annotations(field)
84
+ cls_str = _field_classification(field)
85
+ lines.append(f"| {field.name} | {_type_str(field.type)} | {required} | {default} | {ann_str} | {cls_str} |")
86
+ lines.append("")
87
+
88
+ text = "\n".join(lines)
89
+ return EmittedArtifact(
90
+ target="markdown",
91
+ ref=f"{domain.name}.{model_name}@{version.version}",
92
+ artifact_id=artifact_id,
93
+ path=out_dir / f"{artifact_id}.md",
94
+ content=text,
95
+ content_hash=compute_content_hash(text),
96
+ )
97
+
98
+
99
+ def _emit_projection(
100
+ domain: DomainDef, projection_name: str, version: ProjectionVersion, out_dir: Path
101
+ ) -> EmittedArtifact:
102
+ artifact_id = _artifact_id(domain.name, projection_name, version.version)
103
+ lines: list[str] = []
104
+
105
+ lines.append(f"# {projection_name} v{version.version}")
106
+ lines.append("")
107
+ lines.append(f"**Domain:** {domain.name} ")
108
+ lines.append(f"**Name:** {projection_name} ")
109
+ lines.append(f"**Version:** {version.version} ")
110
+ lines.append(f"**Artifact ID:** {artifact_id} ")
111
+ lines.append(f"**Artifact:** {artifact_id}.md ")
112
+ if domain.owner:
113
+ lines.append(f"**Owner:** {domain.owner} ")
114
+ if domain.contact:
115
+ lines.append(f"**Contact:** {domain.contact} ")
116
+ if domain.description:
117
+ lines.append(f"**Description:** {domain.description} ")
118
+ lines.append("**Kind:** projection ")
119
+ auto_label = "yes" if version.auto_generated else "no"
120
+ lines.append(f"**Auto generated:** {auto_label} ")
121
+ lines.append(
122
+ f"**Source:** {version.source.model} @ {_version_str(version.source.version)} as {version.source.alias} "
123
+ )
124
+ if version.where:
125
+ lines.append(f"**Where:** {version.where} ")
126
+ if version.group_by:
127
+ lines.append(f"**Group by:** {', '.join(version.group_by)} ")
128
+ lines.append("")
129
+
130
+ lines.append("## Sources")
131
+ lines.append("")
132
+ lines.append("| Model | Version | Alias |")
133
+ lines.append("|---|---|---|")
134
+ lines.append(f"| {version.source.model} | {_version_str(version.source.version)} | {version.source.alias} |")
135
+ for join in version.joins:
136
+ lines.append(f"| {join.model} | {_version_str(join.version)} | {join.alias} (join on `{join.on}`) |")
137
+ lines.append("")
138
+
139
+ lines.append("## Fields")
140
+ lines.append("")
141
+ lines.append("| Field | Lineage | Annotations | Classification |")
142
+ lines.append("|---|---|---|---|")
143
+ for field in version.fields:
144
+ lineage_str = _format_lineage(field, version.source.model)
145
+ ann_str = _format_projection_annotations(field)
146
+ cls_str = _projection_field_classification(field)
147
+ lines.append(f"| {field.name} | {lineage_str} | {ann_str} | {cls_str} |")
148
+ lines.append("")
149
+
150
+ text = "\n".join(lines)
151
+ return EmittedArtifact(
152
+ target="markdown",
153
+ ref=f"{domain.name}.{projection_name}@{version.version}",
154
+ artifact_id=artifact_id,
155
+ path=out_dir / f"{artifact_id}.md",
156
+ content=text,
157
+ content_hash=compute_content_hash(text),
158
+ )
159
+
160
+
161
+ def _type_str(field_type) -> str:
162
+ if isinstance(field_type, PrimitiveType):
163
+ return field_type.kind
164
+ if isinstance(field_type, DecimalType):
165
+ return f"decimal({field_type.precision},{field_type.scale})"
166
+ if isinstance(field_type, ArrayType):
167
+ return f"array<{_type_str(field_type.item)}>"
168
+ if isinstance(field_type, MapType):
169
+ return f"map<{_type_str(field_type.key)},{_type_str(field_type.value)}>"
170
+ if isinstance(field_type, RefType):
171
+ return f"ref<{field_type.target}>"
172
+ if isinstance(field_type, EnumType):
173
+ return f"enum({', '.join(field_type.values)})"
174
+ if isinstance(field_type, NamedType):
175
+ return field_type.name
176
+ if isinstance(field_type, ObjectType):
177
+ return "object"
178
+ return "unknown"
179
+
180
+
181
+ def _version_str(version_spec) -> str:
182
+ if isinstance(version_spec, VersionExact):
183
+ return str(version_spec.version)
184
+ if isinstance(version_spec, VersionRange):
185
+ return f">={version_spec.min_inclusive}<{version_spec.max_exclusive}"
186
+ if isinstance(version_spec, VersionMin):
187
+ return f">={version_spec.min_inclusive}"
188
+ if isinstance(version_spec, VersionPinned):
189
+ return f"{version_spec.version}#{version_spec.content_hash}"
190
+ return "?"
191
+
192
+
193
+ def _format_annotations(field: FieldDef) -> str:
194
+ parts: list[str] = []
195
+ for ann in field.annotations:
196
+ if isinstance(ann, AnnKey):
197
+ parts.append("@key")
198
+ elif isinstance(ann, AnnPii):
199
+ parts.append("@pii")
200
+ elif isinstance(ann, AnnServer):
201
+ parts.append("@server")
202
+ elif isinstance(ann, AnnDeprecated):
203
+ parts.append(f"@deprecated → {ann.replaced_by}")
204
+ elif isinstance(ann, AnnOwner):
205
+ parts.append(f"@owner({ann.team})")
206
+ elif isinstance(ann, AnnClassification):
207
+ pass # shown in classification column
208
+ elif isinstance(ann, AnnWire):
209
+ parts.append(render_wire_annotation(ann))
210
+ return ", ".join(parts) if parts else "—"
211
+
212
+
213
+ def _format_projection_annotations(field: ProjectionField) -> str:
214
+ parts: list[str] = []
215
+ for ann in field.annotations:
216
+ if isinstance(ann, AnnPii):
217
+ parts.append("@pii")
218
+ elif isinstance(ann, AnnServer):
219
+ parts.append("@server")
220
+ elif isinstance(ann, AnnDeprecated):
221
+ parts.append(f"@deprecated → {ann.replaced_by}")
222
+ elif isinstance(ann, AnnOwner):
223
+ parts.append(f"@owner({ann.team})")
224
+ elif isinstance(ann, AnnClassification):
225
+ pass
226
+ elif isinstance(ann, AnnWire):
227
+ parts.append(render_wire_annotation(ann))
228
+ return ", ".join(parts) if parts else "—"
229
+
230
+
231
+ def _field_classification(field: FieldDef) -> str:
232
+ for ann in field.annotations:
233
+ if isinstance(ann, AnnClassification):
234
+ return ann.level
235
+ return "—"
236
+
237
+
238
+ def _projection_field_classification(field: ProjectionField) -> str:
239
+ for ann in field.annotations:
240
+ if isinstance(ann, AnnClassification):
241
+ return ann.level
242
+ return "—"
243
+
244
+
245
+ def _format_lineage(field: ProjectionField, source_model: str) -> str:
246
+ mapping = field.mapping
247
+ if isinstance(mapping, DirectMapping):
248
+ return f"direct: {mapping.source_alias}.{mapping.source_field} ({source_model})"
249
+ if isinstance(mapping, ComputedMapping):
250
+ expr = mapping.expression.replace("|", "\\|")
251
+ return f"computed: `{expr}`"
252
+ return "—"
@@ -0,0 +1,355 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ import yaml
7
+
8
+ from modelable.compiler.workspace import Workspace
9
+ from modelable.emitters.base import EmittedArtifact, compute_content_hash
10
+ from modelable.parser.ir import (
11
+ AnnOwner,
12
+ ArrayType,
13
+ ComputedMapping,
14
+ DecimalType,
15
+ DirectMapping,
16
+ DomainDef,
17
+ EnumType,
18
+ FieldDef,
19
+ FieldType,
20
+ MapType,
21
+ MdlFile,
22
+ ModelVersion,
23
+ NamedType,
24
+ ObjectType,
25
+ PrimitiveType,
26
+ ProjectionField,
27
+ ProjectionVersion,
28
+ RefType,
29
+ VersionExact,
30
+ VersionMin,
31
+ VersionPinned,
32
+ VersionRange,
33
+ )
34
+ from modelable.registry.resolver import ResolvedModelRef, resolve_model_ref
35
+
36
+ ODCS_VERSION = "v3.1.0"
37
+
38
+
39
+ def emit_odcs(workspace: Workspace, out_dir: Path) -> list[EmittedArtifact]:
40
+ """Emit Open Data Contract Standard YAML documents for each model and projection version."""
41
+ artifacts: list[EmittedArtifact] = []
42
+
43
+ for domain in workspace.mdl.domains:
44
+ for model_name, versions in domain.models.items():
45
+ for version in versions:
46
+ artifacts.append(_emit_model(domain, model_name, version, out_dir))
47
+ for projection_name, versions in domain.projections.items():
48
+ for version in versions:
49
+ artifacts.append(_emit_projection(domain, projection_name, version, out_dir, workspace.mdl))
50
+
51
+ return artifacts
52
+
53
+
54
+ def _emit_model(domain: DomainDef, model_name: str, version: ModelVersion, out_dir: Path) -> EmittedArtifact:
55
+ artifact_id = _artifact_id(domain.name, model_name, version.version)
56
+ ref = f"{domain.name}.{model_name}@{version.version}"
57
+ properties = [_model_property(field) for field in version.fields]
58
+ custom_properties = _base_custom_properties(domain, ref, version.model_kind.value)
59
+ custom_properties["modelableChangeKind"] = version.change_kind.value
60
+
61
+ doc = _contract_document(
62
+ domain=domain,
63
+ name=model_name,
64
+ version=version.version,
65
+ ref=ref,
66
+ kind=version.model_kind.value,
67
+ schema_custom_properties={"modelableKind": version.model_kind.value},
68
+ properties=properties,
69
+ custom_properties=custom_properties,
70
+ )
71
+
72
+ return _artifact("odcs", ref, artifact_id, out_dir / f"{artifact_id}.odcs.yaml", doc)
73
+
74
+
75
+ def _emit_projection(
76
+ domain: DomainDef,
77
+ projection_name: str,
78
+ version: ProjectionVersion,
79
+ out_dir: Path,
80
+ mdl: MdlFile,
81
+ ) -> EmittedArtifact:
82
+ artifact_id = _artifact_id(domain.name, projection_name, version.version)
83
+ ref = f"{domain.name}.{projection_name}@{version.version}"
84
+ source = _resolve_source(mdl, version)
85
+ properties = [_projection_property(field, version, source) for field in version.fields]
86
+ custom_properties = _base_custom_properties(domain, ref, "projection")
87
+ custom_properties["modelableSource"] = f"{version.source.model}@{_version_label(version.source.version)}"
88
+
89
+ schema_custom_properties: dict[str, Any] = {
90
+ "modelableKind": "projection",
91
+ "modelableSource": custom_properties["modelableSource"],
92
+ }
93
+ if version.where:
94
+ schema_custom_properties["modelableWhere"] = version.where
95
+ if version.group_by:
96
+ schema_custom_properties["modelableGroupBy"] = version.group_by
97
+
98
+ doc = _contract_document(
99
+ domain=domain,
100
+ name=projection_name,
101
+ version=version.version,
102
+ ref=ref,
103
+ kind="projection",
104
+ schema_custom_properties=schema_custom_properties,
105
+ properties=properties,
106
+ custom_properties=custom_properties,
107
+ )
108
+
109
+ return _artifact("odcs", ref, artifact_id, out_dir / f"{artifact_id}.odcs.yaml", doc)
110
+
111
+
112
+ def _contract_document(
113
+ *,
114
+ domain: DomainDef,
115
+ name: str,
116
+ version: int,
117
+ ref: str,
118
+ kind: str,
119
+ schema_custom_properties: dict[str, Any],
120
+ properties: list[dict[str, Any]],
121
+ custom_properties: dict[str, Any],
122
+ ) -> dict[str, Any]:
123
+ schema_entry: dict[str, Any] = {
124
+ "name": name,
125
+ "logicalType": "object",
126
+ "physicalName": name,
127
+ "properties": properties,
128
+ "customProperties": _custom_properties(schema_custom_properties),
129
+ }
130
+ return {
131
+ "apiVersion": ODCS_VERSION,
132
+ "kind": "DataContract",
133
+ "id": f"modelable://{domain.name}/{name}/v{version}",
134
+ "name": _artifact_id(domain.name, name, version),
135
+ "version": str(version),
136
+ "domain": domain.name,
137
+ "status": "active",
138
+ "description": {"purpose": domain.description or f"Modelable {kind} contract for {ref}"},
139
+ "schema": [schema_entry],
140
+ "authoritativeDefinitions": [{"url": f"modelable:{ref}", "type": "modelable"}],
141
+ "customProperties": _custom_properties(custom_properties),
142
+ }
143
+
144
+
145
+ def _model_property(field: FieldDef) -> dict[str, Any]:
146
+ prop = _field_property(field.name, field.type, required=not field.optional)
147
+ if field.is_key:
148
+ prop["primaryKey"] = True
149
+ _apply_governance(prop, field.is_pii, field.classification.value if field.classification else None, _owner(field))
150
+ return prop
151
+
152
+
153
+ def _projection_property(
154
+ field: ProjectionField,
155
+ projection: ProjectionVersion,
156
+ source: ResolvedModelRef | None,
157
+ ) -> dict[str, Any]:
158
+ source_field = _source_field(field, source)
159
+ field_type = source_field.type if source_field is not None else PrimitiveType(kind="string")
160
+ required = not source_field.optional if source_field is not None else True
161
+ prop = _field_property(field.name, field_type, required=required)
162
+
163
+ pii = field.is_pii or (source_field.is_pii if source_field is not None else False)
164
+ classification = field.classification or (source_field.classification if source_field is not None else None)
165
+ owner = _owner(source_field) if source_field is not None else None
166
+ _apply_governance(prop, pii, classification.value if classification is not None else None, owner)
167
+
168
+ custom_properties = {}
169
+ if isinstance(field.mapping, DirectMapping):
170
+ custom_properties["modelableMapping"] = "direct"
171
+ custom_properties["modelableLineage"] = [_source_field_ref(projection, field.mapping)]
172
+ elif isinstance(field.mapping, ComputedMapping):
173
+ custom_properties["modelableMapping"] = "computed"
174
+ custom_properties["modelableExpression"] = field.mapping.expression
175
+ else:
176
+ custom_properties["modelableMapping"] = "unknown"
177
+ prop["customProperties"].extend(_custom_properties(custom_properties))
178
+
179
+ return prop
180
+
181
+
182
+ def _field_property(name: str, field_type: FieldType, *, required: bool) -> dict[str, Any]:
183
+ type_info = _type_info(field_type)
184
+ prop: dict[str, Any] = {
185
+ "name": name,
186
+ "logicalType": type_info["logicalType"],
187
+ "required": required,
188
+ "customProperties": _custom_properties({"modelableType": type_info["modelable_type"]}),
189
+ }
190
+ if "logicalTypeOptions" in type_info:
191
+ prop["logicalTypeOptions"] = type_info["logicalTypeOptions"]
192
+ if "enum" in type_info:
193
+ prop["customProperties"].append({"property": "modelableEnum", "value": type_info["enum"]})
194
+ if extra := type_info.get("extra"):
195
+ prop["customProperties"].extend(_custom_properties(extra))
196
+ return prop
197
+
198
+
199
+ def _type_info(field_type: FieldType) -> dict[str, Any]:
200
+ if isinstance(field_type, PrimitiveType):
201
+ if field_type.kind == "uuid":
202
+ return {
203
+ "logicalType": "string",
204
+ "modelable_type": field_type.kind,
205
+ "logicalTypeOptions": {"format": "uuid"},
206
+ }
207
+ return {"logicalType": field_type.kind, "modelable_type": field_type.kind}
208
+ if isinstance(field_type, DecimalType):
209
+ return {
210
+ "logicalType": "number",
211
+ "modelable_type": f"decimal({field_type.precision},{field_type.scale})",
212
+ "extra": {"modelablePrecision": field_type.precision, "modelableScale": field_type.scale},
213
+ }
214
+ if isinstance(field_type, ArrayType):
215
+ return {
216
+ "logicalType": "array",
217
+ "modelable_type": f"array<{_type_name(field_type.item)}>",
218
+ "extra": {"modelableItemType": _type_name(field_type.item)},
219
+ }
220
+ if isinstance(field_type, MapType):
221
+ return {
222
+ "logicalType": "object",
223
+ "modelable_type": f"map<{_type_name(field_type.key)},{_type_name(field_type.value)}>",
224
+ "extra": {
225
+ "modelableKeyType": _type_name(field_type.key),
226
+ "modelableValueType": _type_name(field_type.value),
227
+ },
228
+ }
229
+ if isinstance(field_type, RefType):
230
+ return {
231
+ "logicalType": "string",
232
+ "modelable_type": f"ref<{field_type.target}>",
233
+ "extra": {"modelableRef": field_type.target},
234
+ }
235
+ if isinstance(field_type, EnumType):
236
+ return {"logicalType": "string", "modelable_type": _type_name(field_type), "enum": field_type.values}
237
+ if isinstance(field_type, ObjectType):
238
+ return {"logicalType": "object", "modelable_type": "object"}
239
+ if isinstance(field_type, NamedType):
240
+ return {
241
+ "logicalType": "object",
242
+ "modelable_type": field_type.name,
243
+ "extra": {"modelableNamedType": field_type.name},
244
+ }
245
+ return {"logicalType": "string", "modelable_type": "unknown"}
246
+
247
+
248
+ def _apply_governance(prop: dict[str, Any], pii: bool, classification: str | None, owner: str | None) -> None:
249
+ custom_properties: dict[str, Any] = {}
250
+ if pii:
251
+ custom_properties["modelablePii"] = True
252
+ if classification:
253
+ prop["classification"] = classification
254
+ if owner:
255
+ custom_properties["modelableOwner"] = owner
256
+ prop["customProperties"].extend(_custom_properties(custom_properties))
257
+
258
+
259
+ def _base_custom_properties(domain: DomainDef, ref: str, kind: str) -> dict[str, Any]:
260
+ custom_properties: dict[str, Any] = {
261
+ "modelableRef": ref,
262
+ "modelableKind": kind,
263
+ }
264
+ if domain.owner:
265
+ custom_properties["modelableOwner"] = domain.owner
266
+ if domain.contact:
267
+ custom_properties["modelableContact"] = domain.contact
268
+ return custom_properties
269
+
270
+
271
+ def _custom_properties(properties: dict[str, Any]) -> list[dict[str, Any]]:
272
+ return [{"property": key, "value": value} for key, value in properties.items()]
273
+
274
+
275
+ def _artifact(target: str, ref: str, artifact_id: str, path: Path, doc: dict[str, Any]) -> EmittedArtifact:
276
+ yaml_content = yaml.safe_dump(doc, sort_keys=False, default_flow_style=False)
277
+ content = f"# @generated by Modelable\n{yaml_content}"
278
+ return EmittedArtifact(
279
+ target=target,
280
+ ref=ref,
281
+ artifact_id=artifact_id,
282
+ path=path,
283
+ content=content,
284
+ content_hash=compute_content_hash(content),
285
+ )
286
+
287
+
288
+ def _resolve_source(mdl: MdlFile, projection: ProjectionVersion) -> ResolvedModelRef | None:
289
+ try:
290
+ return resolve_model_ref(mdl, projection.source.model, projection.source.version)
291
+ except LookupError:
292
+ return None
293
+
294
+
295
+ def _source_field(field: ProjectionField, source: ResolvedModelRef | None) -> FieldDef | None:
296
+ if source is None or not isinstance(field.mapping, DirectMapping):
297
+ return None
298
+ return next(
299
+ (source_field for source_field in source.version.fields if source_field.name == field.mapping.source_field),
300
+ None,
301
+ )
302
+
303
+
304
+ def _source_field_ref(projection: ProjectionVersion, mapping: DirectMapping) -> str:
305
+ version = projection.source.version
306
+ if isinstance(version, VersionExact):
307
+ return f"{projection.source.model}@{version.version}.{mapping.source_field}"
308
+ if isinstance(version, VersionPinned):
309
+ return f"{projection.source.model}@{version.version}#{version.content_hash}.{mapping.source_field}"
310
+ return f"{projection.source.model}@{_version_label(version)}.{mapping.source_field}"
311
+
312
+
313
+ def _owner(field: FieldDef) -> str | None:
314
+ for annotation in field.annotations:
315
+ if isinstance(annotation, AnnOwner):
316
+ return annotation.team
317
+ return None
318
+
319
+
320
+ def _version_label(version_spec: Any) -> str:
321
+ if isinstance(version_spec, VersionExact):
322
+ return str(version_spec.version)
323
+ if isinstance(version_spec, VersionRange):
324
+ return f">={version_spec.min_inclusive}<{version_spec.max_exclusive}"
325
+ if isinstance(version_spec, VersionMin):
326
+ return f">={version_spec.min_inclusive}"
327
+ if isinstance(version_spec, VersionPinned):
328
+ return f"{version_spec.version}#{version_spec.content_hash}"
329
+ if isinstance(version_spec, int):
330
+ return str(version_spec)
331
+ return "?"
332
+
333
+
334
+ def _artifact_id(domain: str, name: str, version: int) -> str:
335
+ return f"{domain}.{name}.v{version}"
336
+
337
+
338
+ def _type_name(field_type: FieldType) -> str:
339
+ if isinstance(field_type, PrimitiveType):
340
+ return field_type.kind
341
+ if isinstance(field_type, DecimalType):
342
+ return f"decimal({field_type.precision},{field_type.scale})"
343
+ if isinstance(field_type, ArrayType):
344
+ return f"array<{_type_name(field_type.item)}>"
345
+ if isinstance(field_type, MapType):
346
+ return f"map<{_type_name(field_type.key)},{_type_name(field_type.value)}>"
347
+ if isinstance(field_type, RefType):
348
+ return f"ref<{field_type.target}>"
349
+ if isinstance(field_type, EnumType):
350
+ return "enum(" + ",".join(field_type.values) + ")"
351
+ if isinstance(field_type, NamedType):
352
+ return field_type.name
353
+ if isinstance(field_type, ObjectType):
354
+ return "object"
355
+ return "unknown"