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,458 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from jsonschema import Draft202012Validator
6
+
7
+ from modelable.compiler.workspace import Workspace
8
+ from modelable.emitters.base import EmittedArtifact, compute_content_hash
9
+ from modelable.emitters.diagnostics import type_loss, validation_failed
10
+ from modelable.governance.por import build_por_reference
11
+ from modelable.parser.ir import (
12
+ AnnClassification,
13
+ AnnDeprecated,
14
+ AnnKey,
15
+ AnnOwner,
16
+ AnnPii,
17
+ AnnServer,
18
+ ArrayType,
19
+ ComputedMapping,
20
+ DecimalType,
21
+ DirectMapping,
22
+ DomainDef,
23
+ EnumType,
24
+ FieldDef,
25
+ MapType,
26
+ ModelVersion,
27
+ NamedType,
28
+ ObjectType,
29
+ PrimitiveType,
30
+ ProjectionField,
31
+ ProjectionVersion,
32
+ RefType,
33
+ VersionExact,
34
+ VersionMin,
35
+ VersionPinned,
36
+ VersionRange,
37
+ )
38
+ from modelable.registry.resolver import resolve_model_ref
39
+
40
+
41
+ def emit_json_schema(workspace: Workspace, out_dir: Path) -> list[EmittedArtifact]:
42
+ """Emit JSON Schema 2020-12 artifacts for every model and projection version."""
43
+ artifacts: list[EmittedArtifact] = []
44
+ for domain in workspace.mdl.domains:
45
+ for model_name, versions in domain.models.items():
46
+ for version in versions:
47
+ artifact = _emit_model_version(domain, model_name, version, out_dir)
48
+ artifacts.append(artifact)
49
+
50
+ for projection_name, versions in domain.projections.items():
51
+ for version in versions:
52
+ artifact = _emit_projection_version(domain, projection_name, version, out_dir, workspace.mdl)
53
+ artifacts.append(artifact)
54
+ return artifacts
55
+
56
+
57
+ def _artifact_id(domain: str, name: str, version: int) -> str:
58
+ return f"{domain}.{name}.v{version}"
59
+
60
+
61
+ def _emit_model_version(domain: DomainDef, model_name: str, version: ModelVersion, out_dir: Path) -> EmittedArtifact:
62
+ artifact_id = _artifact_id(domain.name, model_name, version.version)
63
+ schema: dict = {
64
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
65
+ "$id": artifact_id,
66
+ "type": "object",
67
+ "title": model_name,
68
+ "x-modelable": {
69
+ "domain": domain.name,
70
+ "name": model_name,
71
+ "kind": version.model_kind.value,
72
+ "version": version.version,
73
+ "changeKind": version.change_kind.value,
74
+ },
75
+ "x-modelable-por": build_por_reference(f"{domain.name}.{model_name}.v{version.version}"),
76
+ }
77
+ _add_domain_metadata(schema["x-modelable"], domain)
78
+
79
+ properties: dict[str, dict] = {}
80
+ required: list[str] = []
81
+ warnings: list[str] = []
82
+ defs: dict[str, dict] = {}
83
+
84
+ for field in version.fields:
85
+ prop = _field_to_json_schema(field, field.type, defs=defs, path=[field.name])
86
+ default_value = _field_default(field)
87
+ if default_value is not None:
88
+ prop["default"] = default_value
89
+ if isinstance(field.type, NamedType):
90
+ warnings.append(type_loss(field.type.name))
91
+ properties[field.name] = prop
92
+ if not field.optional:
93
+ required.append(field.name)
94
+
95
+ schema["properties"] = properties
96
+ if required:
97
+ schema["required"] = required
98
+ if defs:
99
+ schema["$defs"] = defs
100
+
101
+ path = out_dir / f"{artifact_id}.json"
102
+ artifact = EmittedArtifact(
103
+ target="json-schema",
104
+ ref=f"{domain.name}.{model_name}@{version.version}",
105
+ artifact_id=artifact_id,
106
+ path=path,
107
+ content=schema,
108
+ content_hash=compute_content_hash(schema),
109
+ warnings=warnings,
110
+ )
111
+
112
+ _validate_schema(artifact)
113
+ return artifact
114
+
115
+
116
+ def _emit_projection_version(
117
+ domain: DomainDef,
118
+ projection_name: str,
119
+ version: ProjectionVersion,
120
+ out_dir: Path,
121
+ mdl,
122
+ ) -> EmittedArtifact:
123
+ artifact_id = _artifact_id(domain.name, projection_name, version.version)
124
+ schema: dict = {
125
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
126
+ "$id": artifact_id,
127
+ "type": "object",
128
+ "title": projection_name,
129
+ "x-modelable": {
130
+ "domain": domain.name,
131
+ "name": projection_name,
132
+ "kind": "projection",
133
+ "version": version.version,
134
+ "source": {
135
+ "model": version.source.model,
136
+ "version": _version_spec_to_json(version.source.version),
137
+ "alias": version.source.alias,
138
+ },
139
+ "autoGenerated": version.auto_generated,
140
+ },
141
+ "x-modelable-por": build_por_reference(f"{domain.name}.{projection_name}.v{version.version}"),
142
+ }
143
+ _add_domain_metadata(schema["x-modelable"], domain)
144
+
145
+ if version.joins:
146
+ schema["x-modelable"]["joins"] = [
147
+ {
148
+ "model": join.model,
149
+ "version": _version_spec_to_json(join.version),
150
+ "alias": join.alias,
151
+ "on": join.on,
152
+ }
153
+ for join in version.joins
154
+ ]
155
+
156
+ if version.where:
157
+ schema["x-modelable"]["where"] = version.where
158
+
159
+ if version.group_by:
160
+ schema["x-modelable"]["groupBy"] = version.group_by
161
+
162
+ properties: dict[str, dict] = {}
163
+ required: list[str] = []
164
+ warnings: list[str] = []
165
+ defs: dict[str, dict] = {}
166
+
167
+ for field in version.fields:
168
+ field_type = _resolve_projection_field_type(field, version, mdl)
169
+ prop = _field_to_json_schema(field, field_type, defs=defs, path=[field.name])
170
+ if isinstance(field_type, NamedType):
171
+ warnings.append(type_loss(field_type.name))
172
+ _add_lineage(prop, field, version)
173
+ properties[field.name] = prop
174
+ # Projection fields are always required in the current IR (no ? syntax).
175
+ required.append(field.name)
176
+
177
+ schema["properties"] = properties
178
+ if required:
179
+ schema["required"] = required
180
+ if defs:
181
+ schema["$defs"] = defs
182
+
183
+ path = out_dir / f"{artifact_id}.json"
184
+ artifact = EmittedArtifact(
185
+ target="json-schema",
186
+ ref=f"{domain.name}.{projection_name}@{version.version}",
187
+ artifact_id=artifact_id,
188
+ path=path,
189
+ content=schema,
190
+ content_hash=compute_content_hash(schema),
191
+ warnings=warnings,
192
+ )
193
+
194
+ _validate_schema(artifact)
195
+ return artifact
196
+
197
+
198
+ def _version_spec_to_json(version_spec) -> dict:
199
+ if isinstance(version_spec, VersionExact):
200
+ return {"kind": "exact", "version": version_spec.version}
201
+ if isinstance(version_spec, VersionRange):
202
+ return {
203
+ "kind": "range",
204
+ "minInclusive": version_spec.min_inclusive,
205
+ "maxExclusive": version_spec.max_exclusive,
206
+ }
207
+ if isinstance(version_spec, VersionMin):
208
+ return {"kind": "min", "minInclusive": version_spec.min_inclusive}
209
+ if isinstance(version_spec, VersionPinned):
210
+ return {
211
+ "kind": "pinned",
212
+ "version": version_spec.version,
213
+ "contentHash": version_spec.content_hash,
214
+ }
215
+ return {}
216
+
217
+
218
+ def _field_default(field: FieldDef):
219
+ if field.default is None:
220
+ return None
221
+ raw = field.default.strip()
222
+ if isinstance(field.type, PrimitiveType):
223
+ if field.type.kind == "bool":
224
+ if raw == "true":
225
+ return True
226
+ if raw == "false":
227
+ return False
228
+ if field.type.kind in {"int"}:
229
+ try:
230
+ return int(raw)
231
+ except ValueError:
232
+ return raw
233
+ if field.type.kind in {"float"}:
234
+ try:
235
+ return float(raw)
236
+ except ValueError:
237
+ return raw
238
+ if len(raw) >= 2 and raw[0] == '"' and raw[-1] == '"':
239
+ return raw[1:-1]
240
+ return raw
241
+
242
+
243
+ def _add_domain_metadata(container: dict, domain: DomainDef) -> None:
244
+ if domain.owner is not None:
245
+ container["owner"] = domain.owner
246
+ if domain.contact is not None:
247
+ container["contact"] = domain.contact
248
+ if domain.description is not None:
249
+ container["description"] = domain.description
250
+
251
+
252
+ def _resolve_projection_field_type(
253
+ field: FieldDef,
254
+ projection: ProjectionVersion,
255
+ mdl,
256
+ ):
257
+ """Resolve the JSON Schema type for a projection field from its source model."""
258
+ if not isinstance(field.mapping, DirectMapping):
259
+ return None
260
+
261
+ # Find the source model version that matches the projection source.
262
+ source_fqn = projection.source.model
263
+ try:
264
+ source_domain, source_model = source_fqn.rsplit(".", 1)
265
+ except ValueError:
266
+ return None
267
+
268
+ try:
269
+ resolved = resolve_model_ref(mdl, f"{source_domain}.{source_model}", projection.source.version)
270
+ except LookupError:
271
+ return None
272
+ source_mv = resolved.version
273
+
274
+ for src_field in source_mv.fields:
275
+ if src_field.name == field.mapping.source_field:
276
+ return src_field.type
277
+
278
+ return None
279
+
280
+
281
+ def _field_to_json_schema(
282
+ field: FieldDef, field_type=None, defs: dict[str, dict] | None = None, path: list[str] | None = None
283
+ ) -> dict:
284
+ prop = _type_to_json_schema(field_type, defs=defs, path=path) if field_type is not None else {}
285
+
286
+ wire_targets = field.wire_targets()
287
+ if wire_targets:
288
+ prop["x-modelable-wire"] = {target: _wire_hint_to_json(hint) for target, hint in sorted(wire_targets.items())}
289
+
290
+ for ann in field.annotations:
291
+ if isinstance(ann, AnnKey):
292
+ prop["x-modelable-field"] = {**(prop.get("x-modelable-field") or {}), "key": True}
293
+ elif isinstance(ann, AnnPii):
294
+ prop["x-modelable-field"] = {**(prop.get("x-modelable-field") or {}), "pii": True}
295
+ elif isinstance(ann, AnnClassification):
296
+ prop["x-modelable-classification"] = ann.level
297
+ elif isinstance(ann, AnnDeprecated):
298
+ prop["deprecated"] = True
299
+ prop["x-modelable-field"] = {
300
+ **(prop.get("x-modelable-field") or {}),
301
+ "replacedBy": ann.replaced_by,
302
+ }
303
+ elif isinstance(ann, AnnOwner):
304
+ prop["x-modelable-field"] = {
305
+ **(prop.get("x-modelable-field") or {}),
306
+ "owner": ann.team,
307
+ }
308
+ elif isinstance(ann, AnnServer):
309
+ prop["x-modelable-field"] = {
310
+ **(prop.get("x-modelable-field") or {}),
311
+ "server": True,
312
+ }
313
+
314
+ json_hint = wire_targets.get("json")
315
+ if (
316
+ json_hint is not None
317
+ and json_hint.encoding == "string"
318
+ and (
319
+ isinstance(field_type, (DecimalType,))
320
+ or (isinstance(field_type, PrimitiveType) and field_type.kind == "int")
321
+ )
322
+ ):
323
+ prop["type"] = "string"
324
+ prop.pop("format", None)
325
+
326
+ return prop
327
+
328
+
329
+ def _type_to_json_schema(field_type, defs: dict[str, dict] | None = None, path: list[str] | None = None) -> dict:
330
+ if isinstance(field_type, PrimitiveType):
331
+ return _primitive_to_json_schema(field_type.kind)
332
+ if isinstance(field_type, DecimalType):
333
+ return {
334
+ "type": "string",
335
+ "pattern": r"^-?\d+(\.\d+)?$",
336
+ "x-modelable-field": {"decimalPrecision": field_type.precision, "decimalScale": field_type.scale},
337
+ }
338
+ if isinstance(field_type, ArrayType):
339
+ return {
340
+ "type": "array",
341
+ "items": _type_to_json_schema(field_type.item, defs=defs, path=path),
342
+ }
343
+ if isinstance(field_type, MapType):
344
+ return {
345
+ "type": "object",
346
+ "additionalProperties": _type_to_json_schema(field_type.value, defs=defs, path=path),
347
+ }
348
+ if isinstance(field_type, RefType):
349
+ return {
350
+ "type": "string",
351
+ "x-modelable-ref": field_type.target,
352
+ }
353
+ if isinstance(field_type, EnumType):
354
+ return {
355
+ "type": "string",
356
+ "enum": field_type.values,
357
+ }
358
+ if isinstance(field_type, NamedType):
359
+ def_name = _definition_name([field_type.name])
360
+ if defs is not None:
361
+ defs.setdefault(
362
+ def_name,
363
+ {
364
+ "title": field_type.name,
365
+ "type": "object",
366
+ "x-modelable-field": {"namedType": field_type.name},
367
+ },
368
+ )
369
+ return {"$ref": f"#/$defs/{def_name}"}
370
+ return {
371
+ "type": "object",
372
+ "x-modelable-field": {"namedType": field_type.name},
373
+ }
374
+ if isinstance(field_type, ObjectType):
375
+ def_name = _definition_name(path or ["InlineObject"])
376
+ if defs is not None:
377
+ defs.setdefault(def_name, _object_type_to_json_schema(field_type, defs=defs, path=path))
378
+ return {"$ref": f"#/$defs/{def_name}"}
379
+ return _object_type_to_json_schema(field_type, defs=defs, path=path)
380
+
381
+ return {"type": "object"}
382
+
383
+
384
+ def _object_type_to_json_schema(
385
+ field_type: ObjectType, defs: dict[str, dict] | None = None, path: list[str] | None = None
386
+ ) -> dict:
387
+ prop: dict = {"title": _definition_name(path or ["InlineObject"]), "type": "object"}
388
+ props: dict[str, dict] = {}
389
+ req: list[str] = []
390
+ for f in field_type.fields:
391
+ child_path = [*(path or []), f.name]
392
+ props[f.name] = _field_to_json_schema(f, f.type, defs=defs, path=child_path)
393
+ if not f.optional:
394
+ req.append(f.name)
395
+ prop["properties"] = props
396
+ if req:
397
+ prop["required"] = req
398
+ return prop
399
+
400
+
401
+ def _definition_name(path: list[str]) -> str:
402
+ return "".join(_pascalize_part(part) for part in path if part)
403
+
404
+
405
+ def _pascalize_part(value: str) -> str:
406
+ return value[:1].upper() + value[1:] if value else value
407
+
408
+
409
+ def _primitive_to_json_schema(kind: str) -> dict:
410
+ mapping: dict[str, dict] = {
411
+ "string": {"type": "string"},
412
+ "bool": {"type": "boolean"},
413
+ "int": {"type": "integer", "format": "int64"},
414
+ "float": {"type": "number"},
415
+ "uuid": {"type": "string", "format": "uuid"},
416
+ "timestamp": {"type": "string", "format": "date-time"},
417
+ "date": {"type": "string", "format": "date"},
418
+ "time": {"type": "string", "format": "time"},
419
+ "duration": {"type": "string", "format": "duration"},
420
+ "binary": {"type": "string", "contentEncoding": "base64"},
421
+ "json": {},
422
+ }
423
+ return mapping.get(kind, {"type": "string"})
424
+
425
+
426
+ def _add_lineage(prop: dict, field: ProjectionField, projection: ProjectionVersion) -> None:
427
+ mapping = field.mapping
428
+ if isinstance(mapping, DirectMapping):
429
+ prop["x-modelable-lineage"] = {
430
+ "kind": "direct",
431
+ "source": f"{mapping.source_alias}.{mapping.source_field}",
432
+ "sourceModel": projection.source.model,
433
+ }
434
+ elif isinstance(mapping, ComputedMapping):
435
+ prop["x-modelable-lineage"] = {
436
+ "kind": "computed",
437
+ "expression": mapping.expression,
438
+ }
439
+
440
+
441
+ def _validate_schema(artifact: EmittedArtifact) -> None:
442
+ try:
443
+ Draft202012Validator.check_schema(artifact.content)
444
+ except Exception as exc:
445
+ artifact.warnings.append(validation_failed(str(artifact.path), str(exc)))
446
+
447
+
448
+ def _wire_hint_to_json(hint) -> dict:
449
+ data: dict[str, object] = {}
450
+ if hint.encoding is not None:
451
+ data["encoding"] = hint.encoding
452
+ if hint.type is not None:
453
+ data["type"] = hint.type
454
+ if hint.case is not None:
455
+ data["case"] = hint.case
456
+ if hint.overrides:
457
+ data["overrides"] = {key: hint.overrides[key] for key in sorted(hint.overrides)}
458
+ return data