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,261 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from pathlib import Path
5
+
6
+ from modelable.diagnostics.model import Diagnostic
7
+ from modelable.lsp.workspace import LspWorkspaceIndex
8
+ from modelable.parser.parse import parse_text_to_ir
9
+ from modelable.registry.signature import compute_version_signature
10
+
11
+ _PINNED_IMPORT_PATTERN = re.compile(
12
+ r"^\s*import\s+domain\s+(?P<domain>[A-Za-z_][A-Za-z0-9_-]*)\s+"
13
+ r"from\s+registry\s+\"(?P<peer>[^\"]+)\""
14
+ r"(?:\s+at\s+(?P<ref_domain>[A-Za-z_][A-Za-z0-9_-]*)\."
15
+ r"(?P<ref_model>[A-Za-z_][A-Za-z0-9_-]*)\s*@\s*"
16
+ r"(?P<version>\d+)\#(?P<signature>[0-9a-fA-F]+))?"
17
+ )
18
+ _PINNED_REFERENCE_PATTERN = re.compile(
19
+ r"^\s*from\s+(?P<domain>[A-Za-z_][A-Za-z0-9_-]*)\."
20
+ r"(?P<model>[A-Za-z_][A-Za-z0-9_-]*)\s*@\s*"
21
+ r"(?P<version>\d+)\#(?P<signature>[0-9a-fA-F]+)\s+as\s+"
22
+ r"(?P<alias>[A-Za-z_][A-Za-z0-9_-]*)"
23
+ )
24
+ _PEERS_BLOCK_PATTERN = re.compile(r"peers\s*:\s*\[(?P<body>.*?)\]", re.DOTALL)
25
+ _PEER_ID_PATTERN = re.compile(r"id\s*:\s*\"(?P<id>[^\"]+)\"")
26
+
27
+
28
+ def build_import_diagnostics(index: LspWorkspaceIndex, uri: str) -> list[Diagnostic]:
29
+ source = index.documents.get(uri)
30
+ if source is None:
31
+ return []
32
+
33
+ diagnostics: list[Diagnostic] = []
34
+ declared_peers = _declared_peer_ids(index)
35
+ mirror_domains = set(mirror_domain_names(index))
36
+ mirror_signatures = _mirror_model_signatures(index)
37
+
38
+ for line_no, line in enumerate(source.text.splitlines()):
39
+ match = _PINNED_IMPORT_PATTERN.match(line)
40
+ if match is None:
41
+ match = _PINNED_REFERENCE_PATTERN.match(line)
42
+ if match is None:
43
+ continue
44
+
45
+ domain_name = match.group("domain")
46
+ model_name = match.group("model")
47
+ version = int(match.group("version"))
48
+ signature = match.group("signature")
49
+ actual_signature = mirror_signatures.get((domain_name, model_name, version))
50
+ if actual_signature is None:
51
+ diagnostics.append(
52
+ Diagnostic(
53
+ code="FED",
54
+ message=(
55
+ f"pinned reference '{domain_name}.{model_name}@{version}#{signature}' "
56
+ "is not available in the local mirror cache"
57
+ ),
58
+ severity="error",
59
+ path=uri,
60
+ line=line_no + 1,
61
+ column=match.start("signature") + 1,
62
+ end_line=line_no + 1,
63
+ end_column=match.end("signature") + 1,
64
+ )
65
+ )
66
+ continue
67
+ if signature.lower() != actual_signature.lower():
68
+ diagnostics.append(
69
+ Diagnostic(
70
+ code="FED",
71
+ message=(
72
+ f"pinned reference '{domain_name}.{model_name}@{version}#{signature}' "
73
+ f"does not match local mirror signature '{actual_signature}'"
74
+ ),
75
+ severity="error",
76
+ path=uri,
77
+ line=line_no + 1,
78
+ column=match.start("signature") + 1,
79
+ end_line=line_no + 1,
80
+ end_column=match.end("signature") + 1,
81
+ )
82
+ )
83
+ continue
84
+
85
+ domain_name = match.group("domain")
86
+ peer_id = match.group("peer")
87
+
88
+ if declared_peers is not None and peer_id not in declared_peers:
89
+ diagnostics.append(
90
+ Diagnostic(
91
+ code="FED",
92
+ message=f"import peer '{peer_id}' is not declared in workspace.mdl",
93
+ severity="warning",
94
+ path=uri,
95
+ line=line_no + 1,
96
+ column=match.start("peer") + 1,
97
+ end_line=line_no + 1,
98
+ end_column=match.end("peer") + 1,
99
+ )
100
+ )
101
+
102
+ if domain_name not in mirror_domains:
103
+ diagnostics.append(
104
+ Diagnostic(
105
+ code="FED",
106
+ message=f"import domain '{domain_name}' is not available in the local mirror cache",
107
+ severity="error",
108
+ path=uri,
109
+ line=line_no + 1,
110
+ column=match.start("domain") + 1,
111
+ end_line=line_no + 1,
112
+ end_column=match.end("domain") + 1,
113
+ )
114
+ )
115
+
116
+ ref_domain = match.group("ref_domain")
117
+ if ref_domain is not None:
118
+ ref_model = match.group("ref_model")
119
+ version = int(match.group("version"))
120
+ signature = match.group("signature")
121
+ actual_signature = mirror_signatures.get((ref_domain, ref_model, version))
122
+ if actual_signature is None:
123
+ diagnostics.append(
124
+ Diagnostic(
125
+ code="FED",
126
+ message=(
127
+ f"pinned reference '{ref_domain}.{ref_model}@{version}#{signature}' "
128
+ "is not available in the local mirror cache"
129
+ ),
130
+ severity="error",
131
+ path=uri,
132
+ line=line_no + 1,
133
+ column=match.start("signature") + 1,
134
+ end_line=line_no + 1,
135
+ end_column=match.end("signature") + 1,
136
+ )
137
+ )
138
+ continue
139
+ if signature.lower() != actual_signature.lower():
140
+ diagnostics.append(
141
+ Diagnostic(
142
+ code="FED",
143
+ message=(
144
+ f"pinned reference '{ref_domain}.{ref_model}@{version}#{signature}' "
145
+ f"does not match local mirror signature '{actual_signature}'"
146
+ ),
147
+ severity="error",
148
+ path=uri,
149
+ line=line_no + 1,
150
+ column=match.start("signature") + 1,
151
+ end_line=line_no + 1,
152
+ end_column=match.end("signature") + 1,
153
+ )
154
+ )
155
+
156
+ return diagnostics
157
+
158
+
159
+ def mirror_domain_names(index: LspWorkspaceIndex) -> list[str]:
160
+ names: set[str] = set()
161
+ for source in _mirror_sources(index):
162
+ for domain in source.domains:
163
+ names.add(domain.name)
164
+ return sorted(names)
165
+
166
+
167
+ def mirror_reference_names(index: LspWorkspaceIndex) -> list[tuple[str, str]]:
168
+ names: set[tuple[str, str]] = set()
169
+ for source in _mirror_sources(index):
170
+ for domain in source.domains:
171
+ for model_name in domain.models:
172
+ names.add((domain.name, model_name))
173
+ for projection_name in domain.projections:
174
+ names.add((domain.name, projection_name))
175
+ return sorted(names)
176
+
177
+
178
+ def mirror_model_versions(index: LspWorkspaceIndex) -> list[tuple[str, str, int]]:
179
+ versions: set[tuple[str, str, int]] = set()
180
+ for source in _mirror_sources(index):
181
+ for domain in source.domains:
182
+ for model_name, model_versions in domain.models.items():
183
+ for version in model_versions:
184
+ versions.add((domain.name, model_name, version.version))
185
+ return sorted(versions)
186
+
187
+
188
+ def mirror_field_names(index: LspWorkspaceIndex, domain_name: str, model_name: str, version: int) -> list[str]:
189
+ names: set[str] = set()
190
+ for source in _mirror_sources(index):
191
+ for domain in source.domains:
192
+ if domain.name != domain_name:
193
+ continue
194
+ versions = domain.models.get(model_name)
195
+ if versions is None:
196
+ continue
197
+ for item in versions:
198
+ if item.version != version:
199
+ continue
200
+ for field in item.fields:
201
+ names.add(field.name)
202
+ return sorted(names)
203
+
204
+
205
+ def _declared_peer_ids(index: LspWorkspaceIndex) -> set[str] | None:
206
+ workspace_text = None
207
+ for source in index.documents.values():
208
+ if source.path is not None and source.path.name == "workspace.mdl":
209
+ workspace_text = source.text
210
+ break
211
+ if workspace_text is None:
212
+ return None
213
+
214
+ match = _PEERS_BLOCK_PATTERN.search(workspace_text)
215
+ if match is None:
216
+ return set()
217
+
218
+ return {item.group("id") for item in _PEER_ID_PATTERN.finditer(match.group("body"))}
219
+
220
+
221
+ def _mirror_sources(index: LspWorkspaceIndex):
222
+ root = _workspace_root(index)
223
+ if root is None:
224
+ return []
225
+
226
+ mirror_root = root / ".modelable" / "mirror"
227
+ if not mirror_root.exists():
228
+ return []
229
+
230
+ parsed_sources = []
231
+ for path in sorted(mirror_root.rglob("*.mdl"), key=lambda item: item.as_posix()):
232
+ try:
233
+ parsed_sources.append(parse_text_to_ir(path.read_text(encoding="utf-8"), path=path))
234
+ except Exception:
235
+ continue
236
+ return parsed_sources
237
+
238
+
239
+ def _mirror_model_signatures(index: LspWorkspaceIndex) -> dict[tuple[str, str, int], str]:
240
+ signatures: dict[tuple[str, str, int], str] = {}
241
+ for source in _mirror_sources(index):
242
+ for domain in source.domains:
243
+ for model_name, versions in domain.models.items():
244
+ for version in versions:
245
+ signatures[(domain.name, model_name, version.version)] = compute_version_signature(
246
+ domain.name,
247
+ model_name,
248
+ version,
249
+ )
250
+ return signatures
251
+
252
+
253
+ def _workspace_root(index: LspWorkspaceIndex) -> Path | None:
254
+ paths = [source.path for source in index.documents.values() if source.path is not None]
255
+ if not paths:
256
+ return None
257
+
258
+ workspace_files = [path for path in paths if path.name == "workspace.mdl"]
259
+ if workspace_files:
260
+ return workspace_files[0].parent
261
+ return paths[0].parent
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from lsprotocol import types
4
+
5
+ from modelable.lsp.workspace import LspWorkspaceIndex
6
+
7
+
8
+ def build_folding_ranges(index: LspWorkspaceIndex, uri: str) -> list[types.FoldingRange] | None:
9
+ source = index.documents.get(uri)
10
+ if source is None:
11
+ return None
12
+ return _compute_folding_ranges(source.text)
13
+
14
+
15
+ def _compute_folding_ranges(text: str) -> list[types.FoldingRange]:
16
+ ranges: list[types.FoldingRange] = []
17
+ lines = text.splitlines()
18
+ stack: list[int] = []
19
+ for line_no, line in enumerate(lines):
20
+ for ch in line:
21
+ if ch == "{":
22
+ stack.append(line_no)
23
+ elif ch == "}" and stack:
24
+ start = stack.pop()
25
+ if start != line_no:
26
+ ranges.append(
27
+ types.FoldingRange(
28
+ start_line=start,
29
+ end_line=line_no,
30
+ kind=types.FoldingRangeKind.Region,
31
+ )
32
+ )
33
+ return ranges
@@ -0,0 +1,64 @@
1
+ from __future__ import annotations
2
+
3
+ from lsprotocol import types
4
+
5
+ from modelable.lsp.workspace import LspWorkspaceIndex
6
+
7
+
8
+ def build_document_formatting(
9
+ index: LspWorkspaceIndex,
10
+ uri: str,
11
+ tab_size: int,
12
+ insert_spaces: bool,
13
+ ) -> list[types.TextEdit] | None:
14
+ source = index.documents.get(uri)
15
+ if source is None:
16
+ return None
17
+
18
+ formatted = _format_text(source.text, tab_size=max(tab_size, 1), insert_spaces=insert_spaces)
19
+ if formatted == source.text:
20
+ return []
21
+
22
+ source_lines = source.text.splitlines()
23
+ line_count = max(len(source_lines), 1)
24
+ end_line = line_count - 1
25
+ end_character = len(source_lines[-1]) if source_lines else 0
26
+ return [
27
+ types.TextEdit(
28
+ range=types.Range(
29
+ start=types.Position(line=0, character=0),
30
+ end=types.Position(line=end_line, character=end_character),
31
+ ),
32
+ new_text=formatted,
33
+ )
34
+ ]
35
+
36
+
37
+ def _format_text(text: str, tab_size: int, insert_spaces: bool) -> str:
38
+ had_trailing_newline = text.endswith("\n")
39
+ lines = text.splitlines()
40
+ if not lines:
41
+ return text
42
+
43
+ indent_unit = " " * tab_size if insert_spaces else "\t"
44
+ depth = 0
45
+ formatted_lines: list[str] = []
46
+
47
+ for line in lines:
48
+ stripped = line.strip()
49
+ if not stripped:
50
+ formatted_lines.append("")
51
+ continue
52
+
53
+ leading_close = stripped.startswith("}")
54
+ indent_depth = max(depth - 1, 0) if leading_close else depth
55
+ formatted_lines.append(f"{indent_unit * indent_depth}{stripped}")
56
+
57
+ open_count = stripped.count("{")
58
+ close_count = stripped.count("}")
59
+ depth = max(depth + open_count - close_count, 0)
60
+
61
+ formatted = "\n".join(formatted_lines)
62
+ if had_trailing_newline:
63
+ formatted += "\n"
64
+ return formatted
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from lsprotocol import types
4
+
5
+ from modelable.lsp.references import build_references
6
+ from modelable.lsp.workspace import LspWorkspaceIndex
7
+
8
+
9
+ def build_document_highlight(
10
+ index: LspWorkspaceIndex,
11
+ uri: str,
12
+ line: int,
13
+ character: int,
14
+ ) -> list[types.DocumentHighlight] | None:
15
+ all_refs = build_references(index, uri, line, character, include_declaration=True)
16
+ if all_refs is None:
17
+ return None
18
+
19
+ usage_refs = build_references(index, uri, line, character, include_declaration=False) or []
20
+ usage_keys = {(loc.range.start.line, loc.range.start.character) for loc in usage_refs if loc.uri == uri}
21
+
22
+ highlights: list[types.DocumentHighlight] = []
23
+ for loc in all_refs:
24
+ if loc.uri != uri:
25
+ continue
26
+ key = (loc.range.start.line, loc.range.start.character)
27
+ kind = types.DocumentHighlightKind.Read if key in usage_keys else types.DocumentHighlightKind.Write
28
+ highlights.append(types.DocumentHighlight(range=loc.range, kind=kind))
29
+
30
+ return highlights if highlights else None