archi-cli 0.2.1__tar.gz → 0.2.2__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: archi-cli
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: ar·cli·mate — de CLI in je ArchiMate: deterministisch native .archimate-modellen inspecteren, muteren, valideren en renderen
5
5
  Project-URL: Homepage, https://github.com/BureauArchitectuurDigitaleOverheid/ai-assisted-architecting
6
6
  Project-URL: Repository, https://github.com/BureauArchitectuurDigitaleOverheid/ai-assisted-architecting
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "archi-cli"
3
- version = "0.2.1"
3
+ version = "0.2.2"
4
4
  description = "ar·cli·mate — de CLI in je ArchiMate: deterministisch native .archimate-modellen inspecteren, muteren, valideren en renderen"
5
5
  readme = "README.md"
6
6
  license = "EUPL-1.2"
@@ -56,10 +56,7 @@ dev = ["pytest>=8", "pre-commit>=4"]
56
56
  target-version = "py312"
57
57
 
58
58
  [tool.ruff.lint]
59
- # Bugs and dead code first. Import sorting and the formatter follow in a
60
- # separate PR once the open feature branches have landed, so this one does
61
- # not cause merge conflicts in every file.
62
- select = ["E", "F", "W", "B"]
59
+ select = ["E", "F", "W", "B", "I"]
63
60
  ignore = [
64
61
  "E501", # line length is left to the formatter
65
62
  "B008", # Typer declares options as call defaults by design
@@ -5,6 +5,7 @@ Mutating subcommands validate the model in memory and refuse to save when
5
5
  validation errors are found. Run `archi normalize` before committing so the
6
6
  serialization stays Archi-canonical.
7
7
  """
8
+
8
9
  from __future__ import annotations
9
10
 
10
11
  import sys
@@ -42,7 +43,8 @@ def report_validation(model, model_path) -> bool:
42
43
  # anchor conventions discovery at the model, not the working directory, so
43
44
  # validating a model from another project uses that project's conventions
44
45
  allowed_keys, _ = discover_conventions(
45
- model_path, start=Path(model_path).resolve().parent)
46
+ model_path, start=Path(model_path).resolve().parent
47
+ )
46
48
  # validation runs after every mutation: a broken links file must not
47
49
  # block modelling (ADR 0010), so here it degrades to a warning; render
48
50
  # and slides, which actually use the links, still fail on it
@@ -75,6 +77,7 @@ def describe(model, node) -> str:
75
77
 
76
78
  # --- subcommand implementations ---------------------------------------------
77
79
 
80
+
78
81
  def cmd_stats(model, args):
79
82
  print(f"Model: {model.name}")
80
83
  counts = Counter(xsi_type(e) for e in model.elements())
@@ -115,8 +118,9 @@ def cmd_show(model, args):
115
118
  node_id = node.get("id")
116
119
  index = model.id_index()
117
120
  for rel in model.relations_of(node_id):
118
- other_id = (rel.get("target") if rel.get("source") == node_id
119
- else rel.get("source"))
121
+ other_id = (
122
+ rel.get("target") if rel.get("source") == node_id else rel.get("source")
123
+ )
120
124
  other = index.get(other_id)
121
125
  direction = "->" if rel.get("source") == node_id else "<-"
122
126
  other_name = other.get("name") if other is not None else other_id
@@ -129,8 +133,11 @@ def cmd_tree(model, args):
129
133
  children = folder.findall("element")
130
134
  indent = " " * depth
131
135
  folder_type = folder.get("type")
132
- label = (f"{folder.get('name')} ({folder_type})" if folder_type
133
- else folder.get("name"))
136
+ label = (
137
+ f"{folder.get('name')} ({folder_type})"
138
+ if folder_type
139
+ else folder.get("name")
140
+ )
134
141
  print(f"{indent}{label}: {len(children)} item(s)")
135
142
  for el in children:
136
143
  if is_element(el) or el.get("name"):
@@ -151,11 +158,15 @@ def cmd_validate(model, args):
151
158
 
152
159
 
153
160
  def cmd_add_element(model, args):
154
- el = model.add_element(args.type, args.name, folder_type=args.folder,
155
- properties=parse_properties(args.property),
156
- documentation=args.documentation,
157
- subfolder=args.subfolder,
158
- create_subfolder=args.create_subfolder)
161
+ el = model.add_element(
162
+ args.type,
163
+ args.name,
164
+ folder_type=args.folder,
165
+ properties=parse_properties(args.property),
166
+ documentation=args.documentation,
167
+ subfolder=args.subfolder,
168
+ create_subfolder=args.create_subfolder,
169
+ )
159
170
  status = save_validated(model, args)
160
171
  if status == 0:
161
172
  print(f"Toegevoegd: {describe(model, el)}")
@@ -163,13 +174,20 @@ def cmd_add_element(model, args):
163
174
 
164
175
 
165
176
  def cmd_add_relation(model, args):
166
- rel = model.add_relation(args.type, args.source, args.target,
167
- name=args.name, subfolder=args.subfolder,
168
- create_subfolder=args.create_subfolder)
177
+ rel = model.add_relation(
178
+ args.type,
179
+ args.source,
180
+ args.target,
181
+ name=args.name,
182
+ subfolder=args.subfolder,
183
+ create_subfolder=args.create_subfolder,
184
+ )
169
185
  status = save_validated(model, args)
170
186
  if status == 0:
171
- print(f"Toegevoegd: {rel.get('id')} [{xsi_type(rel)}] "
172
- f"{args.source} -> {args.target}")
187
+ print(
188
+ f"Toegevoegd: {rel.get('id')} [{xsi_type(rel)}] "
189
+ f"{args.source} -> {args.target}"
190
+ )
173
191
  return status
174
192
 
175
193
 
@@ -185,8 +203,9 @@ def cmd_set_property(model, args):
185
203
 
186
204
 
187
205
  def cmd_move(model, args):
188
- target = model.move(args.ref, args.subfolder,
189
- create_subfolder=args.create_subfolder)
206
+ target = model.move(
207
+ args.ref, args.subfolder, create_subfolder=args.create_subfolder
208
+ )
190
209
  status = save_validated(model, args)
191
210
  if status == 0:
192
211
  print(f"Verplaatst: '{args.ref}' naar folder '{target.get('name')}'")
@@ -237,21 +256,26 @@ def cmd_add_view(model, args):
237
256
  extra = [model.resolve(ref) for ref in (args.element or [])]
238
257
  for node in extra:
239
258
  if not is_element(node):
240
- raise ModelError(
241
- f"'{node.get('name') or node.get('id')}' is geen element")
242
- diagram = add_view(model, args.name, layout=args.layout,
243
- element_types=set(args.type) if args.type else None,
244
- relation_types=(set(args.relation)
245
- if args.relation else None),
246
- prop=args.property, root=args.root,
247
- related=args.related,
248
- extra_elements=extra or None)
259
+ raise ModelError(f"'{node.get('name') or node.get('id')}' is geen element")
260
+ diagram = add_view(
261
+ model,
262
+ args.name,
263
+ layout=args.layout,
264
+ element_types=set(args.type) if args.type else None,
265
+ relation_types=(set(args.relation) if args.relation else None),
266
+ prop=args.property,
267
+ root=args.root,
268
+ related=args.related,
269
+ extra_elements=extra or None,
270
+ )
249
271
  status = save_validated(model, args)
250
272
  if status == 0:
251
273
  objects = len(diagram.findall("child"))
252
274
  connections = len(list(diagram.iter("sourceConnection")))
253
- print(f"View '{args.name}' aangemaakt: {objects} objecten, "
254
- f"{connections} verbindingen")
275
+ print(
276
+ f"View '{args.name}' aangemaakt: {objects} objecten, "
277
+ f"{connections} verbindingen"
278
+ )
255
279
  return status
256
280
 
257
281
 
@@ -263,6 +287,7 @@ def cmd_normalize(model, args):
263
287
 
264
288
  def cmd_setup(model, args):
265
289
  from .engine import download_engine, find_or_none
290
+
266
291
  existing = find_or_none()
267
292
  if existing:
268
293
  print(f"Archi al beschikbaar: {existing}")
@@ -278,15 +303,22 @@ def cmd_render(model, args):
278
303
  html_dir = Path(args.out) / "html"
279
304
  html_written, html_removed = render_all_html(model, html_dir, links=links)
280
305
  deck_written, deck_removed = render_all_slides(
281
- model, Path(args.decks), html_dir / "slides", links=links)
306
+ model, Path(args.decks), html_dir / "slides", links=links
307
+ )
282
308
  for path in written + html_written + deck_written:
283
309
  print(f"Geschreven: {path}")
284
310
  for path in removed + html_removed:
285
311
  print(f"Verwijderd (view bestaat niet meer): {path}")
286
312
  for path in deck_removed:
287
313
  print(f"Verwijderd (deck bestaat niet meer): {path}")
288
- if not (written or removed or html_written or html_removed
289
- or deck_written or deck_removed):
314
+ if not (
315
+ written
316
+ or removed
317
+ or html_written
318
+ or html_removed
319
+ or deck_written
320
+ or deck_removed
321
+ ):
290
322
  print("Views zijn al actueel.")
291
323
  view_files = set(view_stems(model).values())
292
324
  for warning in check_link_files(links, args.out, html_dir, view_files):
@@ -302,13 +334,13 @@ def cmd_slides(model, args):
302
334
  deck = load_deck(Path(deck_path), model)
303
335
  path = Path(args.out) / f"{deck['slug']}.html"
304
336
  path.parent.mkdir(parents=True, exist_ok=True)
305
- if write_if_changed(path, render_deck_html(model, deck,
306
- links=links)):
337
+ if write_if_changed(path, render_deck_html(model, deck, links=links)):
307
338
  written.append(path)
308
339
  removed = []
309
340
  else:
310
341
  written, removed = render_all_slides(
311
- model, Path(args.decks), Path(args.out), links=links)
342
+ model, Path(args.decks), Path(args.out), links=links
343
+ )
312
344
  for path in written:
313
345
  print(f"Geschreven: {path}")
314
346
  for path in removed:
@@ -333,16 +365,21 @@ de serialisatie Archi-canoniek blijft.
333
365
  """
334
366
 
335
367
  app = typer.Typer(
336
- help=HELP, no_args_is_help=True, add_completion=True,
337
- context_settings={"help_option_names": ["-h", "--help"]})
368
+ help=HELP,
369
+ no_args_is_help=True,
370
+ add_completion=True,
371
+ context_settings={"help_option_names": ["-h", "--help"]},
372
+ )
338
373
 
