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.
- modelable/__init__.py +1 -0
- modelable/__main__.py +3 -0
- modelable/_pydantic_py314_compat.py +31 -0
- modelable/cli.py +41 -0
- modelable/commands/__init__.py +1 -0
- modelable/commands/apicurio.py +84 -0
- modelable/commands/codegen.py +241 -0
- modelable/commands/common.py +43 -0
- modelable/commands/compile.py +237 -0
- modelable/commands/create.py +164 -0
- modelable/commands/diff.py +82 -0
- modelable/commands/graph.py +53 -0
- modelable/commands/llm.py +564 -0
- modelable/commands/lsp.py +15 -0
- modelable/commands/runtime.py +37 -0
- modelable/commands/scenario.py +104 -0
- modelable/commands/spec.py +197 -0
- modelable/commands/workspace.py +240 -0
- modelable/compat/__init__.py +11 -0
- modelable/compat/checker.py +179 -0
- modelable/compat/diff.py +169 -0
- modelable/compiler/__init__.py +3 -0
- modelable/compiler/compiler.py +19 -0
- modelable/compiler/workspace.py +346 -0
- modelable/diagnostics/__init__.py +3 -0
- modelable/diagnostics/model.py +27 -0
- modelable/emitters/__init__.py +0 -0
- modelable/emitters/base.py +22 -0
- modelable/emitters/csharp.py +245 -0
- modelable/emitters/dbt_yaml.py +290 -0
- modelable/emitters/diagnostics.py +25 -0
- modelable/emitters/fhir.py +694 -0
- modelable/emitters/fhir_validator.py +36 -0
- modelable/emitters/go.py +334 -0
- modelable/emitters/java.py +264 -0
- modelable/emitters/json_schema.py +458 -0
- modelable/emitters/markdown.py +252 -0
- modelable/emitters/odcs.py +355 -0
- modelable/emitters/openlineage.py +315 -0
- modelable/emitters/openmetadata.py +258 -0
- modelable/emitters/python.py +282 -0
- modelable/emitters/rust.py +643 -0
- modelable/emitters/shapes.py +261 -0
- modelable/emitters/sql.py +266 -0
- modelable/emitters/targets.py +141 -0
- modelable/emitters/typescript.py +352 -0
- modelable/expressions/__init__.py +0 -0
- modelable/expressions/cel.py +547 -0
- modelable/governance/__init__.py +3 -0
- modelable/governance/checker.py +271 -0
- modelable/governance/por.py +46 -0
- modelable/grammar/__init__.py +1 -0
- modelable/grammar/modelable.lark +257 -0
- modelable/graph/__init__.py +5 -0
- modelable/graph/export.py +442 -0
- modelable/llm/__init__.py +43 -0
- modelable/llm/chat.py +255 -0
- modelable/llm/config.py +87 -0
- modelable/llm/context.py +194 -0
- modelable/llm/engine.py +976 -0
- modelable/llm/importers.py +1077 -0
- modelable/llm/provenance.py +84 -0
- modelable/llm/providers.py +182 -0
- modelable/llm/qa.py +126 -0
- modelable/llm/recommendations.py +33 -0
- modelable/llm/redaction.py +19 -0
- modelable/llm/render.py +279 -0
- modelable/llm/update_plan.py +101 -0
- modelable/llm/validation_help.py +10 -0
- modelable/lsp/__init__.py +3 -0
- modelable/lsp/__main__.py +4 -0
- modelable/lsp/code_actions.py +210 -0
- modelable/lsp/completion.py +480 -0
- modelable/lsp/definition.py +343 -0
- modelable/lsp/diagnostics.py +31 -0
- modelable/lsp/document_symbols.py +197 -0
- modelable/lsp/federation.py +261 -0
- modelable/lsp/folding.py +33 -0
- modelable/lsp/formatting.py +64 -0
- modelable/lsp/highlight.py +30 -0
- modelable/lsp/hover.py +370 -0
- modelable/lsp/inlay_hints.py +158 -0
- modelable/lsp/references.py +511 -0
- modelable/lsp/rename.py +564 -0
- modelable/lsp/semantic_tokens.py +412 -0
- modelable/lsp/server.py +370 -0
- modelable/lsp/workspace.py +83 -0
- modelable/lsp/workspace_symbols.py +104 -0
- modelable/parser/__init__.py +94 -0
- modelable/parser/ir.py +451 -0
- modelable/parser/parse.py +47 -0
- modelable/parser/transformer.py +798 -0
- modelable/parser/wire.py +68 -0
- modelable/planner/__init__.py +0 -0
- modelable/planner/lineage.py +91 -0
- modelable/planner/planner.py +134 -0
- modelable/planner/plans.py +122 -0
- modelable/py.typed +0 -0
- modelable/registry/__init__.py +9 -0
- modelable/registry/apicurio.py +166 -0
- modelable/registry/base.py +18 -0
- modelable/registry/factory.py +18 -0
- modelable/registry/index.py +419 -0
- modelable/registry/local.py +26 -0
- modelable/registry/oci.py +22 -0
- modelable/registry/resolver.py +213 -0
- modelable/registry/schema.sql +119 -0
- modelable/registry/signature.py +26 -0
- modelable/release.py +125 -0
- modelable/runtime/__init__.py +5 -0
- modelable/runtime/adapter/__init__.py +17 -0
- modelable/runtime/adapter/base.py +18 -0
- modelable/runtime/adapter/postgres.py +82 -0
- modelable/specs/__init__.py +23 -0
- modelable/specs/tracking.py +220 -0
- modelable/validation/__init__.py +3 -0
- modelable/validation/semantic.py +659 -0
- modelable-1.0.0.dist-info/METADATA +61 -0
- modelable-1.0.0.dist-info/RECORD +122 -0
- modelable-1.0.0.dist-info/WHEEL +4 -0
- modelable-1.0.0.dist-info/entry_points.txt +2 -0
- 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)
|