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,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}")