339
374
  # the global --model, shared by every command through the app callback
340
375
  _state = SimpleNamespace(model=None)
341
376
 
342
377
  ModelOption = typer.Option(
343
- None, "--model",
378
+ None,
379
+ "--model",
344
380
  help="pad naar het .archimate-bestand; standaard gevonden via archi.toml "
345
- "of het enige .archimate-bestand in de huidige map")
381
+ "of het enige .archimate-bestand in de huidige map",
382
+ )
346
383
 
347
384
 
348
385
  @app.callback()
@@ -375,7 +412,8 @@ def _run(command_fn, *, load_model=True, need_model=True, **fields) -> None:
375
412
 
376
413
  # Repeated options declared once; Typer reads the default value + help here.
377
414
  PropertyFilter = typer.Option(
378
- None, "--property", help="filter op property, key=value (herhaalbaar)")
415
+ None, "--property", help="filter op property, key=value (herhaalbaar)"
416
+ )
379
417
 
380
418
 
381
419
  @app.command(help="aantallen per type, relaties en views")
@@ -386,7 +424,8 @@ def stats():
386
424
  @app.command("list", help="elementen tonen, optioneel gefilterd")
387
425
  def list_elements(
388
426
  type: Optional[str] = typer.Option(
389
- None, help="filter op elementtype (bv. Capability)"),
427
+ None, help="filter op elementtype (bv. Capability)"
428
+ ),
390
429
  property: Optional[list[str]] = PropertyFilter,
