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,352 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from pathlib import Path
5
+
6
+ from modelable.compiler.workspace import Workspace
7
+ from modelable.emitters.base import EmittedArtifact, compute_content_hash
8
+ from modelable.emitters.diagnostics import missing_metadata, type_loss
9
+ from modelable.parser.ir import (
10
+ ArrayType,
11
+ ComputedMapping,
12
+ DecimalType,
13
+ DirectMapping,
14
+ DomainDef,
15
+ EnumType,
16
+ FieldDef,
17
+ MapType,
18
+ ModelVersion,
19
+ NamedType,
20
+ ObjectType,
21
+ PrimitiveType,
22
+ ProjectionVersion,
23
+ RefType,
24
+ VersionExact,
25
+ VersionMin,
26
+ VersionPinned,
27
+ VersionRange,
28
+ )
29
+ from modelable.registry.resolver import ResolvedModelRef, resolve_model_ref
30
+
31
+
32
+ def emit_typescript(workspace: Workspace, out_dir: Path) -> list[EmittedArtifact]:
33
+ artifacts: list[EmittedArtifact] = []
34
+ for domain in workspace.mdl.domains:
35
+ for model_name, versions in domain.models.items():
36
+ for version in versions:
37
+ artifacts.append(_emit_model(domain, model_name, version, out_dir, workspace.mdl))
38
+ for projection_name, versions in domain.projections.items():
39
+ for version in versions:
40
+ artifacts.append(_emit_projection(domain, projection_name, version, out_dir, workspace.mdl))
41
+ return artifacts
42
+
43
+
44
+ def _artifact_id(domain: str, name: str, version: int) -> str:
45
+ return f"{domain}.{name}.v{version}"
46
+
47
+
48
+ def _pascalize(value: str) -> str:
49
+ parts = [part for part in re.split(r"[^A-Za-z0-9]+", value) if part]
50
+ return "".join(part[:1].upper() + part[1:] for part in parts)
51
+
52
+
53
+ def _stable_interface_name(domain: str, name: str, version: int) -> str:
54
+ return f"{_pascalize(domain)}{_pascalize(name)}V{version}"
55
+
56
+
57
+ def _iface_to_artifact_id(iface_name: str) -> str:
58
+ """Reverse _stable_interface_name heuristically to produce an artifact id like 'address.Address.v1'."""
59
+ # Strip trailing Vn suffix
60
+ m = re.match(r"^(.+?)V(\d+)$", iface_name)
61
+ if not m:
62
+ return iface_name.lower()
63
+ body, ver = m.group(1), m.group(2)
64
+ # Re-split PascalCase token pairs into domain + model
65
+ parts = re.findall(r"[A-Z][a-z0-9]*", body)
66
+ if len(parts) >= 2:
67
+ domain = parts[0].lower()
68
+ model = "".join(parts[1:])
69
+ return f"{domain}.{model}.v{ver}"
70
+ return f"{body.lower()}.v{ver}"
71
+
72
+
73
+ def _collect_ref_imports(field_type, mdl, resolved_refs: dict[str, str]) -> None:
74
+ """Recursively collect resolved RefType targets into resolved_refs."""
75
+ if isinstance(field_type, RefType):
76
+ target = field_type.target
77
+ if target not in resolved_refs:
78
+ try:
79
+ from modelable.parser.ir import VersionMin
80
+
81
+ resolved: ResolvedModelRef = resolve_model_ref(mdl, target, VersionMin(min_inclusive=1))
82
+ iface = _stable_interface_name(resolved.domain_name, resolved.model_name, resolved.version.version)
83
+ resolved_refs[target] = iface
84
+ except LookupError, ValueError:
85
+ pass
86
+ elif isinstance(field_type, ArrayType):
87
+ _collect_ref_imports(field_type.item, mdl, resolved_refs)
88
+ elif isinstance(field_type, MapType):
89
+ _collect_ref_imports(field_type.value, mdl, resolved_refs)
90
+ elif isinstance(field_type, ObjectType):
91
+ for f in field_type.fields:
92
+ _collect_ref_imports(f.type, mdl, resolved_refs)
93
+
94
+
95
+ def _emit_model(domain: DomainDef, model_name: str, version: ModelVersion, out_dir: Path, mdl=None) -> EmittedArtifact:
96
+ artifact_id = _artifact_id(domain.name, model_name, version.version)
97
+ interface_name = _stable_interface_name(domain.name, model_name, version.version)
98
+
99
+ # Resolve ref<X> fields to stable interface names; collect imports.
100
+ resolved_refs: dict[str, str] = {} # ref target → stable interface name
101
+ if mdl is not None:
102
+ for field in version.fields:
103
+ _collect_ref_imports(field.type, mdl, resolved_refs)
104
+
105
+ declaration_json_wire = version.wire_targets().get("json")
106
+ field_case = declaration_json_wire.field_case if declaration_json_wire is not None else None
107
+
108
+ import_lines: list[str] = []
109
+ for iface in sorted(set(resolved_refs.values())):
110
+ iface_id = _iface_to_artifact_id(iface)
111
+ import_lines.append(f'import type {{ {iface} }} from "./{iface_id}";')
112
+
113
+ meta_lines = _metadata_lines(
114
+ _domain_metadata_entries(
115
+ domain,
116
+ model_name,
117
+ version.version,
118
+ version.model_kind.value,
119
+ version.change_kind.value,
120
+ )
121
+ )
122
+ body_lines: list[str] = []
123
+ body_lines.append(f"export interface {interface_name} {{")
124
+ warnings: list[str] = []
125
+ for field in version.fields:
126
+ if isinstance(field.type, NamedType):
127
+ warnings.append(missing_metadata(f"{domain.name}.{model_name}.{field.name}"))
128
+ field_name = _apply_case(field.name, field_case) if field_case else field.name
129
+ body_lines.append(
130
+ f" {field_name}{'?' if field.optional else ''}: {_type_to_ts(field.type, wire_targets=field.wire_targets(), resolved_refs=resolved_refs)};"
131
+ )
132
+ body_lines.append("}")
133
+ body_lines.append(f"export type {model_name} = {interface_name};")
134
+
135
+ all_lines = import_lines + ([""] if import_lines else []) + meta_lines + body_lines
136
+ content = "\n".join(all_lines) + "\n"
137
+ return EmittedArtifact(
138
+ target="typescript",
139
+ ref=f"{domain.name}.{model_name}@{version.version}",
140
+ artifact_id=artifact_id,
141
+ path=out_dir / f"{artifact_id}.ts",
142
+ content=content,
143
+ content_hash=compute_content_hash(content),
144
+ warnings=warnings,
145
+ )
146
+
147
+
148
+ def _emit_projection(
149
+ domain: DomainDef,
150
+ projection_name: str,
151
+ version: ProjectionVersion,
152
+ out_dir: Path,
153
+ mdl,
154
+ ) -> EmittedArtifact:
155
+ artifact_id = _artifact_id(domain.name, projection_name, version.version)
156
+ interface_name = _stable_interface_name(domain.name, projection_name, version.version)
157
+ lines = _metadata_lines(
158
+ _domain_metadata_entries(
159
+ domain,
160
+ projection_name,
161
+ version.version,
162
+ "projection",
163
+ source=f"{version.source.model}@{_version_label(version.source.version)}",
164
+ where=version.where,
165
+ group_by=", ".join(version.group_by) if version.group_by else None,
166
+ )
167
+ )
168
+ declaration_json_wire = version.wire_targets().get("json")
169
+ field_case = declaration_json_wire.field_case if declaration_json_wire is not None else None
170
+ lines.append(f"export interface {interface_name} {{")
171
+ warnings: list[str] = []
172
+ for field in version.fields:
173
+ field_type = _resolve_projection_field_type(field, version, mdl)
174
+ if field_type is None:
175
+ warnings.append(type_loss(f"{domain.name}.{projection_name}.{field.name}"))
176
+ elif isinstance(field_type, NamedType):
177
+ warnings.append(missing_metadata(f"{domain.name}.{projection_name}.{field.name}"))
178
+ field_name = _apply_case(field.name, field_case) if field_case else field.name
179
+ lines.append(f" {field_name}: {_type_to_ts(field_type, wire_targets=field.wire_targets())};")
180
+ lines.append("}")
181
+ lines.append(f"export type {projection_name} = {interface_name};")
182
+ return EmittedArtifact(
183
+ target="typescript",
184
+ ref=f"{domain.name}.{projection_name}@{version.version}",
185
+ artifact_id=artifact_id,
186
+ path=out_dir / f"{artifact_id}.ts",
187
+ content="\n".join(lines) + "\n",
188
+ content_hash=compute_content_hash("\n".join(lines) + "\n"),
189
+ warnings=warnings,
190
+ )
191
+
192
+
193
+ def _metadata_lines(entries: list[str]) -> list[str]:
194
+ lines = ["/**"]
195
+ lines.extend(f" * {entry}" for entry in entries)
196
+ lines.append(" */")
197
+ return lines
198
+
199
+
200
+ def _domain_metadata_entries(
201
+ domain: DomainDef,
202
+ name: str,
203
+ version: int,
204
+ kind: str,
205
+ change_kind: str | None = None,
206
+ source: str | None = None,
207
+ where: str | None = None,
208
+ group_by: str | None = None,
209
+ ) -> list[str]:
210
+ entries = [f"@modelable domain: {domain.name}", f"@modelable name: {name}"]
211
+ if domain.owner is not None:
212
+ entries.append(f"@modelable owner: {domain.owner}")
213
+ if domain.contact is not None:
214
+ entries.append(f"@modelable contact: {domain.contact}")
215
+ if domain.description is not None:
216
+ entries.append(f"@modelable description: {domain.description}")
217
+ if change_kind is not None:
218
+ entries.append(f"@modelable kind: {kind}")
219
+ entries.append(f"@modelable version: {version}")
220
+ entries.append(f"@modelable changeKind: {change_kind}")
221
+ else:
222
+ entries.append(f"@modelable kind: {kind}")
223
+ entries.append(f"@modelable version: {version}")
224
+ if source is not None:
225
+ entries.append(f"@modelable source: {source}")
226
+ if where is not None:
227
+ entries.append(f"@modelable where: {where}")
228
+ if group_by is not None:
229
+ entries.append(f"@modelable groupBy: {group_by}")
230
+ return entries
231
+
232
+
233
+ def _resolve_projection_field_type(
234
+ field: FieldDef,
235
+ projection: ProjectionVersion,
236
+ mdl,
237
+ ):
238
+ if not isinstance(field.mapping, DirectMapping):
239
+ return None
240
+ try:
241
+ source_domain, source_model = projection.source.model.rsplit(".", 1)
242
+ except ValueError:
243
+ return None
244
+ try:
245
+ resolved = resolve_model_ref(mdl, f"{source_domain}.{source_model}", projection.source.version)
246
+ except LookupError:
247
+ return None
248
+ source_mv = resolved.version
249
+ for src_field in source_mv.fields:
250
+ if src_field.name == field.mapping.source_field:
251
+ return src_field.type
252
+ return None
253
+
254
+
255
+ def _version_label(version_spec) -> str:
256
+ if isinstance(version_spec, VersionExact):
257
+ return str(version_spec.version)
258
+ if isinstance(version_spec, VersionRange):
259
+ return f">={version_spec.min_inclusive}<{version_spec.max_exclusive}"
260
+ if isinstance(version_spec, VersionMin):
261
+ return f">={version_spec.min_inclusive}"
262
+ if isinstance(version_spec, VersionPinned):
263
+ return f"{version_spec.version}#{version_spec.content_hash}"
264
+ return "?"
265
+
266
+
267
+ def _apply_case(value: str, case: str) -> str:
268
+ """Convert an enum value string to the specified wire case convention."""
269
+ words = re.sub(r"([a-z])([A-Z])", r"\1_\2", value)
270
+ words_list = [w for w in re.split(r"[^A-Za-z0-9]+", words) if w]
271
+ if not words_list:
272
+ return value
273
+ if case == "SCREAMING_SNAKE_CASE":
274
+ return "_".join(w.upper() for w in words_list)
275
+ if case == "snake_case":
276
+ return "_".join(w.lower() for w in words_list)
277
+ if case == "camelCase":
278
+ return words_list[0].lower() + "".join(w.capitalize() for w in words_list[1:])
279
+ if case == "PascalCase":
280
+ return "".join(w.capitalize() for w in words_list)
281
+ return value
282
+
283
+
284
+ def _type_to_ts(
285
+ field_type,
286
+ *,
287
+ wire_targets: dict[str, object] | None = None,
288
+ resolved_refs: dict[str, str] | None = None,
289
+ ) -> str:
290
+ json_wire = None
291
+ if wire_targets is not None:
292
+ json_wire = wire_targets.get("json")
293
+ if isinstance(field_type, PrimitiveType):
294
+ if (
295
+ json_wire is not None
296
+ and getattr(json_wire, "encoding", None) == "string"
297
+ and field_type.kind in {"int", "float"}
298
+ ):
299
+ return "string"
300
+ mapping = {
301
+ "string": "string",
302
+ "int": "number",
303
+ "float": "number",
304
+ "bool": "boolean",
305
+ "date": "string",
306
+ "time": "string",
307
+ "timestamp": "string",
308
+ "uuid": "string",
309
+ "duration": "string",
310
+ "binary": "string",
311
+ "json": "unknown",
312
+ }
313
+ return mapping.get(field_type.kind, "unknown")
314
+ if isinstance(field_type, DecimalType):
315
+ return "string"
316
+ if isinstance(field_type, ArrayType):
317
+ item_ts = _type_to_ts(field_type.item)
318
+ if isinstance(field_type.item, EnumType):
319
+ return f"({item_ts})[]"
320
+ return f"{item_ts}[]"
321
+ if isinstance(field_type, MapType):
322
+ return f"Record<string, {_type_to_ts(field_type.value)}>"
323
+ if isinstance(field_type, RefType):
324
+ if resolved_refs and field_type.target in resolved_refs:
325
+ return resolved_refs[field_type.target]
326
+ return "string"
327
+ if isinstance(field_type, EnumType):
328
+ case = getattr(json_wire, "case", None) if json_wire is not None else None
329
+ overrides = getattr(json_wire, "overrides", {}) if json_wire is not None else {}
330
+ wire_values = []
331
+ for v in field_type.values:
332
+ if v in overrides:
333
+ wire_values.append(overrides[v])
334
+ elif case:
335
+ wire_values.append(_apply_case(v, case))
336
+ else:
337
+ wire_values.append(v)
338
+ values = " | ".join(repr(v) for v in wire_values)
339
+ return values or "string"
340
+ if isinstance(field_type, ObjectType):
341
+ inner = "; ".join(
342
+ f"{field.name}{'?' if field.optional else ''}: {_type_to_ts(field.type, wire_targets=field.wire_targets(), resolved_refs=resolved_refs)}"
343
+ for field in field_type.fields
344
+ )
345
+ return f"{{ {inner} }}"
346
+ if isinstance(field_type, NamedType):
347
+ return field_type.name
348
+ if field_type is None:
349
+ return "unknown"
350
+ if isinstance(field_type, ComputedMapping):
351
+ return "unknown"
352
+ return "unknown"
File without changes