datalex-cli 0.1.1__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.
- datalex_cli/__init__.py +1 -0
- datalex_cli/datalex_cli.py +658 -0
- datalex_cli/main.py +2925 -0
- datalex_cli-0.1.1.dist-info/METADATA +228 -0
- datalex_cli-0.1.1.dist-info/RECORD +64 -0
- datalex_cli-0.1.1.dist-info/WHEEL +5 -0
- datalex_cli-0.1.1.dist-info/entry_points.txt +2 -0
- datalex_cli-0.1.1.dist-info/licenses/LICENSE +21 -0
- datalex_cli-0.1.1.dist-info/top_level.txt +2 -0
- datalex_core/__init__.py +94 -0
- datalex_core/_schemas/datalex/common.schema.json +127 -0
- datalex_core/_schemas/datalex/domain.schema.json +24 -0
- datalex_core/_schemas/datalex/entity.schema.json +158 -0
- datalex_core/_schemas/datalex/model.schema.json +141 -0
- datalex_core/_schemas/datalex/policy.schema.json +70 -0
- datalex_core/_schemas/datalex/project.schema.json +82 -0
- datalex_core/_schemas/datalex/snippet.schema.json +24 -0
- datalex_core/_schemas/datalex/source.schema.json +104 -0
- datalex_core/_schemas/datalex/term.schema.json +30 -0
- datalex_core/canonical.py +166 -0
- datalex_core/completion.py +204 -0
- datalex_core/connectors/__init__.py +39 -0
- datalex_core/connectors/base.py +417 -0
- datalex_core/connectors/bigquery.py +229 -0
- datalex_core/connectors/databricks.py +262 -0
- datalex_core/connectors/mysql.py +266 -0
- datalex_core/connectors/postgres.py +309 -0
- datalex_core/connectors/redshift.py +298 -0
- datalex_core/connectors/snowflake.py +336 -0
- datalex_core/connectors/sqlserver.py +425 -0
- datalex_core/datalex/__init__.py +26 -0
- datalex_core/datalex/diff.py +188 -0
- datalex_core/datalex/errors.py +85 -0
- datalex_core/datalex/loader.py +512 -0
- datalex_core/datalex/migrate_layout.py +382 -0
- datalex_core/datalex/parse_cache.py +102 -0
- datalex_core/datalex/project.py +214 -0
- datalex_core/datalex/types.py +224 -0
- datalex_core/dbt/__init__.py +18 -0
- datalex_core/dbt/emit.py +344 -0
- datalex_core/dbt/manifest.py +329 -0
- datalex_core/dbt/profiles.py +185 -0
- datalex_core/dbt/sync.py +279 -0
- datalex_core/dbt/warehouse.py +215 -0
- datalex_core/dialects/__init__.py +15 -0
- datalex_core/dialects/_common.py +48 -0
- datalex_core/dialects/base.py +47 -0
- datalex_core/dialects/postgres.py +164 -0
- datalex_core/dialects/registry.py +36 -0
- datalex_core/dialects/snowflake.py +129 -0
- datalex_core/diffing.py +358 -0
- datalex_core/docs_generator.py +797 -0
- datalex_core/doctor.py +181 -0
- datalex_core/generators.py +478 -0
- datalex_core/importers.py +1176 -0
- datalex_core/issues.py +23 -0
- datalex_core/loader.py +21 -0
- datalex_core/migrate.py +316 -0
- datalex_core/modeling.py +679 -0
- datalex_core/packages.py +430 -0
- datalex_core/policy.py +1037 -0
- datalex_core/resolver.py +456 -0
- datalex_core/schema.py +54 -0
- datalex_core/semantic.py +1561 -0
datalex_cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# CLI package marker
|
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
"""`datalex datalex ...` CLI surface — thin wrapper over datalex_core.datalex.
|
|
2
|
+
|
|
3
|
+
Subcommands:
|
|
4
|
+
migrate to-datalex-layout <v3-model.yaml> split legacy v3 model into DataLex tree
|
|
5
|
+
validate <project-root> load + validate a DataLex project
|
|
6
|
+
emit ddl <project-root> --dialect ... emit per-dialect DDL for every physical entity
|
|
7
|
+
diff <old-root> <new-root> semantic diff with explicit rename tracking
|
|
8
|
+
info <project-root> print a summary (entity/term/domain counts)
|
|
9
|
+
|
|
10
|
+
All subcommands accept --output-json for machine-readable output where sensible.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from datalex_core.datalex import load_project
|
|
21
|
+
from datalex_core.datalex.diff import diff_entities
|
|
22
|
+
from datalex_core.datalex.errors import DataLexLoadError
|
|
23
|
+
from datalex_core.datalex.migrate_layout import migrate_project
|
|
24
|
+
import datalex_core.dialects # noqa: F401 — side-effect registers built-in dialects
|
|
25
|
+
from datalex_core.dialects.registry import get_dialect, known_dialects
|
|
26
|
+
from datalex_core.dbt import emit_dbt, import_manifest, write_import_result
|
|
27
|
+
from datalex_core.dbt.sync import sync_dbt_project, report_to_json
|
|
28
|
+
from datalex_core.packages import PackageResolveError, load_imports_for, resolve_imports
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def register_datalex(parent_sub: argparse._SubParsersAction) -> None:
|
|
32
|
+
"""Register `datalex datalex <...>` under the given subparsers object."""
|
|
33
|
+
datalex = parent_sub.add_parser("datalex", help="DataLex spec-layout tooling")
|
|
34
|
+
dsub = datalex.add_subparsers(dest="datalex_command", required=True)
|
|
35
|
+
|
|
36
|
+
# migrate
|
|
37
|
+
migrate_parser = dsub.add_parser(
|
|
38
|
+
"migrate", help="Migrate legacy v3 model to DataLex file-per-entity layout"
|
|
39
|
+
)
|
|
40
|
+
msub = migrate_parser.add_subparsers(dest="migrate_command", required=True)
|
|
41
|
+
to_layout = msub.add_parser(
|
|
42
|
+
"to-datalex-layout",
|
|
43
|
+
help="Split a v3 *.model.yaml into DataLex file-per-entity project",
|
|
44
|
+
)
|
|
45
|
+
to_layout.add_argument("model", help="Path to legacy v3 *.model.yaml")
|
|
46
|
+
to_layout.add_argument(
|
|
47
|
+
"--output-root", help="Where to write the new tree (default: alongside model)"
|
|
48
|
+
)
|
|
49
|
+
to_layout.add_argument(
|
|
50
|
+
"--dialect",
|
|
51
|
+
default="postgres",
|
|
52
|
+
help="Physical dialect the v3 model targets (default: postgres)",
|
|
53
|
+
)
|
|
54
|
+
to_layout.add_argument(
|
|
55
|
+
"--dry-run",
|
|
56
|
+
action="store_true",
|
|
57
|
+
help="Print what would be written without writing",
|
|
58
|
+
)
|
|
59
|
+
to_layout.add_argument(
|
|
60
|
+
"--output-json", action="store_true", help="Print machine-readable report"
|
|
61
|
+
)
|
|
62
|
+
to_layout.set_defaults(func=_cmd_migrate)
|
|
63
|
+
|
|
64
|
+
# validate
|
|
65
|
+
validate_parser = dsub.add_parser(
|
|
66
|
+
"validate", help="Load and validate a DataLex project"
|
|
67
|
+
)
|
|
68
|
+
validate_parser.add_argument("root", help="Project root containing datalex.yaml")
|
|
69
|
+
validate_parser.add_argument(
|
|
70
|
+
"--output-json", action="store_true", help="Emit diagnostics as JSON"
|
|
71
|
+
)
|
|
72
|
+
validate_parser.add_argument(
|
|
73
|
+
"--non-strict",
|
|
74
|
+
action="store_true",
|
|
75
|
+
help="Do not exit non-zero on errors; just print them",
|
|
76
|
+
)
|
|
77
|
+
validate_parser.set_defaults(func=_cmd_validate)
|
|
78
|
+
|
|
79
|
+
# emit ddl
|
|
80
|
+
emit_parser = dsub.add_parser("emit", help="Emit artifacts from a DataLex project")
|
|
81
|
+
esub = emit_parser.add_subparsers(dest="emit_command", required=True)
|
|
82
|
+
ddl_parser = esub.add_parser("ddl", help="Emit per-dialect DDL for physical entities")
|
|
83
|
+
ddl_parser.add_argument("root", help="Project root")
|
|
84
|
+
ddl_parser.add_argument(
|
|
85
|
+
"--dialect",
|
|
86
|
+
required=True,
|
|
87
|
+
help=f"Dialect. One of: {', '.join(sorted(known_dialects())) or '(none registered)'}",
|
|
88
|
+
)
|
|
89
|
+
ddl_parser.add_argument(
|
|
90
|
+
"--out", help="Write DDL to this file (default: stdout)"
|
|
91
|
+
)
|
|
92
|
+
ddl_parser.set_defaults(func=_cmd_emit_ddl)
|
|
93
|
+
|
|
94
|
+
# diff
|
|
95
|
+
diff_parser = dsub.add_parser(
|
|
96
|
+
"diff", help="Semantic diff between two DataLex projects"
|
|
97
|
+
)
|
|
98
|
+
diff_parser.add_argument("old", help="Old project root")
|
|
99
|
+
diff_parser.add_argument("new", help="New project root")
|
|
100
|
+
diff_parser.add_argument(
|
|
101
|
+
"--output-json",
|
|
102
|
+
action="store_true",
|
|
103
|
+
help="Emit diff as JSON (default: human-readable)",
|
|
104
|
+
)
|
|
105
|
+
diff_parser.add_argument(
|
|
106
|
+
"--exit-on-breaking",
|
|
107
|
+
action="store_true",
|
|
108
|
+
help="Exit non-zero if any breaking changes are detected",
|
|
109
|
+
)
|
|
110
|
+
diff_parser.set_defaults(func=_cmd_diff)
|
|
111
|
+
|
|
112
|
+
# info
|
|
113
|
+
info_parser = dsub.add_parser("info", help="Summarize a DataLex project")
|
|
114
|
+
info_parser.add_argument("root", help="Project root")
|
|
115
|
+
info_parser.add_argument(
|
|
116
|
+
"--output-json", action="store_true", help="Emit summary as JSON"
|
|
117
|
+
)
|
|
118
|
+
info_parser.set_defaults(func=_cmd_info)
|
|
119
|
+
|
|
120
|
+
# dbt
|
|
121
|
+
dbt_parser = dsub.add_parser("dbt", help="dbt integration (emit / import)")
|
|
122
|
+
dbt_sub = dbt_parser.add_subparsers(dest="dbt_command", required=True)
|
|
123
|
+
|
|
124
|
+
dbt_emit = dbt_sub.add_parser(
|
|
125
|
+
"emit", help="Emit dbt-parseable YAML (sources.yml + schema.yml)"
|
|
126
|
+
)
|
|
127
|
+
dbt_emit.add_argument("root", help="DataLex project root")
|
|
128
|
+
dbt_emit.add_argument(
|
|
129
|
+
"--out-dir", required=True, help="Target directory to write dbt YAML"
|
|
130
|
+
)
|
|
131
|
+
dbt_emit.add_argument(
|
|
132
|
+
"--only",
|
|
133
|
+
choices=["sources", "models", "all"],
|
|
134
|
+
default="all",
|
|
135
|
+
help="Emit only sources, only models, or both (default: all)",
|
|
136
|
+
)
|
|
137
|
+
dbt_emit.add_argument(
|
|
138
|
+
"--output-json",
|
|
139
|
+
action="store_true",
|
|
140
|
+
help="Emit the report as JSON",
|
|
141
|
+
)
|
|
142
|
+
dbt_emit.set_defaults(func=_cmd_dbt_emit)
|
|
143
|
+
|
|
144
|
+
dbt_import = dbt_sub.add_parser(
|
|
145
|
+
"import",
|
|
146
|
+
help="Import a dbt manifest.json into DataLex source/model files (idempotent by unique_id)",
|
|
147
|
+
)
|
|
148
|
+
dbt_import.add_argument("manifest", help="Path to dbt target/manifest.json")
|
|
149
|
+
dbt_import.add_argument(
|
|
150
|
+
"--out-root",
|
|
151
|
+
required=True,
|
|
152
|
+
help="Target DataLex project root to write sources/ and models/dbt/",
|
|
153
|
+
)
|
|
154
|
+
dbt_import.add_argument(
|
|
155
|
+
"--merge-from",
|
|
156
|
+
help="Existing DataLex project root to merge user-authored fields from",
|
|
157
|
+
)
|
|
158
|
+
dbt_import.set_defaults(func=_cmd_dbt_import)
|
|
159
|
+
|
|
160
|
+
dbt_sync = dbt_sub.add_parser(
|
|
161
|
+
"sync",
|
|
162
|
+
help="Sync a dbt project into DataLex (manifest + live warehouse types)",
|
|
163
|
+
)
|
|
164
|
+
dbt_sync.add_argument("dbt_project", help="Path to dbt project (contains dbt_project.yml)")
|
|
165
|
+
dbt_sync.add_argument(
|
|
166
|
+
"--out-root",
|
|
167
|
+
required=True,
|
|
168
|
+
help="DataLex project root to write sources/ and models/dbt/",
|
|
169
|
+
)
|
|
170
|
+
dbt_sync.add_argument(
|
|
171
|
+
"--profile",
|
|
172
|
+
dest="target_override",
|
|
173
|
+
help="Pick a non-default target name from the dbt profile",
|
|
174
|
+
)
|
|
175
|
+
dbt_sync.add_argument(
|
|
176
|
+
"--profiles-dir",
|
|
177
|
+
help="Override profiles.yml search (default: dbt's own precedence)",
|
|
178
|
+
)
|
|
179
|
+
dbt_sync.add_argument(
|
|
180
|
+
"--manifest",
|
|
181
|
+
help="Explicit path to manifest.json (default: <dbt_project>/target/manifest.json)",
|
|
182
|
+
)
|
|
183
|
+
dbt_sync.add_argument(
|
|
184
|
+
"--skip-warehouse",
|
|
185
|
+
action="store_true",
|
|
186
|
+
help="Skip live warehouse introspection; use manifest data_type only",
|
|
187
|
+
)
|
|
188
|
+
dbt_sync.add_argument(
|
|
189
|
+
"--output-json",
|
|
190
|
+
action="store_true",
|
|
191
|
+
help="Emit the sync report as JSON",
|
|
192
|
+
)
|
|
193
|
+
dbt_sync.set_defaults(func=_cmd_dbt_sync)
|
|
194
|
+
|
|
195
|
+
# expand
|
|
196
|
+
expand_parser = dsub.add_parser(
|
|
197
|
+
"expand",
|
|
198
|
+
help="Preview a project with snippets expanded (does not modify files)",
|
|
199
|
+
)
|
|
200
|
+
expand_parser.add_argument("root", help="Project root")
|
|
201
|
+
expand_parser.add_argument(
|
|
202
|
+
"--output-json",
|
|
203
|
+
action="store_true",
|
|
204
|
+
help="Print expanded entities as JSON",
|
|
205
|
+
)
|
|
206
|
+
expand_parser.set_defaults(func=_cmd_expand)
|
|
207
|
+
|
|
208
|
+
# packages
|
|
209
|
+
packages_parser = dsub.add_parser(
|
|
210
|
+
"packages", help="Cross-repo package resolution (Phase C)"
|
|
211
|
+
)
|
|
212
|
+
packages_sub = packages_parser.add_subparsers(dest="packages_command", required=True)
|
|
213
|
+
|
|
214
|
+
pkg_resolve = packages_sub.add_parser(
|
|
215
|
+
"resolve",
|
|
216
|
+
help="Resolve `imports:` in datalex.yaml — fetch, cache, and write .datalex/lock.yaml",
|
|
217
|
+
)
|
|
218
|
+
pkg_resolve.add_argument("root", help="Project root")
|
|
219
|
+
pkg_resolve.add_argument(
|
|
220
|
+
"--update",
|
|
221
|
+
action="store_true",
|
|
222
|
+
help="Re-fetch packages and regenerate lockfile even if entries exist",
|
|
223
|
+
)
|
|
224
|
+
pkg_resolve.add_argument(
|
|
225
|
+
"--cache-root",
|
|
226
|
+
help="Override the package cache root (default: ~/.datalex/packages)",
|
|
227
|
+
)
|
|
228
|
+
pkg_resolve.add_argument(
|
|
229
|
+
"--output-json", action="store_true", help="Print a JSON report"
|
|
230
|
+
)
|
|
231
|
+
pkg_resolve.set_defaults(func=_cmd_packages_resolve)
|
|
232
|
+
|
|
233
|
+
pkg_list = packages_sub.add_parser(
|
|
234
|
+
"list", help="Show resolved packages and their cached locations"
|
|
235
|
+
)
|
|
236
|
+
pkg_list.add_argument("root", help="Project root")
|
|
237
|
+
pkg_list.add_argument(
|
|
238
|
+
"--output-json", action="store_true", help="Print as JSON"
|
|
239
|
+
)
|
|
240
|
+
pkg_list.set_defaults(func=_cmd_packages_list)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# ----------------- command impls -----------------
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _cmd_migrate(args: argparse.Namespace) -> int:
|
|
247
|
+
report = migrate_project(
|
|
248
|
+
args.model,
|
|
249
|
+
output_root=args.output_root,
|
|
250
|
+
default_dialect=args.dialect,
|
|
251
|
+
dry_run=args.dry_run,
|
|
252
|
+
)
|
|
253
|
+
if args.output_json:
|
|
254
|
+
payload = {
|
|
255
|
+
"project_root": str(report.project_root),
|
|
256
|
+
"manifest_written": report.manifest_written,
|
|
257
|
+
"entities_written": report.entities_written,
|
|
258
|
+
"terms_written": report.terms_written,
|
|
259
|
+
"domains_written": report.domains_written,
|
|
260
|
+
"warnings": report.warnings,
|
|
261
|
+
"files": report.files,
|
|
262
|
+
"dry_run": bool(args.dry_run),
|
|
263
|
+
}
|
|
264
|
+
print(json.dumps(payload, indent=2))
|
|
265
|
+
else:
|
|
266
|
+
print(report.summary())
|
|
267
|
+
if args.dry_run:
|
|
268
|
+
print("\n(dry-run — no files written)")
|
|
269
|
+
for f in report.files:
|
|
270
|
+
print(f" would write: {f}")
|
|
271
|
+
return 0
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _cmd_validate(args: argparse.Namespace) -> int:
|
|
275
|
+
strict = not args.non_strict
|
|
276
|
+
try:
|
|
277
|
+
project = load_project(args.root, strict=strict)
|
|
278
|
+
except DataLexLoadError as e:
|
|
279
|
+
if args.output_json:
|
|
280
|
+
print(json.dumps({"errors": [err.to_dict() for err in e.errors]}, indent=2))
|
|
281
|
+
else:
|
|
282
|
+
for err in e.errors:
|
|
283
|
+
print(str(err), file=sys.stderr)
|
|
284
|
+
return 1
|
|
285
|
+
|
|
286
|
+
errors = project.errors.to_list()
|
|
287
|
+
if args.output_json:
|
|
288
|
+
print(
|
|
289
|
+
json.dumps(
|
|
290
|
+
{
|
|
291
|
+
"root": str(project.root),
|
|
292
|
+
"entities": len(project.entities),
|
|
293
|
+
"terms": len(project.terms),
|
|
294
|
+
"domains": len(project.domains),
|
|
295
|
+
"sources": len(project.sources),
|
|
296
|
+
"models": len(project.models),
|
|
297
|
+
"policies": len(project.policies),
|
|
298
|
+
"snippets": len(project.snippets),
|
|
299
|
+
"errors": errors,
|
|
300
|
+
},
|
|
301
|
+
indent=2,
|
|
302
|
+
)
|
|
303
|
+
)
|
|
304
|
+
else:
|
|
305
|
+
print(f"DataLex project: {project.root}")
|
|
306
|
+
print(f" entities: {len(project.entities)}")
|
|
307
|
+
print(f" terms: {len(project.terms)}")
|
|
308
|
+
print(f" domains: {len(project.domains)}")
|
|
309
|
+
print(f" sources: {len(project.sources)}")
|
|
310
|
+
print(f" models: {len(project.models)}")
|
|
311
|
+
print(f" policies: {len(project.policies)}")
|
|
312
|
+
print(f" snippets: {len(project.snippets)}")
|
|
313
|
+
if errors:
|
|
314
|
+
print(f"\n{len(errors)} diagnostic(s):")
|
|
315
|
+
for err in project.errors.errors:
|
|
316
|
+
print(f" {err}")
|
|
317
|
+
return 1 if project.errors.has_errors() else 0
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _cmd_emit_ddl(args: argparse.Namespace) -> int:
|
|
321
|
+
try:
|
|
322
|
+
project = load_project(args.root, strict=True)
|
|
323
|
+
except DataLexLoadError as e:
|
|
324
|
+
for err in e.errors:
|
|
325
|
+
print(str(err), file=sys.stderr)
|
|
326
|
+
return 1
|
|
327
|
+
|
|
328
|
+
try:
|
|
329
|
+
dialect = get_dialect(args.dialect)
|
|
330
|
+
except KeyError:
|
|
331
|
+
print(
|
|
332
|
+
f"Unknown dialect '{args.dialect}'. Known: {', '.join(sorted(known_dialects()))}",
|
|
333
|
+
file=sys.stderr,
|
|
334
|
+
)
|
|
335
|
+
return 2
|
|
336
|
+
|
|
337
|
+
# Build name -> physical_name map so FK emission references the actual table name
|
|
338
|
+
# rather than the logical/snake name used as a key inside DataLex.
|
|
339
|
+
physical_name_of = {}
|
|
340
|
+
for ent in project.physical_entities(dialect=args.dialect):
|
|
341
|
+
physical_name_of[ent.get("name")] = ent.get("physical_name") or ent.get("name")
|
|
342
|
+
|
|
343
|
+
chunks = []
|
|
344
|
+
for ent in project.physical_entities(dialect=args.dialect):
|
|
345
|
+
chunks.append(dialect.render_entity(_resolve_refs(ent, physical_name_of)))
|
|
346
|
+
|
|
347
|
+
body = "\n".join(chunks).rstrip() + "\n" if chunks else ""
|
|
348
|
+
|
|
349
|
+
if args.out:
|
|
350
|
+
Path(args.out).parent.mkdir(parents=True, exist_ok=True)
|
|
351
|
+
Path(args.out).write_text(body, encoding="utf-8")
|
|
352
|
+
print(f"Wrote {len(chunks)} entity DDL block(s) to {args.out}")
|
|
353
|
+
else:
|
|
354
|
+
sys.stdout.write(body)
|
|
355
|
+
return 0
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _cmd_diff(args: argparse.Namespace) -> int:
|
|
359
|
+
try:
|
|
360
|
+
old = load_project(args.old, strict=False)
|
|
361
|
+
new = load_project(args.new, strict=False)
|
|
362
|
+
except DataLexLoadError as e:
|
|
363
|
+
for err in e.errors:
|
|
364
|
+
print(str(err), file=sys.stderr)
|
|
365
|
+
return 1
|
|
366
|
+
|
|
367
|
+
result = diff_entities(old.entities, new.entities)
|
|
368
|
+
|
|
369
|
+
if args.output_json:
|
|
370
|
+
print(json.dumps(result, indent=2, default=str))
|
|
371
|
+
else:
|
|
372
|
+
_print_diff_human(result)
|
|
373
|
+
|
|
374
|
+
if args.exit_on_breaking and result.get("breaking"):
|
|
375
|
+
return 3
|
|
376
|
+
return 0
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _cmd_info(args: argparse.Namespace) -> int:
|
|
380
|
+
try:
|
|
381
|
+
project = load_project(args.root, strict=False)
|
|
382
|
+
except DataLexLoadError as e:
|
|
383
|
+
for err in e.errors:
|
|
384
|
+
print(str(err), file=sys.stderr)
|
|
385
|
+
return 1
|
|
386
|
+
|
|
387
|
+
entities_by_layer = {"conceptual": 0, "logical": 0, "physical": 0}
|
|
388
|
+
dialects: dict = {}
|
|
389
|
+
for key, ent in project.entities.items():
|
|
390
|
+
layer = key.split(":", 1)[0]
|
|
391
|
+
entities_by_layer[layer] = entities_by_layer.get(layer, 0) + 1
|
|
392
|
+
if layer == "physical":
|
|
393
|
+
d = ent.get("dialect") or "(unspecified)"
|
|
394
|
+
dialects[d] = dialects.get(d, 0) + 1
|
|
395
|
+
|
|
396
|
+
if args.output_json:
|
|
397
|
+
print(
|
|
398
|
+
json.dumps(
|
|
399
|
+
{
|
|
400
|
+
"root": str(project.root),
|
|
401
|
+
"entities_by_layer": entities_by_layer,
|
|
402
|
+
"physical_by_dialect": dialects,
|
|
403
|
+
"terms": len(project.terms),
|
|
404
|
+
"domains": len(project.domains),
|
|
405
|
+
"sources": len(project.sources),
|
|
406
|
+
"models": len(project.models),
|
|
407
|
+
"policies": len(project.policies),
|
|
408
|
+
"snippets": len(project.snippets),
|
|
409
|
+
},
|
|
410
|
+
indent=2,
|
|
411
|
+
)
|
|
412
|
+
)
|
|
413
|
+
else:
|
|
414
|
+
print(f"DataLex project: {project.root}")
|
|
415
|
+
print(" entities:")
|
|
416
|
+
for layer, n in entities_by_layer.items():
|
|
417
|
+
print(f" {layer:11s} {n}")
|
|
418
|
+
if dialects:
|
|
419
|
+
print(" physical by dialect:")
|
|
420
|
+
for d, n in sorted(dialects.items()):
|
|
421
|
+
print(f" {d:11s} {n}")
|
|
422
|
+
print(f" terms: {len(project.terms)}")
|
|
423
|
+
print(f" domains: {len(project.domains)}")
|
|
424
|
+
print(f" sources: {len(project.sources)}")
|
|
425
|
+
print(f" models: {len(project.models)}")
|
|
426
|
+
print(f" policies: {len(project.policies)}")
|
|
427
|
+
print(f" snippets: {len(project.snippets)}")
|
|
428
|
+
return 0
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _cmd_dbt_emit(args: argparse.Namespace) -> int:
|
|
432
|
+
try:
|
|
433
|
+
project = load_project(args.root, strict=True)
|
|
434
|
+
except DataLexLoadError as e:
|
|
435
|
+
for err in e.errors:
|
|
436
|
+
print(str(err), file=sys.stderr)
|
|
437
|
+
return 1
|
|
438
|
+
|
|
439
|
+
include_sources = args.only in ("all", "sources")
|
|
440
|
+
include_models = args.only in ("all", "models")
|
|
441
|
+
report = emit_dbt(
|
|
442
|
+
project,
|
|
443
|
+
out_dir=args.out_dir,
|
|
444
|
+
include_sources=include_sources,
|
|
445
|
+
include_models=include_models,
|
|
446
|
+
)
|
|
447
|
+
if args.output_json:
|
|
448
|
+
print(
|
|
449
|
+
json.dumps(
|
|
450
|
+
{
|
|
451
|
+
"out_dir": args.out_dir,
|
|
452
|
+
"sources": report.sources,
|
|
453
|
+
"models": report.models,
|
|
454
|
+
"files": report.files,
|
|
455
|
+
},
|
|
456
|
+
indent=2,
|
|
457
|
+
)
|
|
458
|
+
)
|
|
459
|
+
else:
|
|
460
|
+
print(report.summary())
|
|
461
|
+
return 0
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _cmd_dbt_import(args: argparse.Namespace) -> int:
|
|
465
|
+
result = import_manifest(
|
|
466
|
+
args.manifest,
|
|
467
|
+
existing_project_root=args.merge_from or args.out_root,
|
|
468
|
+
)
|
|
469
|
+
written = write_import_result(result, args.out_root)
|
|
470
|
+
print(
|
|
471
|
+
f"Imported {len(result.sources)} source(s) and {len(result.models)} model(s) "
|
|
472
|
+
f"from {args.manifest} into {args.out_root}."
|
|
473
|
+
)
|
|
474
|
+
for f in written:
|
|
475
|
+
print(f" - {f}")
|
|
476
|
+
if result.warnings:
|
|
477
|
+
print("\nWarnings:")
|
|
478
|
+
for w in result.warnings:
|
|
479
|
+
print(f" - {w}")
|
|
480
|
+
return 0
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _cmd_dbt_sync(args: argparse.Namespace) -> int:
|
|
484
|
+
try:
|
|
485
|
+
report = sync_dbt_project(
|
|
486
|
+
dbt_project_dir=args.dbt_project,
|
|
487
|
+
datalex_root=args.out_root,
|
|
488
|
+
profiles_dir=args.profiles_dir,
|
|
489
|
+
target_override=args.target_override,
|
|
490
|
+
skip_warehouse=args.skip_warehouse,
|
|
491
|
+
manifest_path=args.manifest,
|
|
492
|
+
)
|
|
493
|
+
except FileNotFoundError as e:
|
|
494
|
+
print(f"error: {e}", file=sys.stderr)
|
|
495
|
+
return 1
|
|
496
|
+
|
|
497
|
+
if args.output_json:
|
|
498
|
+
print(report_to_json(report))
|
|
499
|
+
else:
|
|
500
|
+
print(report.summary())
|
|
501
|
+
return 0
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _cmd_expand(args: argparse.Namespace) -> int:
|
|
505
|
+
try:
|
|
506
|
+
project = load_project(args.root, strict=False)
|
|
507
|
+
except DataLexLoadError as e:
|
|
508
|
+
for err in e.errors:
|
|
509
|
+
print(str(err), file=sys.stderr)
|
|
510
|
+
return 1
|
|
511
|
+
|
|
512
|
+
# load_project already ran resolve(), so snippets are already expanded.
|
|
513
|
+
# Emit the resulting entity dicts so users can diff against the on-disk YAML
|
|
514
|
+
# to see what each snippet contributed.
|
|
515
|
+
if args.output_json:
|
|
516
|
+
print(
|
|
517
|
+
json.dumps(
|
|
518
|
+
{"entities": project.entities, "sources": project.sources, "models": project.models},
|
|
519
|
+
indent=2,
|
|
520
|
+
default=str,
|
|
521
|
+
)
|
|
522
|
+
)
|
|
523
|
+
else:
|
|
524
|
+
import yaml as _yaml
|
|
525
|
+
|
|
526
|
+
for key, ent in sorted(project.entities.items()):
|
|
527
|
+
print(f"# {key}")
|
|
528
|
+
print(_yaml.safe_dump(ent, sort_keys=False, default_flow_style=False))
|
|
529
|
+
return 0
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _cmd_packages_resolve(args: argparse.Namespace) -> int:
|
|
533
|
+
try:
|
|
534
|
+
report = resolve_imports(
|
|
535
|
+
args.root,
|
|
536
|
+
cache_root=args.cache_root,
|
|
537
|
+
update=args.update,
|
|
538
|
+
)
|
|
539
|
+
except PackageResolveError as e:
|
|
540
|
+
print(f"error: {e}", file=sys.stderr)
|
|
541
|
+
return 1
|
|
542
|
+
|
|
543
|
+
if args.output_json:
|
|
544
|
+
print(
|
|
545
|
+
json.dumps(
|
|
546
|
+
{
|
|
547
|
+
"lockfile": str(report.lockfile_path) if report.lockfile_path else None,
|
|
548
|
+
"lockfile_written": report.lockfile_written,
|
|
549
|
+
"packages": [
|
|
550
|
+
{
|
|
551
|
+
"package": r.spec.package,
|
|
552
|
+
"version": r.spec.version,
|
|
553
|
+
"ref": r.spec.ref,
|
|
554
|
+
"alias": r.spec.default_alias(),
|
|
555
|
+
"root": str(r.root),
|
|
556
|
+
"resolved_sha": r.resolved_sha,
|
|
557
|
+
"content_hash": r.content_hash,
|
|
558
|
+
}
|
|
559
|
+
for r in report.resolved
|
|
560
|
+
],
|
|
561
|
+
"warnings": report.warnings,
|
|
562
|
+
},
|
|
563
|
+
indent=2,
|
|
564
|
+
)
|
|
565
|
+
)
|
|
566
|
+
else:
|
|
567
|
+
print(report.summary())
|
|
568
|
+
return 0
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def _cmd_packages_list(args: argparse.Namespace) -> int:
|
|
572
|
+
try:
|
|
573
|
+
resolved = load_imports_for(args.root)
|
|
574
|
+
except PackageResolveError as e:
|
|
575
|
+
print(f"error: {e}", file=sys.stderr)
|
|
576
|
+
return 1
|
|
577
|
+
if args.output_json:
|
|
578
|
+
print(
|
|
579
|
+
json.dumps(
|
|
580
|
+
[
|
|
581
|
+
{
|
|
582
|
+
"package": r.spec.package,
|
|
583
|
+
"alias": r.spec.default_alias(),
|
|
584
|
+
"version": r.spec.version,
|
|
585
|
+
"ref": r.spec.ref,
|
|
586
|
+
"root": str(r.root),
|
|
587
|
+
"resolved_sha": r.resolved_sha,
|
|
588
|
+
"content_hash": r.content_hash,
|
|
589
|
+
}
|
|
590
|
+
for r in resolved
|
|
591
|
+
],
|
|
592
|
+
indent=2,
|
|
593
|
+
)
|
|
594
|
+
)
|
|
595
|
+
else:
|
|
596
|
+
if not resolved:
|
|
597
|
+
print("(no packages)")
|
|
598
|
+
for r in resolved:
|
|
599
|
+
suffix = f"@{r.spec.version}" if r.spec.version else ""
|
|
600
|
+
alias = f" [alias={r.spec.default_alias()}]"
|
|
601
|
+
print(f"{r.spec.package}{suffix}{alias} -> {r.root}")
|
|
602
|
+
return 0
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def _resolve_refs(entity: dict, physical_name_of: dict) -> dict:
|
|
606
|
+
"""Return a shallow copy of entity with column references.entity rewritten
|
|
607
|
+
to target physical names, so FK DDL points at the actual table name rather
|
|
608
|
+
than the DataLex logical/snake key.
|
|
609
|
+
"""
|
|
610
|
+
cols_out = []
|
|
611
|
+
for col in entity.get("columns", []) or []:
|
|
612
|
+
ref = col.get("references")
|
|
613
|
+
if ref and ref.get("entity") in physical_name_of:
|
|
614
|
+
new_ref = dict(ref)
|
|
615
|
+
new_ref["entity"] = physical_name_of[ref["entity"]]
|
|
616
|
+
col = {**col, "references": new_ref}
|
|
617
|
+
cols_out.append(col)
|
|
618
|
+
return {**entity, "columns": cols_out}
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def _print_diff_human(result: dict) -> None:
|
|
622
|
+
added = result.get("added") or []
|
|
623
|
+
removed = result.get("removed") or []
|
|
624
|
+
renamed = result.get("renamed") or []
|
|
625
|
+
changed = result.get("changed") or []
|
|
626
|
+
breaking = result.get("breaking") or []
|
|
627
|
+
|
|
628
|
+
if added:
|
|
629
|
+
print(f"Added ({len(added)}):")
|
|
630
|
+
for k in added:
|
|
631
|
+
print(f" + {k}")
|
|
632
|
+
if removed:
|
|
633
|
+
print(f"Removed ({len(removed)}):")
|
|
634
|
+
for k in removed:
|
|
635
|
+
print(f" - {k}")
|
|
636
|
+
if renamed:
|
|
637
|
+
print(f"Renamed ({len(renamed)}):")
|
|
638
|
+
for old, new in renamed:
|
|
639
|
+
print(f" ~ {old} -> {new}")
|
|
640
|
+
if changed:
|
|
641
|
+
print(f"Changed ({len(changed)}):")
|
|
642
|
+
for ch in changed:
|
|
643
|
+
print(f" * {ch.get('entity')}")
|
|
644
|
+
cols = ch.get("columns") or {}
|
|
645
|
+
for a in cols.get("added", []):
|
|
646
|
+
print(f" + column {a}")
|
|
647
|
+
for r in cols.get("removed", []):
|
|
648
|
+
print(f" - column {r}")
|
|
649
|
+
for rn in cols.get("renamed", []):
|
|
650
|
+
print(f" ~ column {rn['from']} -> {rn['to']}")
|
|
651
|
+
for c in cols.get("changed", []):
|
|
652
|
+
print(f" * column {c.get('name')}: {list(k for k in c if k != 'name')}")
|
|
653
|
+
if breaking:
|
|
654
|
+
print(f"\nBreaking ({len(breaking)}):")
|
|
655
|
+
for b in breaking:
|
|
656
|
+
print(f" ! {b}")
|
|
657
|
+
if not (added or removed or renamed or changed):
|
|
658
|
+
print("No changes.")
|