391
430
  ):
392
431
  _run(cmd_list, type=type, property=property)
@@ -410,9 +449,11 @@ def validate_command():
410
449
  @app.command("normalize", help="serialisatie canoniek maken via de Archi CLI")
411
450
  def normalize_command(
412
451
  no_download: bool = typer.Option(
413
- False, "--no-download",
452
+ False,
453
+ "--no-download",
414
454
  help="haal de Archi-engine niet automatisch op; faal als hij "
415
- "ontbreekt (voor CI en luchtdichte omgevingen)"),
455
+ "ontbreekt (voor CI en luchtdichte omgevingen)",
456
+ ),
416
457
  ):
417
458
  _run(cmd_normalize, load_model=False, no_download=no_download)
418
459
 
@@ -427,51 +468,68 @@ def add_element(
427
468
  type: str = typer.Option(..., help="elementtype (bv. Capability)"),
428
469
  name: str = typer.Option(..., help="naam van het element"),
429
470
  folder: Optional[str] = typer.Option(
430
- None, help="folder-type; default volgt uit het type"),
471
+ None, help="folder-type; default volgt uit het type"
472
+ ),
431
473
  property: Optional[list[str]] = typer.Option(
432
- None, "--property", help="key=value (herhaalbaar)"),
474
+ None, "--property", help="key=value (herhaalbaar)"
475
+ ),
433
476
  documentation: Optional[str] = typer.Option(None),
