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,290 @@
|
|
|
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
|
+
ArrayType,
|
|
12
|
+
DecimalType,
|
|
13
|
+
DirectMapping,
|
|
14
|
+
DomainDef,
|
|
15
|
+
EnumType,
|
|
16
|
+
MapType,
|
|
17
|
+
MdlFile,
|
|
18
|
+
ModelVersion,
|
|
19
|
+
NamedType,
|
|
20
|
+
ObjectType,
|
|
21
|
+
PrimitiveType,
|
|
22
|
+
ProjectionField,
|
|
23
|
+
ProjectionVersion,
|
|
24
|
+
RefType,
|
|
25
|
+
VersionExact,
|
|
26
|
+
VersionMin,
|
|
27
|
+
VersionPinned,
|
|
28
|
+
VersionRange,
|
|
29
|
+
)
|
|
30
|
+
from modelable.registry.resolver import resolve_model_ref
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def emit_dbt_yaml(workspace: Workspace, out_dir: Path) -> list[EmittedArtifact]:
|
|
34
|
+
"""Emit dbt schema.yml fragments for every model and projection version."""
|
|
35
|
+
artifacts: list[EmittedArtifact] = []
|
|
36
|
+
for domain in workspace.mdl.domains:
|
|
37
|
+
for model_name, versions in domain.models.items():
|
|
38
|
+
for version in versions:
|
|
39
|
+
artifacts.append(_emit_model(domain, model_name, version, out_dir))
|
|
40
|
+
for projection_name, versions in domain.projections.items():
|
|
41
|
+
for version in versions:
|
|
42
|
+
artifacts.append(_emit_projection(domain, projection_name, version, out_dir, workspace.mdl))
|
|
43
|
+
return artifacts
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _emit_model(domain: DomainDef, model_name: str, version: ModelVersion, out_dir: Path) -> EmittedArtifact:
|
|
47
|
+
artifact_id = f"{domain.name}.{model_name}.v{version.version}"
|
|
48
|
+
|
|
49
|
+
meta: dict[str, Any] = {
|
|
50
|
+
"modelable_domain": domain.name,
|
|
51
|
+
"modelable_name": model_name,
|
|
52
|
+
"modelable_kind": version.model_kind.value,
|
|
53
|
+
"modelable_version": version.version,
|
|
54
|
+
"modelable_change_kind": version.change_kind.value,
|
|
55
|
+
}
|
|
56
|
+
if domain.owner:
|
|
57
|
+
meta["modelable_owner"] = domain.owner
|
|
58
|
+
if domain.contact:
|
|
59
|
+
meta["modelable_contact"] = domain.contact
|
|
60
|
+
|
|
61
|
+
columns: list[dict[str, Any]] = []
|
|
62
|
+
for field in version.fields:
|
|
63
|
+
col: dict[str, Any] = {
|
|
64
|
+
"name": field.name,
|
|
65
|
+
"data_type": _type_to_dbt(field.type, wire_targets=field.wire_targets()),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# constraints
|
|
69
|
+
constraints: list[dict[str, str]] = []
|
|
70
|
+
if field.is_key:
|
|
71
|
+
constraints.append({"type": "primary_key"})
|
|
72
|
+
if not field.optional:
|
|
73
|
+
constraints.append({"type": "not_null"})
|
|
74
|
+
if constraints:
|
|
75
|
+
col["constraints"] = constraints
|
|
76
|
+
|
|
77
|
+
# meta
|
|
78
|
+
col_meta: dict[str, Any] = {}
|
|
79
|
+
if field.is_pii:
|
|
80
|
+
col_meta["modelable_pii"] = True
|
|
81
|
+
if field.classification:
|
|
82
|
+
col_meta["modelable_classification"] = field.classification.value
|
|
83
|
+
if domain.owner:
|
|
84
|
+
col_meta["modelable_owner"] = domain.owner
|
|
85
|
+
|
|
86
|
+
if col_meta:
|
|
87
|
+
col["meta"] = col_meta
|
|
88
|
+
|
|
89
|
+
columns.append(col)
|
|
90
|
+
|
|
91
|
+
model_entry: dict[str, Any] = {
|
|
92
|
+
"name": model_name,
|
|
93
|
+
"latest_version": version.version,
|
|
94
|
+
"meta": meta,
|
|
95
|
+
"versions": [
|
|
96
|
+
{
|
|
97
|
+
"v": version.version,
|
|
98
|
+
"columns": columns,
|
|
99
|
+
}
|
|
100
|
+
],
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if domain.description:
|
|
104
|
+
model_entry["description"] = domain.description
|
|
105
|
+
|
|
106
|
+
doc = {
|
|
107
|
+
"version": 2,
|
|
108
|
+
"models": [model_entry],
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
yaml_content = yaml.safe_dump(doc, sort_keys=False, default_flow_style=False)
|
|
112
|
+
content = f"# @generated by Modelable\n{yaml_content}"
|
|
113
|
+
|
|
114
|
+
return EmittedArtifact(
|
|
115
|
+
target="dbt-yaml",
|
|
116
|
+
ref=f"{domain.name}.{model_name}@{version.version}",
|
|
117
|
+
artifact_id=artifact_id,
|
|
118
|
+
path=out_dir / f"{artifact_id}.yml",
|
|
119
|
+
content=content,
|
|
120
|
+
content_hash=compute_content_hash(content),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _emit_projection(
|
|
125
|
+
domain: DomainDef,
|
|
126
|
+
projection_name: str,
|
|
127
|
+
version: ProjectionVersion,
|
|
128
|
+
out_dir: Path,
|
|
129
|
+
mdl: MdlFile,
|
|
130
|
+
) -> EmittedArtifact:
|
|
131
|
+
artifact_id = f"{domain.name}.{projection_name}.v{version.version}"
|
|
132
|
+
|
|
133
|
+
meta: dict[str, Any] = {
|
|
134
|
+
"modelable_domain": domain.name,
|
|
135
|
+
"modelable_name": projection_name,
|
|
136
|
+
"modelable_kind": "projection",
|
|
137
|
+
"modelable_version": version.version,
|
|
138
|
+
"modelable_source": f"{version.source.model}@{_version_label(version.source.version)}",
|
|
139
|
+
}
|
|
140
|
+
if domain.owner:
|
|
141
|
+
meta["modelable_owner"] = domain.owner
|
|
142
|
+
if domain.contact:
|
|
143
|
+
meta["modelable_contact"] = domain.contact
|
|
144
|
+
if version.where:
|
|
145
|
+
meta["modelable_where"] = version.where
|
|
146
|
+
if version.group_by:
|
|
147
|
+
meta["modelable_group_by"] = ", ".join(version.group_by)
|
|
148
|
+
|
|
149
|
+
columns: list[dict[str, Any]] = []
|
|
150
|
+
for field in version.fields:
|
|
151
|
+
col: dict[str, Any] = {
|
|
152
|
+
"name": field.name,
|
|
153
|
+
"data_type": _resolve_projection_field_type_string(field, version, mdl),
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
col_meta: dict[str, Any] = {}
|
|
157
|
+
lineage = _resolve_lineage(field, version, mdl)
|
|
158
|
+
if lineage:
|
|
159
|
+
col_meta["modelable_lineage"] = [lineage]
|
|
160
|
+
|
|
161
|
+
if field.is_pii:
|
|
162
|
+
col_meta["modelable_pii"] = True
|
|
163
|
+
if field.classification:
|
|
164
|
+
col_meta["modelable_classification"] = field.classification.value
|
|
165
|
+
if domain.owner:
|
|
166
|
+
col_meta["modelable_owner"] = domain.owner
|
|
167
|
+
|
|
168
|
+
if col_meta:
|
|
169
|
+
col["meta"] = col_meta
|
|
170
|
+
|
|
171
|
+
columns.append(col)
|
|
172
|
+
|
|
173
|
+
model_entry: dict[str, Any] = {
|
|
174
|
+
"name": projection_name,
|
|
175
|
+
"latest_version": version.version,
|
|
176
|
+
"meta": meta,
|
|
177
|
+
"versions": [
|
|
178
|
+
{
|
|
179
|
+
"v": version.version,
|
|
180
|
+
"columns": columns,
|
|
181
|
+
}
|
|
182
|
+
],
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if domain.description:
|
|
186
|
+
model_entry["description"] = domain.description
|
|
187
|
+
|
|
188
|
+
doc = {
|
|
189
|
+
"version": 2,
|
|
190
|
+
"models": [model_entry],
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
yaml_content = yaml.safe_dump(doc, sort_keys=False, default_flow_style=False)
|
|
194
|
+
content = f"# @generated by Modelable\n{yaml_content}"
|
|
195
|
+
|
|
196
|
+
return EmittedArtifact(
|
|
197
|
+
target="dbt-yaml",
|
|
198
|
+
ref=f"{domain.name}.{projection_name}@{version.version}",
|
|
199
|
+
artifact_id=artifact_id,
|
|
200
|
+
path=out_dir / f"{artifact_id}.yml",
|
|
201
|
+
content=content,
|
|
202
|
+
content_hash=compute_content_hash(content),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _version_label(version_spec: Any) -> str:
|
|
207
|
+
if isinstance(version_spec, VersionExact):
|
|
208
|
+
return str(version_spec.version)
|
|
209
|
+
if isinstance(version_spec, VersionRange):
|
|
210
|
+
return f">={version_spec.min_inclusive}<{version_spec.max_exclusive}"
|
|
211
|
+
if isinstance(version_spec, VersionMin):
|
|
212
|
+
return f">={version_spec.min_inclusive}"
|
|
213
|
+
if isinstance(version_spec, VersionPinned):
|
|
214
|
+
return f"{version_spec.version}#{version_spec.content_hash}"
|
|
215
|
+
if isinstance(version_spec, int):
|
|
216
|
+
return str(version_spec)
|
|
217
|
+
return "?"
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _type_to_dbt(field_type: Any, *, wire_targets: dict[str, Any] | None = None) -> str:
|
|
221
|
+
json_wire = wire_targets.get("json") if wire_targets is not None else None
|
|
222
|
+
|
|
223
|
+
if isinstance(field_type, PrimitiveType):
|
|
224
|
+
if (
|
|
225
|
+
json_wire is not None
|
|
226
|
+
and getattr(json_wire, "encoding", None) == "string"
|
|
227
|
+
and field_type.kind in {"int", "float"}
|
|
228
|
+
):
|
|
229
|
+
return "varchar"
|
|
230
|
+
|
|
231
|
+
mapping = {
|
|
232
|
+
"string": "text",
|
|
233
|
+
"int": "integer",
|
|
234
|
+
"float": "float",
|
|
235
|
+
"bool": "boolean",
|
|
236
|
+
"date": "date",
|
|
237
|
+
"time": "time",
|
|
238
|
+
"timestamp": "timestamp",
|
|
239
|
+
"uuid": "uuid",
|
|
240
|
+
"duration": "varchar",
|
|
241
|
+
"binary": "binary",
|
|
242
|
+
"json": "text",
|
|
243
|
+
}
|
|
244
|
+
return mapping.get(field_type.kind, "varchar")
|
|
245
|
+
|
|
246
|
+
if isinstance(field_type, DecimalType):
|
|
247
|
+
return f"numeric({field_type.precision}, {field_type.scale})"
|
|
248
|
+
if isinstance(field_type, ArrayType):
|
|
249
|
+
return f"{_type_to_dbt(field_type.item)}[]"
|
|
250
|
+
if isinstance(field_type, MapType):
|
|
251
|
+
return "text"
|
|
252
|
+
if isinstance(field_type, RefType):
|
|
253
|
+
return "text"
|
|
254
|
+
if isinstance(field_type, EnumType):
|
|
255
|
+
return "text"
|
|
256
|
+
if isinstance(field_type, ObjectType):
|
|
257
|
+
return "text"
|
|
258
|
+
if isinstance(field_type, NamedType):
|
|
259
|
+
return "text"
|
|
260
|
+
return "text"
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _resolve_projection_field_type_string(field: ProjectionField, projection: ProjectionVersion, mdl: MdlFile) -> str:
|
|
264
|
+
if not isinstance(field.mapping, DirectMapping):
|
|
265
|
+
return "text"
|
|
266
|
+
try:
|
|
267
|
+
source_domain, source_model = projection.source.model.rsplit(".", 1)
|
|
268
|
+
resolved = resolve_model_ref(mdl, f"{source_domain}.{source_model}", projection.source.version)
|
|
269
|
+
except ValueError, LookupError:
|
|
270
|
+
return "text"
|
|
271
|
+
|
|
272
|
+
for src_field in resolved.version.fields:
|
|
273
|
+
if src_field.name == field.mapping.source_field:
|
|
274
|
+
return _type_to_dbt(src_field.type, wire_targets=src_field.wire_targets())
|
|
275
|
+
return "text"
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _resolve_lineage(field: ProjectionField, projection: ProjectionVersion, mdl: MdlFile) -> str | None:
|
|
279
|
+
if not isinstance(field.mapping, DirectMapping):
|
|
280
|
+
return None
|
|
281
|
+
try:
|
|
282
|
+
source_domain, source_model = projection.source.model.rsplit(".", 1)
|
|
283
|
+
resolved = resolve_model_ref(mdl, f"{source_domain}.{source_model}", projection.source.version)
|
|
284
|
+
except ValueError, LookupError:
|
|
285
|
+
return None
|
|
286
|
+
|
|
287
|
+
for src_field in resolved.version.fields:
|
|
288
|
+
if src_field.name == field.mapping.source_field:
|
|
289
|
+
return f"{resolved.domain_name}.{resolved.model_name}@{_version_label(resolved.version.version)}.{src_field.name}"
|
|
290
|
+
return None
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def emit_warning(code: str, message: str) -> str:
|
|
5
|
+
return f"[{code}] {message}"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def unsupported_target(target: str) -> str:
|
|
9
|
+
return emit_warning("EMIT001", f"Unsupported target: {target}")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def type_loss(field_type: str) -> str:
|
|
13
|
+
return emit_warning("EMIT002", f"Type '{field_type}' cannot be represented without loss")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def missing_metadata(field: str) -> str:
|
|
17
|
+
return emit_warning("EMIT003", f"Missing metadata required by target: {field}")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def validation_failed(path: str, detail: str) -> str:
|
|
21
|
+
return emit_warning("EMIT004", f"Generated artifact failed validation: {path} ({detail})")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def deferred_target(target: str) -> str:
|
|
25
|
+
return emit_warning("EMIT005", f"Deferred target requested in current phase: {target}")
|