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.
- modelable/__init__.py +1 -0
- modelable/__main__.py +3 -0
- modelable/_pydantic_py314_compat.py +31 -0
- modelable/cli.py +41 -0
- modelable/commands/__init__.py +1 -0
- modelable/commands/apicurio.py +84 -0
- modelable/commands/codegen.py +241 -0
- modelable/commands/common.py +43 -0
- modelable/commands/compile.py +237 -0
- modelable/commands/create.py +164 -0
- modelable/commands/diff.py +82 -0
- modelable/commands/graph.py +53 -0
- modelable/commands/llm.py +564 -0
- modelable/commands/lsp.py +15 -0
- modelable/commands/runtime.py +37 -0
- modelable/commands/scenario.py +104 -0
- modelable/commands/spec.py +197 -0
- modelable/commands/workspace.py +240 -0
- modelable/compat/__init__.py +11 -0
- modelable/compat/checker.py +179 -0
- modelable/compat/diff.py +169 -0
- modelable/compiler/__init__.py +3 -0
- modelable/compiler/compiler.py +19 -0
- modelable/compiler/workspace.py +346 -0
- modelable/diagnostics/__init__.py +3 -0
- modelable/diagnostics/model.py +27 -0
- modelable/emitters/__init__.py +0 -0
- modelable/emitters/base.py +22 -0
- modelable/emitters/csharp.py +245 -0
- modelable/emitters/dbt_yaml.py +290 -0
- modelable/emitters/diagnostics.py +25 -0
- modelable/emitters/fhir.py +694 -0
- modelable/emitters/fhir_validator.py +36 -0
- modelable/emitters/go.py +334 -0
- modelable/emitters/java.py +264 -0
- modelable/emitters/json_schema.py +458 -0
- modelable/emitters/markdown.py +252 -0
- modelable/emitters/odcs.py +355 -0
- modelable/emitters/openlineage.py +315 -0
- modelable/emitters/openmetadata.py +258 -0
- modelable/emitters/python.py +282 -0
- modelable/emitters/rust.py +643 -0
- modelable/emitters/shapes.py +261 -0
- modelable/emitters/sql.py +266 -0
- modelable/emitters/targets.py +141 -0
- modelable/emitters/typescript.py +352 -0
- modelable/expressions/__init__.py +0 -0
- modelable/expressions/cel.py +547 -0
- modelable/governance/__init__.py +3 -0
- modelable/governance/checker.py +271 -0
- modelable/governance/por.py +46 -0
- modelable/grammar/__init__.py +1 -0
- modelable/grammar/modelable.lark +257 -0
- modelable/graph/__init__.py +5 -0
- modelable/graph/export.py +442 -0
- modelable/llm/__init__.py +43 -0
- modelable/llm/chat.py +255 -0
- modelable/llm/config.py +87 -0
- modelable/llm/context.py +194 -0
- modelable/llm/engine.py +976 -0
- modelable/llm/importers.py +1077 -0
- modelable/llm/provenance.py +84 -0
- modelable/llm/providers.py +182 -0
- modelable/llm/qa.py +126 -0
- modelable/llm/recommendations.py +33 -0
- modelable/llm/redaction.py +19 -0
- modelable/llm/render.py +279 -0
- modelable/llm/update_plan.py +101 -0
- modelable/llm/validation_help.py +10 -0
- modelable/lsp/__init__.py +3 -0
- modelable/lsp/__main__.py +4 -0
- modelable/lsp/code_actions.py +210 -0
- modelable/lsp/completion.py +480 -0
- modelable/lsp/definition.py +343 -0
- modelable/lsp/diagnostics.py +31 -0
- modelable/lsp/document_symbols.py +197 -0
- modelable/lsp/federation.py +261 -0
- modelable/lsp/folding.py +33 -0
- modelable/lsp/formatting.py +64 -0
- modelable/lsp/highlight.py +30 -0
- modelable/lsp/hover.py +370 -0
- modelable/lsp/inlay_hints.py +158 -0
- modelable/lsp/references.py +511 -0
- modelable/lsp/rename.py +564 -0
- modelable/lsp/semantic_tokens.py +412 -0
- modelable/lsp/server.py +370 -0
- modelable/lsp/workspace.py +83 -0
- modelable/lsp/workspace_symbols.py +104 -0
- modelable/parser/__init__.py +94 -0
- modelable/parser/ir.py +451 -0
- modelable/parser/parse.py +47 -0
- modelable/parser/transformer.py +798 -0
- modelable/parser/wire.py +68 -0
- modelable/planner/__init__.py +0 -0
- modelable/planner/lineage.py +91 -0
- modelable/planner/planner.py +134 -0
- modelable/planner/plans.py +122 -0
- modelable/py.typed +0 -0
- modelable/registry/__init__.py +9 -0
- modelable/registry/apicurio.py +166 -0
- modelable/registry/base.py +18 -0
- modelable/registry/factory.py +18 -0
- modelable/registry/index.py +419 -0
- modelable/registry/local.py +26 -0
- modelable/registry/oci.py +22 -0
- modelable/registry/resolver.py +213 -0
- modelable/registry/schema.sql +119 -0
- modelable/registry/signature.py +26 -0
- modelable/release.py +125 -0
- modelable/runtime/__init__.py +5 -0
- modelable/runtime/adapter/__init__.py +17 -0
- modelable/runtime/adapter/base.py +18 -0
- modelable/runtime/adapter/postgres.py +82 -0
- modelable/specs/__init__.py +23 -0
- modelable/specs/tracking.py +220 -0
- modelable/validation/__init__.py +3 -0
- modelable/validation/semantic.py +659 -0
- modelable-1.0.0.dist-info/METADATA +61 -0
- modelable-1.0.0.dist-info/RECORD +122 -0
- modelable-1.0.0.dist-info/WHEEL +4 -0
- modelable-1.0.0.dist-info/entry_points.txt +2 -0
- 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"
|