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
|
@@ -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
|