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.
Files changed (64) hide show
  1. datalex_cli/__init__.py +1 -0
  2. datalex_cli/datalex_cli.py +658 -0
  3. datalex_cli/main.py +2925 -0
  4. datalex_cli-0.1.1.dist-info/METADATA +228 -0
  5. datalex_cli-0.1.1.dist-info/RECORD +64 -0
  6. datalex_cli-0.1.1.dist-info/WHEEL +5 -0
  7. datalex_cli-0.1.1.dist-info/entry_points.txt +2 -0
  8. datalex_cli-0.1.1.dist-info/licenses/LICENSE +21 -0
  9. datalex_cli-0.1.1.dist-info/top_level.txt +2 -0
  10. datalex_core/__init__.py +94 -0
  11. datalex_core/_schemas/datalex/common.schema.json +127 -0
  12. datalex_core/_schemas/datalex/domain.schema.json +24 -0
  13. datalex_core/_schemas/datalex/entity.schema.json +158 -0
  14. datalex_core/_schemas/datalex/model.schema.json +141 -0
  15. datalex_core/_schemas/datalex/policy.schema.json +70 -0
  16. datalex_core/_schemas/datalex/project.schema.json +82 -0
  17. datalex_core/_schemas/datalex/snippet.schema.json +24 -0
  18. datalex_core/_schemas/datalex/source.schema.json +104 -0
  19. datalex_core/_schemas/datalex/term.schema.json +30 -0
  20. datalex_core/canonical.py +166 -0
  21. datalex_core/completion.py +204 -0
  22. datalex_core/connectors/__init__.py +39 -0
  23. datalex_core/connectors/base.py +417 -0
  24. datalex_core/connectors/bigquery.py +229 -0
  25. datalex_core/connectors/databricks.py +262 -0
  26. datalex_core/connectors/mysql.py +266 -0
  27. datalex_core/connectors/postgres.py +309 -0
  28. datalex_core/connectors/redshift.py +298 -0
  29. datalex_core/connectors/snowflake.py +336 -0
  30. datalex_core/connectors/sqlserver.py +425 -0
  31. datalex_core/datalex/__init__.py +26 -0
  32. datalex_core/datalex/diff.py +188 -0
  33. datalex_core/datalex/errors.py +85 -0
  34. datalex_core/datalex/loader.py +512 -0
  35. datalex_core/datalex/migrate_layout.py +382 -0
  36. datalex_core/datalex/parse_cache.py +102 -0
  37. datalex_core/datalex/project.py +214 -0
  38. datalex_core/datalex/types.py +224 -0
  39. datalex_core/dbt/__init__.py +18 -0
  40. datalex_core/dbt/emit.py +344 -0
  41. datalex_core/dbt/manifest.py +329 -0
  42. datalex_core/dbt/profiles.py +185 -0
  43. datalex_core/dbt/sync.py +279 -0
  44. datalex_core/dbt/warehouse.py +215 -0
  45. datalex_core/dialects/__init__.py +15 -0
  46. datalex_core/dialects/_common.py +48 -0
  47. datalex_core/dialects/base.py +47 -0
  48. datalex_core/dialects/postgres.py +164 -0
  49. datalex_core/dialects/registry.py +36 -0
  50. datalex_core/dialects/snowflake.py +129 -0
  51. datalex_core/diffing.py +358 -0
  52. datalex_core/docs_generator.py +797 -0
  53. datalex_core/doctor.py +181 -0
  54. datalex_core/generators.py +478 -0
  55. datalex_core/importers.py +1176 -0
  56. datalex_core/issues.py +23 -0
  57. datalex_core/loader.py +21 -0
  58. datalex_core/migrate.py +316 -0
  59. datalex_core/modeling.py +679 -0
  60. datalex_core/packages.py +430 -0
  61. datalex_core/policy.py +1037 -0
  62. datalex_core/resolver.py +456 -0
  63. datalex_core/schema.py +54 -0
  64. datalex_core/semantic.py +1561 -0
@@ -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.")