434
477
  subfolder: Optional[str] = typer.Option(
435
- None, help="submap binnen de laagfolder, geneste mappen met '/'"),
478
+ None, help="submap binnen de laagfolder, geneste mappen met '/'"
479
+ ),
436
480
  create_subfolder: bool = typer.Option(
437
- False, "--create-subfolder",
438
- help="ontbrekende submap aanmaken (anders: fout)"),
481
+ False, "--create-subfolder", help="ontbrekende submap aanmaken (anders: fout)"
482
+ ),
439
483
  ):
440
- _run(cmd_add_element, type=type, name=name, folder=folder,
441
- property=property, documentation=documentation,
442
- subfolder=subfolder, create_subfolder=create_subfolder)
484
+ _run(
485
+ cmd_add_element,
486
+ type=type,
487
+ name=name,
488
+ folder=folder,
489
+ property=property,
490
+ documentation=documentation,
491
+ subfolder=subfolder,
492
+ create_subfolder=create_subfolder,
493
+ )
443
494
 
444
495
 
445
496
  @app.command("add-relation", help="relatie toevoegen")
446
497
  def add_relation(
447
- type: str = typer.Option(
448
- ..., help="bv. Aggregation of AggregationRelationship"),
498
+ type: str = typer.Option(..., help="bv. Aggregation of AggregationRelationship"),
449
499
  source: str = typer.Option(..., help="id of unieke naam"),
450
500
  target: str = typer.Option(..., help="id of unieke naam"),
451
501
  name: Optional[str] = typer.Option(None, help="NL-label op de relatie"),
452
502
  subfolder: Optional[str] = typer.Option(
453
- None, help="submap binnen Relations, geneste mappen met '/'"),
503
+ None, help="submap binnen Relations, geneste mappen met '/'"
504
+ ),
454
505
  create_subfolder: bool = typer.Option(
455
- False, "--create-subfolder",
456
- help="ontbrekende submap aanmaken (anders: fout)"),
506
+ False, "--create-subfolder", help="ontbrekende submap aanmaken (anders: fout)"
507
+ ),
457
508
  ):
458
- _run(cmd_add_relation, type=type, source=source,
459
- target=target, name=name, subfolder=subfolder,
460
- create_subfolder=create_subfolder)
509
+ _run(
510
+ cmd_add_relation,
511
+ type=type,
512
+ source=source,
513
+ target=target,
514
+ name=name,
515
+ subfolder=subfolder,
516
+ create_subfolder=create_subfolder,
517
+ )
461
518
 
462
519
 
463
520
  @app.command(help="element, relatie of view naar een submap verplaatsen")
464
521
  def move(
465
522
  ref: str = typer.Argument(help="id of (unieke) naam"),
466
523
  subfolder: str = typer.Option(
467
- ..., help="submap binnen de huidige laagfolder, geneste mappen met "
468
- "'/'; leeg ('') = terug naar de laagfolder zelf"),
524
+ ...,
525
+ help="submap binnen de huidige laagfolder, geneste mappen met "
526
+ "'/'; leeg ('') = terug naar de laagfolder zelf",
527
+ ),
469
528
  create_subfolder: bool = typer.Option(
470
- False, "--create-subfolder",
471
- help="ontbrekende submap aanmaken (anders: fout)"),
529
+ False, "--create-subfolder", help="ontbrekende submap aanmaken (anders: fout)"
530
+ ),
472
531
  ):
