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
modelable/parser/ir.py ADDED
@@ -0,0 +1,451 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import StrEnum
4
+ from typing import Annotated, Literal
5
+
6
+ from pydantic import BaseModel, Field, model_validator
7
+
8
+ from modelable.diagnostics.model import Diagnostic
9
+
10
+
11
+ class ParseError(Exception):
12
+ """Raised when .mdl input cannot be parsed."""
13
+
14
+ def __init__(
15
+ self,
16
+ message: str,
17
+ *,
18
+ path: str | None = None,
19
+ line: int | None = None,
20
+ column: int | None = None,
21
+ end_line: int | None = None,
22
+ end_column: int | None = None,
23
+ ) -> None:
24
+ self.message = message
25
+ self.path = path
26
+ self.line = line
27
+ self.column = column
28
+ self.end_line = end_line
29
+ self.end_column = end_column
30
+ super().__init__(message)
31
+
32
+ def diagnostic(self, path: str | None = None) -> Diagnostic:
33
+ return Diagnostic(
34
+ code="PARSE",
35
+ message=self.message,
36
+ severity="error",
37
+ path=str(path or self.path or "<input>"),
38
+ line=self.line,
39
+ column=self.column,
40
+ end_line=self.end_line,
41
+ end_column=self.end_column,
42
+ )
43
+
44
+
45
+ class ValidationError(Exception):
46
+ """Raised when .mdl input parses but fails semantic validation."""
47
+
48
+ def __init__(self, errors: list[str]):
49
+ self.errors = errors
50
+ super().__init__("\n".join(errors))
51
+
52
+
53
+ class AnnKey(BaseModel):
54
+ kind: Literal["key"] = "key"
55
+
56
+
57
+ class AnnPii(BaseModel):
58
+ kind: Literal["pii"] = "pii"
59
+
60
+
61
+ class ClassificationLevel(StrEnum):
62
+ open = "open"
63
+ internal = "internal"
64
+ confidential = "confidential"
65
+ restricted = "restricted"
66
+ secret = "secret"
67
+
68
+
69
+ class AnnClassification(BaseModel):
70
+ kind: Literal["classification"] = "classification"
71
+ level: str
72
+
73
+
74
+ class AnnDeprecated(BaseModel):
75
+ kind: Literal["deprecated"] = "deprecated"
76
+ replaced_by: str
77
+
78
+
79
+ class AnnOwner(BaseModel):
80
+ kind: Literal["owner"] = "owner"
81
+ team: str
82
+
83
+
84
+ class AnnServer(BaseModel):
85
+ kind: Literal["server"] = "server"
86
+
87
+
88
+ class WireTargetHint(BaseModel):
89
+ encoding: str | None = None
90
+ type: str | None = None
91
+ case: str | None = None
92
+ overrides: dict[str, str] = Field(default_factory=dict)
93
+ field_case: str | None = None
94
+
95
+
96
+ class AnnWire(BaseModel):
97
+ kind: Literal["wire"] = "wire"
98
+ targets: dict[str, WireTargetHint] = Field(default_factory=dict)
99
+
100
+ @model_validator(mode="after")
101
+ def _validate_targets(self) -> AnnWire:
102
+ if not self.targets:
103
+ raise ValueError("wire annotations must declare at least one target")
104
+ for target, hint in self.targets.items():
105
+ if (
106
+ hint.encoding is None
107
+ and hint.type is None
108
+ and hint.case is None
109
+ and not hint.overrides
110
+ and hint.field_case is None
111
+ ):
112
+ raise ValueError(f"wire target '{target}' must define at least one option")
113
+ return self
114
+
115
+
116
+ class AnnPitCutoff(BaseModel):
117
+ kind: Literal["pit_cutoff"] = "pit_cutoff"
118
+ expression: str
119
+
120
+
121
+ class AnnLatestBefore(BaseModel):
122
+ kind: Literal["latest_before"] = "latest_before"
123
+ expression: str
124
+
125
+
126
+ class AnnLatestOnly(BaseModel):
127
+ kind: Literal["latest_only"] = "latest_only"
128
+
129
+
130
+ class AnnCustom(BaseModel):
131
+ kind: Literal["custom"] = "custom"
132
+ name: str
133
+ expression: str | None = None
134
+
135
+
136
+ Annotation = Annotated[
137
+ AnnKey
138
+ | AnnPii
139
+ | AnnClassification
140
+ | AnnDeprecated
141
+ | AnnOwner
142
+ | AnnServer
143
+ | AnnWire
144
+ | AnnPitCutoff
145
+ | AnnLatestBefore
146
+ | AnnLatestOnly
147
+ | AnnCustom,
148
+ Field(discriminator="kind"),
149
+ ]
150
+
151
+
152
+ class PrimitiveType(BaseModel):
153
+ kind: Literal[
154
+ "string",
155
+ "int",
156
+ "float",
157
+ "bool",
158
+ "date",
159
+ "time",
160
+ "timestamp",
161
+ "uuid",
162
+ "duration",
163
+ "binary",
164
+ "json",
165
+ ]
166
+
167
+
168
+ class DecimalType(BaseModel):
169
+ kind: Literal["decimal"] = "decimal"
170
+ precision: int
171
+ scale: int
172
+
173
+
174
+ class ArrayType(BaseModel):
175
+ kind: Literal["array"] = "array"
176
+ item: FieldType
177
+
178
+
179
+ class MapType(BaseModel):
180
+ kind: Literal["map"] = "map"
181
+ key: FieldType
182
+ value: FieldType
183
+
184
+
185
+ class RefType(BaseModel):
186
+ kind: Literal["ref"] = "ref"
187
+ target: str
188
+
189
+
190
+ class EnumType(BaseModel):
191
+ kind: Literal["enum"] = "enum"
192
+ values: list[str]
193
+
194
+
195
+ class ObjectType(BaseModel):
196
+ kind: Literal["object"] = "object"
197
+ fields: list[FieldDef]
198
+
199
+
200
+ class NamedType(BaseModel):
201
+ kind: Literal["named"] = "named"
202
+ name: str
203
+
204
+
205
+ FieldType = Annotated[
206
+ PrimitiveType | DecimalType | ArrayType | MapType | RefType | EnumType | ObjectType | NamedType,
207
+ Field(discriminator="kind"),
208
+ ]
209
+
210
+
211
+ class FieldDef(BaseModel):
212
+ name: str
213
+ type: FieldType
214
+ optional: bool = False
215
+ default: str | None = None
216
+ annotations: list[Annotation] = Field(default_factory=list)
217
+
218
+ @property
219
+ def is_key(self) -> bool:
220
+ return any(annotation.kind == "key" for annotation in self.annotations)
221
+
222
+ @property
223
+ def is_pii(self) -> bool:
224
+ return any(annotation.kind == "pii" for annotation in self.annotations)
225
+
226
+ @property
227
+ def classification(self) -> ClassificationLevel | None:
228
+ for annotation in self.annotations:
229
+ if annotation.kind == "classification":
230
+ try:
231
+ return ClassificationLevel(annotation.level)
232
+ except ValueError:
233
+ return None
234
+ return None
235
+
236
+ def wire_targets(self) -> dict[str, WireTargetHint]:
237
+ from modelable.parser.wire import wire_targets_from_annotations
238
+
239
+ return wire_targets_from_annotations(self.annotations)
240
+
241
+
242
+ class ModelKind(StrEnum):
243
+ entity = "entity"
244
+ aggregate = "aggregate"
245
+ event = "event"
246
+ value = "value"
247
+
248
+
249
+ class ChangeKind(StrEnum):
250
+ additive = "additive"
251
+ breaking = "breaking"
252
+
253
+
254
+ class AccessGrant(BaseModel):
255
+ principal: str
256
+ permissions: list[str]
257
+
258
+
259
+ class AccessBlock(BaseModel):
260
+ entity: list[AccessGrant] = Field(default_factory=list)
261
+ properties: dict[str, list[AccessGrant]] = Field(default_factory=dict)
262
+
263
+
264
+ class ModelVersion(BaseModel):
265
+ model_kind: ModelKind
266
+ version: int
267
+ change_kind: ChangeKind
268
+ fields: list[FieldDef]
269
+ access: AccessBlock | None = None
270
+ has_version_header: bool = True
271
+ has_change_kind: bool = True
272
+ annotations: list[Annotation] = Field(default_factory=list)
273
+
274
+ def wire_targets(self) -> dict[str, WireTargetHint]:
275
+ from modelable.parser.wire import wire_targets_from_annotations
276
+
277
+ return wire_targets_from_annotations(self.annotations)
278
+
279
+
280
+ class VersionExact(BaseModel):
281
+ kind: Literal["exact"] = "exact"
282
+ version: int
283
+
284
+
285
+ class VersionRange(BaseModel):
286
+ kind: Literal["range"] = "range"
287
+ min_inclusive: int
288
+ max_exclusive: int
289
+
290
+
291
+ class VersionMin(BaseModel):
292
+ kind: Literal["min"] = "min"
293
+ min_inclusive: int
294
+
295
+
296
+ class VersionPinned(BaseModel):
297
+ kind: Literal["pinned"] = "pinned"
298
+ version: int
299
+ content_hash: str
300
+
301
+
302
+ VersionSpec = Annotated[
303
+ VersionExact | VersionRange | VersionMin | VersionPinned,
304
+ Field(discriminator="kind"),
305
+ ]
306
+
307
+
308
+ class SourceRef(BaseModel):
309
+ model: str
310
+ version: VersionSpec
311
+ alias: str
312
+ where: str | None = None
313
+
314
+
315
+ class JoinRef(BaseModel):
316
+ model: str
317
+ version: VersionSpec
318
+ alias: str
319
+ on: str
320
+ join_kind: str = "inner"
321
+ cardinality: str | None = None
322
+ annotations: list[Annotation] = Field(default_factory=list)
323
+
324
+
325
+ class DirectMapping(BaseModel):
326
+ kind: Literal["direct"] = "direct"
327
+ source_alias: str
328
+ source_field: str
329
+
330
+
331
+ class ComputedMapping(BaseModel):
332
+ kind: Literal["computed"] = "computed"
333
+ expression: str
334
+
335
+
336
+ ProjectionMapping = Annotated[
337
+ DirectMapping | ComputedMapping,
338
+ Field(discriminator="kind"),
339
+ ]
340
+
341
+
342
+ class ProjectionField(BaseModel):
343
+ name: str
344
+ mapping: ProjectionMapping
345
+ annotations: list[Annotation] = Field(default_factory=list)
346
+
347
+ @property
348
+ def is_pii(self) -> bool:
349
+ return any(annotation.kind == "pii" for annotation in self.annotations)
350
+
351
+ @property
352
+ def classification(self) -> ClassificationLevel | None:
353
+ for annotation in self.annotations:
354
+ if annotation.kind == "classification":
355
+ try:
356
+ return ClassificationLevel(annotation.level)
357
+ except ValueError:
358
+ return None
359
+ return None
360
+
361
+ def wire_targets(self) -> dict[str, WireTargetHint]:
362
+ from modelable.parser.wire import wire_targets_from_annotations
363
+
364
+ return wire_targets_from_annotations(self.annotations)
365
+
366
+
367
+ class ProjectionVersion(BaseModel):
368
+ version: int
369
+ source: SourceRef
370
+ joins: list[JoinRef] = Field(default_factory=list)
371
+ where: str | None = None
372
+ group_by: list[str] = Field(default_factory=list)
373
+ fields: list[ProjectionField]
374
+ auto_generated: bool = False
375
+ access: AccessBlock | None = None
376
+ annotations: list[Annotation] = Field(default_factory=list)
377
+
378
+ def wire_targets(self) -> dict[str, WireTargetHint]:
379
+ from modelable.parser.wire import wire_targets_from_annotations
380
+
381
+ return wire_targets_from_annotations(self.annotations)
382
+
383
+
384
+ class AutoProjectionTarget(BaseModel):
385
+ kind: Literal["db", "request", "reply", "event"]
386
+ excluded_fields: list[str] = Field(default_factory=list)
387
+ excluded_annotations: list[Annotation] = Field(default_factory=list)
388
+ operations: list[str] = Field(default_factory=list)
389
+
390
+
391
+ class AutoProjectionDecl(BaseModel):
392
+ model: str
393
+ version: int
394
+ targets: list[AutoProjectionTarget]
395
+
396
+
397
+ class GenerateTarget(BaseModel):
398
+ name: str
399
+ dialect: str | None = None
400
+ output_path: str | None = None
401
+
402
+
403
+ class AiConfig(BaseModel):
404
+ provider: str | None = None
405
+ model: str | None = None
406
+ repair_attempts: int | None = None
407
+
408
+
409
+ class FieldMapping(BaseModel):
410
+ source: str
411
+ target: str
412
+
413
+
414
+ class BindingDef(BaseModel):
415
+ name: str
416
+ model: str
417
+ model_version: int
418
+ adapter: str
419
+ table: str | None = None
420
+ field_mappings: list[FieldMapping] = Field(default_factory=list)
421
+
422
+
423
+ class DomainDef(BaseModel):
424
+ name: str
425
+ owner: str | None = None
426
+ contact: str | None = None
427
+ description: str | None = None
428
+ models: dict[str, list[ModelVersion]] = Field(default_factory=dict)
429
+ projections: dict[str, list[ProjectionVersion]] = Field(default_factory=dict)
430
+ auto_projections: list[AutoProjectionDecl] = Field(default_factory=list)
431
+ generate_targets: list[GenerateTarget] = Field(default_factory=list)
432
+
433
+
434
+ class WorkspaceDef(BaseModel):
435
+ label: str | None = None
436
+ name: str | None = None
437
+ description: str | None = None
438
+ generate_targets: list[GenerateTarget] = Field(default_factory=list)
439
+ ai: AiConfig | None = None
440
+
441
+
442
+ class MdlFile(BaseModel):
443
+ domains: list[DomainDef] = Field(default_factory=list)
444
+ bindings: list[BindingDef] = Field(default_factory=list)
445
+ workspace: WorkspaceDef | None = None
446
+
447
+
448
+ ArrayType.model_rebuild()
449
+ MapType.model_rebuild()
450
+ ObjectType.model_rebuild()
451
+ FieldDef.model_rebuild()
@@ -0,0 +1,47 @@
1
+ from functools import cache
2
+ from importlib.resources import files
3
+ from pathlib import Path
4
+
5
+ from lark import Lark, Tree, UnexpectedInput
6
+
7
+ from modelable.parser.ir import MdlFile, ParseError
8
+ from modelable.parser.transformer import MdlTransformer
9
+
10
+
11
+ @cache
12
+ def _parser() -> Lark:
13
+ grammar_path = files("modelable.grammar").joinpath("modelable.lark")
14
+ return Lark(
15
+ grammar_path.read_text(encoding="utf-8"),
16
+ parser="earley",
17
+ ambiguity="resolve",
18
+ )
19
+
20
+
21
+ def parse_text(text: str) -> Tree:
22
+ try:
23
+ return _parser().parse(text)
24
+ except UnexpectedInput as exc:
25
+ raise ParseError(
26
+ str(exc),
27
+ line=getattr(exc, "line", None),
28
+ column=getattr(exc, "column", None),
29
+ end_line=getattr(exc, "end_line", None),
30
+ end_column=getattr(exc, "end_column", None),
31
+ ) from exc
32
+
33
+
34
+ def parse_file(path: str | Path) -> Tree:
35
+ return parse_text(Path(path).read_text(encoding="utf-8"))
36
+
37
+
38
+ def parse_text_to_ir(text: str, path: str | Path | None = None) -> MdlFile:
39
+ try:
40
+ return MdlTransformer().transform(parse_text(text))
41
+ except ValueError as exc:
42
+ raise ParseError(str(exc)) from exc
43
+
44
+
45
+ def parse_file_to_ir(path: str | Path) -> MdlFile:
46
+ path = Path(path)
47
+ return parse_text_to_ir(path.read_text(encoding="utf-8"), path=path)