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,179 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+ from modelable.compat.diff import FieldChange, compare_model_versions
6
+ from modelable.parser.ir import ChangeKind, DirectMapping, MdlFile, ModelVersion
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class CompatibilityReport:
11
+ domain_name: str
12
+ model_name: str
13
+ from_version: int
14
+ to_version: int
15
+ status: str
16
+ findings: list[str] = field(default_factory=list)
17
+ changes: list[FieldChange] = field(default_factory=list)
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class ProjectionImpact:
22
+ domain_name: str
23
+ projection_name: str
24
+ version: int
25
+ status: str # "broken", "affected", "compatible"
26
+ reason: str | None = None
27
+
28
+
29
+ def check_model_version_compatibility(
30
+ mdl: MdlFile,
31
+ domain_name: str,
32
+ model_name: str,
33
+ from_version: int,
34
+ to_version: int,
35
+ ) -> CompatibilityReport:
36
+ """Compare two published versions and classify the change set."""
37
+ old_version = _find_version(mdl, domain_name, model_name, from_version)
38
+ new_version = _find_version(mdl, domain_name, model_name, to_version)
39
+ changes = compare_model_versions(old_version, new_version)
40
+ findings = [_format_finding(change) for change in changes]
41
+ status = "breaking" if _has_breaking_change(changes, new_version) else "compatible"
42
+ return CompatibilityReport(
43
+ domain_name=domain_name,
44
+ model_name=model_name,
45
+ from_version=from_version,
46
+ to_version=to_version,
47
+ status=status,
48
+ findings=findings,
49
+ changes=changes,
50
+ )
51
+
52
+
53
+ def analyze_impact(
54
+ mdl: MdlFile,
55
+ report: CompatibilityReport,
56
+ dependent: tuple[str, str, int],
57
+ ) -> ProjectionImpact:
58
+ """Analyze the impact of a model change on a specific downstream projection."""
59
+ dep_domain_name, dep_proj_name, dep_version = dependent
60
+
61
+ # Find the projection version
62
+ pv = None
63
+ for domain in mdl.domains:
64
+ if domain.name == dep_domain_name:
65
+ pv = next(
66
+ (v for v in domain.projections.get(dep_proj_name, []) if v.version == dep_version),
67
+ None,
68
+ )
69
+ break
70
+
71
+ if pv is None:
72
+ return ProjectionImpact(dep_domain_name, dep_proj_name, dep_version, "affected", "unresolved projection")
73
+
74
+ # Determine the alias(es) used for this source model
75
+ source_ref = f"{report.domain_name}.{report.model_name}"
76
+ aliases = []
77
+ if pv.source.model == source_ref:
78
+ aliases.append(pv.source.alias)
79
+ for join in pv.joins:
80
+ if join.model == source_ref:
81
+ aliases.append(join.alias)
82
+
83
+ # Check if any breaking field change affects this projection's direct mappings
84
+ impacted_fields = []
85
+ for change in report.changes:
86
+ is_breaking_field = change.kind in {
87
+ "removed_field",
88
+ "renamed_field",
89
+ "type_changed",
90
+ "enum_changed",
91
+ "identity_changed",
92
+ }
93
+ if change.kind == "added_field" and change.to_optional is False:
94
+ is_breaking_field = True
95
+
96
+ if not is_breaking_field:
97
+ continue
98
+
99
+ for proj_field in pv.fields:
100
+ if (
101
+ isinstance(proj_field.mapping, DirectMapping)
102
+ and proj_field.mapping.source_alias in aliases
103
+ and proj_field.mapping.source_field == change.field_name
104
+ ):
105
+ impacted_fields.append(f"field '{change.field_name}' ({change.kind})")
106
+ break
107
+
108
+ if impacted_fields:
109
+ return ProjectionImpact(
110
+ dep_domain_name,
111
+ dep_proj_name,
112
+ dep_version,
113
+ "broken",
114
+ f"uses {', '.join(impacted_fields)}",
115
+ )
116
+
117
+ if report.status == "breaking":
118
+ return ProjectionImpact(
119
+ dep_domain_name,
120
+ dep_proj_name,
121
+ dep_version,
122
+ "affected",
123
+ f"source {source_ref} is marked breaking",
124
+ )
125
+
126
+ return ProjectionImpact(dep_domain_name, dep_proj_name, dep_version, "compatible")
127
+
128
+
129
+ def _find_version(
130
+ mdl: MdlFile,
131
+ domain_name: str,
132
+ model_name: str,
133
+ version: int,
134
+ ) -> ModelVersion:
135
+ for domain in mdl.domains:
136
+ if domain.name != domain_name:
137
+ continue
138
+ versions = domain.models.get(model_name, [])
139
+ for candidate in versions:
140
+ if candidate.version == version:
141
+ return candidate
142
+ break
143
+ raise LookupError(f"unknown model version {domain_name}.{model_name}@{version}")
144
+
145
+
146
+ def _format_finding(change: FieldChange) -> str:
147
+ if change.kind == "renamed_field":
148
+ return f"renamed_field {change.field_name} -> {change.replacement}"
149
+ if change.kind == "nullability_changed":
150
+ return (
151
+ f"nullability_changed {change.field_name}: "
152
+ f"{_bool_word(change.from_optional)} -> {_bool_word(change.to_optional)}"
153
+ )
154
+ if change.kind == "identity_changed":
155
+ return f"identity_changed {change.field_name}"
156
+ if change.kind == "enum_changed":
157
+ return f"enum_changed {change.field_name}"
158
+ if change.kind == "type_changed":
159
+ return f"type_changed {change.field_name}"
160
+ if change.kind == "removed_field":
161
+ return f"removed_field {change.field_name}"
162
+ if change.kind == "added_field":
163
+ return f"added_field {change.field_name}"
164
+ return f"{change.kind} {change.field_name}"
165
+
166
+
167
+ def _bool_word(value: bool | None) -> str:
168
+ if value is None:
169
+ return "unknown"
170
+ return "optional" if value else "required"
171
+
172
+
173
+ def _has_breaking_change(changes: list[FieldChange], new_version: ModelVersion) -> bool:
174
+ for change in changes:
175
+ if change.kind in {"removed_field", "renamed_field", "type_changed", "enum_changed", "identity_changed"}:
176
+ return True
177
+ if change.kind == "added_field" and change.to_optional is False:
178
+ return True
179
+ return new_version.change_kind == ChangeKind.breaking
@@ -0,0 +1,169 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import dataclass
5
+
6
+ from modelable.parser.ir import AnnDeprecated, EnumType, FieldDef, ModelVersion
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class FieldChange:
11
+ kind: str
12
+ field_name: str
13
+ previous_name: str | None = None
14
+ replacement: str | None = None
15
+ from_optional: bool | None = None
16
+ to_optional: bool | None = None
17
+ from_type: str | None = None
18
+ to_type: str | None = None
19
+
20
+
21
+ def compare_model_versions(old_version: ModelVersion, new_version: ModelVersion) -> list[FieldChange]:
22
+ """Compare two published model versions field by field."""
23
+ changes: list[FieldChange] = []
24
+ old_fields = {field.name: field for field in old_version.fields}
25
+ new_fields = {field.name: field for field in new_version.fields}
26
+ matched_old: set[str] = set()
27
+ matched_new: set[str] = set()
28
+
29
+ for old_field in old_version.fields:
30
+ replacement = _deprecated_replacement(old_field)
31
+ if replacement is None:
32
+ continue
33
+ new_field = new_fields.get(replacement)
34
+ if new_field is None or replacement in matched_new:
35
+ continue
36
+ changes.append(
37
+ FieldChange(
38
+ kind="renamed_field",
39
+ field_name=old_field.name,
40
+ previous_name=old_field.name,
41
+ replacement=replacement,
42
+ from_optional=old_field.optional,
43
+ to_optional=new_field.optional,
44
+ from_type=_type_signature(old_field),
45
+ to_type=_type_signature(new_field),
46
+ )
47
+ )
48
+ if old_field.optional != new_field.optional:
49
+ changes.append(
50
+ FieldChange(
51
+ kind="nullability_changed",
52
+ field_name=replacement,
53
+ from_optional=old_field.optional,
54
+ to_optional=new_field.optional,
55
+ from_type=_type_signature(old_field),
56
+ to_type=_type_signature(new_field),
57
+ )
58
+ )
59
+ if _type_signature(old_field) != _type_signature(new_field):
60
+ if isinstance(old_field.type, EnumType) and isinstance(new_field.type, EnumType):
61
+ kind = "enum_changed"
62
+ else:
63
+ kind = "type_changed"
64
+ changes.append(
65
+ FieldChange(
66
+ kind=kind,
67
+ field_name=replacement,
68
+ from_type=_type_signature(old_field),
69
+ to_type=_type_signature(new_field),
70
+ )
71
+ )
72
+ matched_old.add(old_field.name)
73
+ matched_new.add(replacement)
74
+
75
+ for old_field in old_version.fields:
76
+ if old_field.name in matched_old:
77
+ continue
78
+ if old_field.name not in new_fields:
79
+ changes.append(
80
+ FieldChange(
81
+ kind="removed_field",
82
+ field_name=old_field.name,
83
+ from_optional=old_field.optional,
84
+ from_type=_type_signature(old_field),
85
+ )
86
+ )
87
+
88
+ for name in _sorted_common_field_names(old_fields, new_fields):
89
+ old_field = old_fields[name]
90
+ new_field = new_fields[name]
91
+
92
+ if old_field.optional != new_field.optional:
93
+ changes.append(
94
+ FieldChange(
95
+ kind="nullability_changed",
96
+ field_name=name,
97
+ from_optional=old_field.optional,
98
+ to_optional=new_field.optional,
99
+ from_type=_type_signature(old_field),
100
+ to_type=_type_signature(new_field),
101
+ )
102
+ )
103
+
104
+ if old_field.is_key != new_field.is_key:
105
+ changes.append(
106
+ FieldChange(
107
+ kind="identity_changed",
108
+ field_name=name,
109
+ from_type=_type_signature(old_field),
110
+ to_type=_type_signature(new_field),
111
+ )
112
+ )
113
+
114
+ old_sig = _type_signature(old_field)
115
+ new_sig = _type_signature(new_field)
116
+ if old_sig == new_sig:
117
+ continue
118
+ if isinstance(old_field.type, EnumType) and isinstance(new_field.type, EnumType):
119
+ changes.append(
120
+ FieldChange(
121
+ kind="enum_changed",
122
+ field_name=name,
123
+ from_type=old_sig,
124
+ to_type=new_sig,
125
+ )
126
+ )
127
+ else:
128
+ changes.append(
129
+ FieldChange(
130
+ kind="type_changed",
131
+ field_name=name,
132
+ from_type=old_sig,
133
+ to_type=new_sig,
134
+ )
135
+ )
136
+
137
+ for new_field in new_version.fields:
138
+ if new_field.name in matched_new:
139
+ continue
140
+ if new_field.name not in old_fields:
141
+ changes.append(
142
+ FieldChange(
143
+ kind="added_field",
144
+ field_name=new_field.name,
145
+ to_optional=new_field.optional,
146
+ to_type=_type_signature(new_field),
147
+ )
148
+ )
149
+
150
+ return changes
151
+
152
+
153
+ def _sorted_common_field_names(
154
+ old_fields: dict[str, FieldDef],
155
+ new_fields: dict[str, FieldDef],
156
+ ) -> list[str]:
157
+ names = [field.name for field in old_fields.values() if field.name in new_fields]
158
+ return names
159
+
160
+
161
+ def _deprecated_replacement(field: FieldDef) -> str | None:
162
+ for annotation in field.annotations:
163
+ if isinstance(annotation, AnnDeprecated):
164
+ return annotation.replaced_by
165
+ return None
166
+
167
+
168
+ def _type_signature(field: FieldDef) -> str:
169
+ return json.dumps(field.type.model_dump(mode="json"), sort_keys=True)
@@ -0,0 +1,3 @@
1
+ from modelable.compiler.compiler import compile_file, compile_text
2
+
3
+ __all__ = ["compile_file", "compile_text"]
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from modelable.parser.ir import MdlFile
6
+ from modelable.parser.parse import parse_file_to_ir, parse_text_to_ir
7
+ from modelable.validation.semantic import validate
8
+
9
+
10
+ def compile_text(text: str) -> tuple[MdlFile, list[str]]:
11
+ """Parse and validate .mdl text."""
12
+ mdl = parse_text_to_ir(text)
13
+ return mdl, validate(mdl)
14
+
15
+
16
+ def compile_file(path: str | Path) -> tuple[MdlFile, list[str]]:
17
+ """Parse and validate a .mdl file."""
18
+ mdl = parse_file_to_ir(path)
19
+ return mdl, validate(mdl)