473
- _run(cmd_move, ref=ref, subfolder=subfolder,
474
- create_subfolder=create_subfolder)
532
+ _run(cmd_move, ref=ref, subfolder=subfolder, create_subfolder=create_subfolder)
475
533
 
476
534
 
477
535
  @app.command("set-property", help="property zetten of bijwerken")
@@ -510,8 +568,8 @@ def set_documentation(
510
568
  def remove(
511
569
  ref: str = typer.Argument(help="id of (unieke) naam"),
512
570
  cascade: bool = typer.Option(
513
- False, help="verwijder ook relaties en view-objecten die ernaar "
514
- "verwijzen"),
571
+ False, help="verwijder ook relaties en view-objecten die ernaar verwijzen"
572
+ ),
515
573
  ):
516
574
  _run(cmd_remove, ref=ref, cascade=cascade)
517
575
 
@@ -524,9 +582,11 @@ def set_model_name(name: str = typer.Argument(help="nieuwe modelnaam")):
524
582
  @app.command(help="views renderen naar Mermaid, NLDD-HTML en slidedecks")
525
583
  def render(
526
584
  out: str = typer.Option(
527
- "views", help="doelmap voor de markdown-bestanden (default: views)"),
585
+ "views", help="doelmap voor de markdown-bestanden (default: views)"
586
+ ),
528
587
  decks: str = typer.Option(
529
- "decks", help="map met deckdefinities in TOML (default: decks)"),
588
+ "decks", help="map met deckdefinities in TOML (default: decks)"
589
+ ),
530
590
  ):
531
591
  _run(cmd_render, out=out, decks=decks)
532
592
 
@@ -534,12 +594,15 @@ def render(
534
594
  @app.command(help="slidedecks renderen naar zelfstandige HTML")
535
595
  def slides(
536
596
  deck: Optional[list[str]] = typer.Option(
537
- None, "--deck", help="specifiek deckbestand (.toml); herhaalbaar; "
538
- "default: alle decks in de decks-map"),
597
+ None,
598
+ "--deck",
599
+ help="specifiek deckbestand (.toml); herhaalbaar; "
600
+ "default: alle decks in de decks-map",
601
+ ),
539
602
  decks: str = typer.Option(
540
- "decks", help="map met deckdefinities in TOML (default: decks)"),
541
- out: str = typer.Option(
542
- "views/html/slides", help="doelmap voor de HTML-bestanden"),
603
+ "decks", help="map met deckdefinities in TOML (default: decks)"
604
+ ),
605
+ out: str = typer.Option("views/html/slides", help="doelmap voor de HTML-bestanden"),
543
606
  ):
544
607
  _run(cmd_slides, deck=deck, decks=decks, out=out)
545
608
 
@@ -549,27 +612,44 @@ def add_view_command(
549
612
  name: str = typer.Option(..., help="naam van de nieuwe view"),
550
613
  layout: str = typer.Option("grid", help="grid of cluster"),
551
614
  type: Optional[list[str]] = typer.Option(
552
- None, help="elementtype in de selectie (herhaalbaar)"),
615
+ None, help="elementtype in de selectie (herhaalbaar)"
616
+ ),
553
617
  relation: Optional[list[str]] = typer.Option(
554
- None, help="relatietype in de selectie (herhaalbaar)"),
618
+ None, help="relatietype in de selectie (herhaalbaar)"
619
+ ),
555
620
  property: Optional[str] = typer.Option(
556
- None, "--property", help="selectiefilter, key=value"),
621
+ None, "--property", help="selectiefilter, key=value"
622
+ ),
557
623
  root: Optional[str] = typer.Option(
558
- None, help="element (id of naam): dit element plus alles wat het "
559
- "aggregeert of composeert"),
624
+ None,
625
+ help="element (id of naam): dit element plus alles wat het "
626
+ "aggregeert of composeert",
627
+ ),
560
628
  related: bool = typer.Option(
561
- False, help="voeg ook direct gerelateerde elementen toe (één stap, "
562
- "alleen samen met --root zinvol)"),
629
+ False,
630
+ help="voeg ook direct gerelateerde elementen toe (één stap, "
631
+ "alleen samen met --root zinvol)",
632
+ ),
563
633
  element: Optional[list[str]] = typer.Option(
564
- None, "--element", help="element (id of unieke naam) toevoegen aan de "
565
- "selectie (herhaalbaar)"),
634
+ None,
635
+ "--element",
636
+ help="element (id of unieke naam) toevoegen aan de selectie (herhaalbaar)",
637
+ ),
566
638
  ):
567
639
  if layout not in ("grid", "cluster"):
568
640
  typer.echo("FOUT: --layout moet grid of cluster zijn", err=True)
569
641
  raise typer.Exit(1)
570
- _run(cmd_add_view, name=name, layout=layout, type=type,
571
- relation=relation, property=property, root=root, related=related,
572
- element=element)
642
+ _run(
643
+ cmd_add_view,
644
+ name=name,
645
+ layout=layout,
646
+ type=type,
647
+ relation=relation,
648
+ property=property,
649
+ root=root,
650
+ related=related,
651
+ element=element,
652
+ )
573
653
 
574
654
 
575
655
  def main(argv=None) -> int:
@@ -17,6 +17,7 @@ Resolution order for the conventions list:
17
17
  3. the built-in default set, so the property-key check stays meaningful even
18
18
  without a project list
19
19
  """
20
+
20
21
  from __future__ import annotations
21
22
 
22
23
  import tomllib
@@ -78,8 +79,7 @@ def discover_model(explicit=None, start=None):
78
79
  # a relative path in config is relative to the config file's directory
79
80
  path = (config_path.parent / table["model"]).resolve()
80
81
  if not path.exists():
81
- raise ModelError(
82
- f"Modelpad uit {config_path.name} bestaat niet: {path}")
82
+ raise ModelError(f"Modelpad uit {config_path.name} bestaat niet: {path}")
83
83
  return path
84
84
 
85
85
  candidates = sorted(start.glob("*.archimate"))
@@ -88,12 +88,14 @@ def discover_model(explicit=None, start=None):
88
88
  if not candidates:
89
89
  raise ModelError(
90
90
  "Geen modelbestand gevonden. Geef --model <pad>, of zet "
91
- "`[tool.archi] model = \"...\"` in archi.toml, of draai in een map "
92
- "met precies één .archimate-bestand.")
91
+ '`[tool.archi] model = "..."` in archi.toml, of draai in een map '
92
+ "met precies één .archimate-bestand."
93
+ )
93
94
  names = ", ".join(c.name for c in candidates)
94
95
  raise ModelError(
95
96
  f"Meerdere .archimate-bestanden gevonden ({names}). Kies er één met "
96
- "--model <pad> of leg het vast in archi.toml.")
97
+ "--model <pad> of leg het vast in archi.toml."
98
+ )
97
99
 
98
100
 
99
101
  def discover_links(model_path, start=None):
@@ -109,8 +111,7 @@ def discover_links(model_path, start=None):
109
111
  return None
110
112
  path = (config_path.parent / table["links"]).resolve()
111
113
  if not path.exists():
112
- raise ModelError(
113
- f"Linkspad uit {config_path.name} bestaat niet: {path}")
114
+ raise ModelError(f"Linkspad uit {config_path.name} bestaat niet: {path}")
114
115
  return path
115
116
 
116
117
 
@@ -131,11 +132,13 @@ def discover_conventions(model_path, start=None):
131
132
  path = (config_path.parent / table["conventions"]).resolve()
132
133
  if not path.exists():
133
134
  raise ModelError(
134
- f"Conventiepad uit {config_path.name} bestaat niet: {path}")
135
+ f"Conventiepad uit {config_path.name} bestaat niet: {path}"
136
+ )
135
137
  keys = allowed_property_keys(path)
136
138
  if not keys:
137
139
  raise ModelError(
138
- f"Conventiebestand {path} bevat geen Property-keys-sectie.")
140
+ f"Conventiebestand {path} bevat geen Property-keys-sectie."
141
+ )
139
142
  return keys, str(path)
140
143
 
141
144
  model_dir = Path(model_path).resolve().parent