arclith-cli 0.9.0__tar.gz → 0.11.0__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.
Files changed (30) hide show
  1. {arclith_cli-0.9.0 → arclith_cli-0.11.0}/PKG-INFO +2 -2
  2. {arclith_cli-0.9.0 → arclith_cli-0.11.0}/README.md +125 -8
  3. arclith_cli-0.11.0/arclith_cli/__init__.py +1 -0
  4. {arclith_cli-0.9.0 → arclith_cli-0.11.0}/arclith_cli/add_adapter.py +75 -1
  5. {arclith_cli-0.9.0 → arclith_cli-0.11.0}/arclith_cli/capabilities.py +311 -7
  6. arclith_cli-0.11.0/arclith_cli/core_scaffold.py +258 -0
  7. arclith_cli-0.11.0/arclith_cli/init_project.py +283 -0
  8. {arclith_cli-0.9.0 → arclith_cli-0.11.0}/arclith_cli/main.py +87 -1
  9. {arclith_cli-0.9.0 → arclith_cli-0.11.0}/arclith_cli/project_paths.py +31 -3
  10. {arclith_cli-0.9.0 → arclith_cli-0.11.0}/arclith_cli/rename.py +23 -2
  11. {arclith_cli-0.9.0 → arclith_cli-0.11.0}/pyproject.toml +2 -2
  12. {arclith_cli-0.9.0 → arclith_cli-0.11.0}/tests/e2e_manual.sh +1 -1
  13. {arclith_cli-0.9.0 → arclith_cli-0.11.0}/tests/test_add_adapter.py +214 -4
  14. arclith_cli-0.11.0/tests/test_capabilities.py +205 -0
  15. arclith_cli-0.11.0/tests/test_core_scaffold.py +208 -0
  16. {arclith_cli-0.9.0 → arclith_cli-0.11.0}/tests/test_e2e_scaffold.py +156 -1
  17. {arclith_cli-0.9.0 → arclith_cli-0.11.0}/tests/test_project_paths.py +16 -0
  18. {arclith_cli-0.9.0 → arclith_cli-0.11.0}/tests/test_rename.py +12 -1
  19. {arclith_cli-0.9.0 → arclith_cli-0.11.0}/uv.lock +17 -3
  20. arclith_cli-0.9.0/arclith_cli/__init__.py +0 -1
  21. arclith_cli-0.9.0/tests/test_capabilities.py +0 -113
  22. {arclith_cli-0.9.0 → arclith_cli-0.11.0}/.gitignore +0 -0
  23. {arclith_cli-0.9.0 → arclith_cli-0.11.0}/LICENSE +0 -0
  24. {arclith_cli-0.9.0 → arclith_cli-0.11.0}/Makefile +0 -0
  25. {arclith_cli-0.9.0 → arclith_cli-0.11.0}/arclith_cli/adapter_templates.py +0 -0
  26. {arclith_cli-0.9.0 → arclith_cli-0.11.0}/arclith_cli/entity_scanner.py +0 -0
  27. {arclith_cli-0.9.0 → arclith_cli-0.11.0}/arclith_cli/export_config.py +0 -0
  28. {arclith_cli-0.9.0 → arclith_cli-0.11.0}/arclith_cli/scaffold.py +0 -0
  29. {arclith_cli-0.9.0 → arclith_cli-0.11.0}/arclith_cli/updater.py +0 -0
  30. {arclith_cli-0.9.0 → arclith_cli-0.11.0}/tests/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arclith-cli
3
- Version: 0.9.0
3
+ Version: 0.11.0
4
4
  Summary: CLI scaffolding tool for arclith — hexagonal architecture framework
5
5
  Author-email: Killian KOPP <killiankopp@gmail.com>
6
6
  License: Apache License
@@ -186,7 +186,7 @@ License: Apache License
186
186
  License-File: LICENSE
187
187
  Keywords: arclith,cli,ddd,hexagonal-architecture,scaffold
188
188
  Requires-Python: >=3.13
189
- Requires-Dist: arclith>=0.12.0
189
+ Requires-Dist: arclith>=0.14.0
190
190
  Requires-Dist: httpx>=0.27.0
191
191
  Requires-Dist: rich>=13.0.0
192
192
  Requires-Dist: typer>=0.15.0
@@ -10,6 +10,25 @@ uv tool install "git+https://github.com/karned-rekipe/arclith.git#subdirectory=c
10
10
 
11
11
  ## Commandes
12
12
 
13
+ ### `init` — Initialiser un projet minimal
14
+
15
+ Crée un projet Arclith vide de métier, avec le layout canonique `src/<package>/...`, une
16
+ configuration minimale et un `main.py` prêt à recevoir les adapters.
17
+
18
+ ```bash
19
+ # Mode interactif
20
+ arclith-cli init
21
+
22
+ # Mode direct
23
+ arclith-cli init todo-list-service
24
+ arclith-cli init todo-list-service --dir ~/projects
25
+ ```
26
+
27
+ Cette commande ne crée aucune entité, aucun CRUD et aucun endpoint métier. Elle sert quand on veut
28
+ construire le projet étape par étape avec `add-entity`, `add-usecase`, puis `add-adapter`.
29
+
30
+ ---
31
+
13
32
  ### `new` — Créer un projet
14
33
 
15
34
  Scaffold un nouveau projet arclith depuis le template officiel `_sample`.
@@ -34,6 +53,71 @@ Le projet généré utilise un layout `src/<package>/...` pour le code applicati
34
53
 
35
54
  ---
36
55
 
56
+ ### `add-entity` — Ajouter une entité métier
57
+
58
+ Crée uniquement le fichier minimal d'une entité dans `src/<package>/domain/models/`.
59
+
60
+ ```bash
61
+ cd my-recipe-service
62
+ arclith-cli add-entity ShoppingItem
63
+ ```
64
+
65
+ Fichier généré :
66
+
67
+ ```text
68
+ src/<package>/domain/models/shopping_item.py
69
+ ```
70
+
71
+ La commande ne génère aucun CRUD, aucun port repository, aucun adapter et aucun endpoint. Elle pose seulement le point d'ancrage du modèle métier ; le développeur complète ensuite les champs et invariants de l'entité.
72
+
73
+ ---
74
+
75
+ ### `add-usecase` — Ajouter un cas d'usage
76
+
77
+ Crée le port inbound minimal dans `src/<package>/domain/ports/inbound/`, puis le fichier minimal du
78
+ cas d'usage dans `src/<package>/application/use_cases/`.
79
+
80
+ ```bash
81
+ cd my-recipe-service
82
+ arclith-cli add-usecase PlanShoppingList
83
+ arclith-cli add-usecase find-by-name
84
+ ```
85
+
86
+ Fichier généré :
87
+
88
+ ```text
89
+ src/<package>/domain/ports/inbound/plan_shopping_list.py
90
+ src/<package>/application/use_cases/plan_shopping_list.py
91
+ ```
92
+
93
+ Le nom peut être fourni en PascalCase, snake_case ou kebab-case. Le suffixe `UseCase` est normalisé : `PlanShoppingListUseCase` et `plan-shopping-list-use-case` génèrent tous les deux `PlanShoppingListUseCase`.
94
+
95
+ Comme `add-entity`, cette commande ne câble pas FastAPI, FastMCP, LangGraph, un repository ou un service. Les adapters se branchent ensuite explicitement avec `add-adapter` et devraient dépendre du port inbound généré.
96
+
97
+ ---
98
+
99
+ ### `add-planner` — Ajouter un planner applicatif
100
+
101
+ Crée uniquement le fichier minimal d'un planner dans `src/<package>/application/planners/`.
102
+
103
+ ```bash
104
+ cd my-recipe-service
105
+ arclith-cli add-planner IngredientIntent
106
+ arclith-cli add-planner command-router
107
+ ```
108
+
109
+ Fichier généré :
110
+
111
+ ```text
112
+ src/<package>/application/planners/ingredient_intent.py
113
+ ```
114
+
115
+ Le planner est le composant applicatif qui transforme une demande naturelle en commande ou DTO
116
+ structuré. Il ne remplace pas LangGraph : LangGraph orchestre les nœuds, tandis que le planner porte
117
+ la traduction d'intention. Le fichier généré reste volontairement vide de logique métier.
118
+
119
+ ---
120
+
37
121
  ### `add-adapter` — Ajouter un adapter
38
122
 
39
123
  Wizard interactif à lancer **depuis la racine du projet cible**. Scaffold le code Python et/ou les fichiers de configuration pour un nouvel adapter. Par défaut, la capacité cible est `repository`.
@@ -49,29 +133,39 @@ Mode direct, utile pour CI, scripts de migration ou commandes reproductibles :
49
133
  arclith-cli add-adapter --adapter mongodb --entity Recipe --db-name my_recipe_service --yes
50
134
  arclith-cli add-adapter --adapter duckdb --all-entities --path data/ --no-activate --yes
51
135
  arclith-cli add-adapter --adapter mariadb --entity Recipe --param database=my_recipe_service --param user=app --yes
136
+ arclith-cli add-adapter --capability api --adapter fastapi --param port=8080 --yes
137
+ arclith-cli add-adapter --capability mcp --adapter fastmcp --param port=8081 --yes
138
+ arclith-cli add-adapter --capability llm --adapter lmstudio --param model_name=qwen/qwen3.5-9b --yes
52
139
  arclith-cli add-adapter --capability agent --adapter langgraph --param graph_name=recipe_agent --yes
53
140
  arclith-cli add-adapter --capability observability --adapter langsmith
141
+ arclith-cli add-adapter --capability observability --adapter opentelemetry --param service_name=my_recipe_service --yes
54
142
  arclith-cli add-adapter --capability repository --adapter memory --entity Recipe --yes
55
143
  ```
56
144
 
57
145
  **Étapes du wizard :**
58
146
 
59
- 1. **Type d'adapter** — selon la capacité : `memory` · `mongodb` · `duckdb` · `mariadb` · `langgraph` · `langsmith`
60
- 2. **Entité(s) cible(s)** — détectées automatiquement pour les adapters entity-scoped ; ignorées pour `agent/langgraph` et `observability/langsmith`
147
+ 1. **Type d'adapter** — selon la capacité : `memory` · `mongodb` · `duckdb` · `mariadb` · `fastapi` · `fastmcp` · `lmstudio` · `openai` · `anthropic` · `langgraph` · `langsmith` · `opentelemetry`
148
+ 2. **Entité(s) cible(s)** — détectées automatiquement pour les adapters entity-scoped ; ignorées pour les transports globaux, `llm/*`, `agent/langgraph` et les adapters d'observability
61
149
  3. **Paramètres** — questions spécifiques à l'adapter :
62
150
  - `mongodb` → `db_name`, `multitenant`
63
151
  - `duckdb` → `path`
64
152
  - `mariadb` → `host`, `port`, `database`, `user`, `driver`, `table_prefix`
153
+ - `fastapi` → `host`, `port`, `reload`
154
+ - `fastmcp` → `host`, `port`
155
+ - `lmstudio` → `model_name`, `base_url`, `api_key`
156
+ - `openai` → `model_name`, `base_url`, `OPENAI_API_KEY`
157
+ - `anthropic` → `model_name`, `ANTHROPIC_API_KEY`
65
158
  - `langgraph` → `graph_name`
66
159
  - `langsmith` → `tracing`, `project`, `endpoint`, `LANGSMITH_API_KEY`
160
+ - `opentelemetry` → `service_name`, `endpoint`, `protocol`, `traces`, `metrics`, `instrument_fastapi`
67
161
  - `memory` → aucun paramètre
68
- 4. **Activation** — met à jour `config/adapters/adapters.yaml` pour les capacités à sélecteur (`repository: <adapter>` ou `observability: langsmith`) ; `agent/langgraph` est exposé par `langgraph.json` et `config/adapters/inbound/langgraph.yaml`
162
+ 4. **Activation** — met à jour `config/adapters/adapters.yaml` pour les capacités activables (`repository: <adapter>` ou `observability.enabled: [<adapter>, ...]`) ; `api/fastapi`, `mcp/fastmcp`, `llm/*` et `agent/langgraph` sont exposés par leurs fichiers de configuration scopés
69
163
  5. **Récapitulatif** — liste des fichiers créés ou remplacés avant confirmation
70
164
 
71
165
  | Option | Défaut | Description |
72
166
  |--------|--------|-------------|
73
- | `--capability` | `repository` | Capacité cible du catalogue standardisé (`repository`, `agent`, `observability`) |
74
- | `--adapter` / `-a` | interactif | Adapter du catalogue : `memory`, `mongodb`, `duckdb`, `mariadb`, `langgraph`, `langsmith` |
167
+ | `--capability` | `repository` | Capacité cible du catalogue standardisé (`repository`, `api`, `mcp`, `llm`, `agent`, `observability`) |
168
+ | `--adapter` / `-a` | interactif | Adapter du catalogue : `memory`, `mongodb`, `duckdb`, `mariadb`, `fastapi`, `fastmcp`, `lmstudio`, `openai`, `anthropic`, `langgraph`, `langsmith`, `opentelemetry` |
75
169
  | `--entity` / `-e` | auto si une seule entité | Entité cible, liste séparée par virgule acceptée |
76
170
  | `--all-entities` | `false` | Génère l'adapter pour toutes les entités détectées |
77
171
  | `--activate/--no-activate` | `--activate` | Met à jour `config/adapters/adapters.yaml` quand la capacité expose une clé d'activation |
@@ -97,20 +191,38 @@ src/<package>/infrastructure/containers/<entity>_container.py # RepositoryRegis
97
191
 
98
192
  ```bash
99
193
  uv add "arclith[langgraph]"
194
+ arclith-cli add-adapter --capability llm --adapter lmstudio --param model_name=qwen/qwen3.5-9b --yes
100
195
  arclith-cli add-adapter --capability agent --adapter langgraph
101
196
  arclith-cli add-adapter --capability observability --adapter langsmith
102
197
  uv run langgraph dev --no-browser --allow-blocking --port 2024
103
198
  ```
104
199
 
200
+ L'adapter `llm/lmstudio` génère `config/adapters/outbound/lm.yaml`, chargé dans
201
+ `AppConfig.adapters.lm`. Les adapters `llm/openai` et `llm/anthropic` génèrent aussi un mapping
202
+ `config/secrets.yaml` vers `OPENAI_API_KEY` ou `ANTHROPIC_API_KEY`.
203
+
105
204
  L'adapter `agent/langgraph` génère `langgraph.json`, `config/adapters/inbound/langgraph.yaml` et
106
205
  `src/<package>/adapters/inbound/langgraph/agent.py`. Le projet ne modifie ensuite que ce fichier pour
107
206
  son agent. Comme `fastapi` et `fastmcp`, LangGraph est configuré par son nom produit dans
108
207
  `AppConfig.langgraph`, sans `adapters.agent`. L'adapter `observability/langsmith` génère
109
- `config/adapters/outbound/langsmith.yaml`, met
208
+ `config/adapters/outbound/langsmith.yaml`, l'ajoute à `observability.enabled`, met
110
209
  à jour `.env` et ajoute `.env` au `.gitignore` si besoin. LangSmith Studio devient l'endroit standard
111
210
  pour tester les agents. Une `LANGSMITH_API_KEY` déjà présente est conservée si aucune nouvelle valeur
112
211
  n'est fournie.
113
212
 
213
+ **OpenTelemetry :**
214
+
215
+ ```bash
216
+ uv add "arclith[opentelemetry]"
217
+ arclith-cli add-adapter --capability observability --adapter opentelemetry --param service_name=my-recipe-service --yes
218
+ ```
219
+
220
+ L'adapter `observability/opentelemetry` génère `config/adapters/outbound/opentelemetry.yaml`, met à
221
+ jour `.env`, l'ajoute à `observability.enabled` et branche l'instrumentation FastAPI quand
222
+ `Arclith.fastapi()` construit l'application. Il peut être activé en même temps que LangSmith.
223
+ Le fichier `opentelemetry.yaml` ne porte pas de flag `enabled`: l'activation se fait uniquement dans
224
+ `observability.enabled`.
225
+
114
226
  Parcours complet avec entité, API, LangGraph, LangSmith et LM Studio:
115
227
  [`docs/agent-quickstart.md`](../docs/agent-quickstart.md).
116
228
 
@@ -178,12 +290,14 @@ config/
178
290
  soft_delete.yaml # soft_delete: { retention_days }
179
291
  secrets.yaml # secrets: { resolver, mappings, vault, yaml }
180
292
  adapters/
181
- adapters.yaml # adapters: { logger, repository } ← adapter actif
293
+ adapters.yaml # adapters: { logger, repository, observability.enabled }
182
294
  outbound/
183
295
  mongodb.yaml # adapters.mongodb: { db_name, multitenant }
184
296
  duckdb.yaml # adapters.duckdb: { path, multitenant }
185
297
  mariadb.yaml # adapters.mariadb: { host, port, database, user, ... }
298
+ lm.yaml # adapters.lm: { provider, model_name, api_key, base_url }
186
299
  langsmith.yaml # adapters.langsmith: { tracing, project, endpoint, ... }
300
+ opentelemetry.yaml # adapters.opentelemetry: { endpoint, protocol, traces, metrics, ... }
187
301
  inbound/
188
302
  fastapi.yaml # api: { host, port, reload }
189
303
  fastmcp.yaml # mcp: { host, port }
@@ -199,7 +313,10 @@ Pour changer l'adapter actif sans passer par le wizard :
199
313
  ```yaml
200
314
  # config/adapters/adapters.yaml
201
315
  repository: duckdb # memory | mongodb | duckdb | mariadb
202
- observability: langsmith
316
+ observability:
317
+ enabled:
318
+ - langsmith
319
+ - opentelemetry
203
320
  ```
204
321
 
205
322
  Pour MariaDB, ne committez pas le mot de passe. Mappez `adapters.mariadb.password` ou `adapters.mariadb.url` via `config/secrets.yaml`, un resolver `env` ou Vault.
@@ -0,0 +1 @@
1
+ __version__ = "0.11.0"
@@ -5,6 +5,7 @@ from pathlib import Path
5
5
  from typing import Any
6
6
 
7
7
  import typer
8
+ import yaml
8
9
  from rich.console import Console
9
10
  from rich.panel import Panel
10
11
  from rich.prompt import Confirm, Prompt
@@ -20,6 +21,7 @@ from .capabilities import (
20
21
  AdapterSpec,
21
22
  CapabilitySpec,
22
23
  ParameterSpec,
24
+ SecretMappingSpec,
23
25
  capability_names,
24
26
  get_capability,
25
27
  )
@@ -384,7 +386,7 @@ def _show_recap(
384
386
  cfg_path = project_dir / "config" / "adapters" / "adapters.yaml"
385
387
  table.add_row(
386
388
  str(cfg_path.relative_to(project_dir)),
387
- f"[cyan]mis à jour ({capability.activation_config_key})[/cyan]",
389
+ f"[cyan]mis à jour ({_activation_config_label(capability)})[/cyan]",
388
390
  )
389
391
 
390
392
  console.print()
@@ -409,6 +411,10 @@ def _list_generated_files(
409
411
  gitignore = project_dir / ".gitignore"
410
412
  files.append((gitignore, "mis à jour" if gitignore.exists() else "créé"))
411
413
 
414
+ if adapter.has_secret_mappings():
415
+ secrets_file = project_dir / "config" / "secrets.yaml"
416
+ files.append((secrets_file, "mis à jour" if secrets_file.exists() else "créé"))
417
+
412
418
  template_vars = _file_template_vars(project_dir, paths, adapter, params={})
413
419
  for file_template in adapter.file_templates:
414
420
  path = project_dir / render(file_template.path, template_vars)
@@ -430,6 +436,12 @@ def _list_generated_files(
430
436
  return files
431
437
 
432
438
 
439
+ def _activation_config_label(capability: CapabilitySpec) -> str:
440
+ if capability.name == "observability":
441
+ return "observability.enabled"
442
+ return capability.activation_config_key or ""
443
+
444
+
433
445
  # ── Step 5 : generate ─────────────────────────────────────────────────────────
434
446
 
435
447
  def _generate(
@@ -459,6 +471,11 @@ def _generate(
459
471
  _ensure_env_is_ignored(project_dir)
460
472
  console.print(f"[green]✓[/green] {env_path.relative_to(project_dir)}")
461
473
 
474
+ if adapter.has_secret_mappings():
475
+ secrets_path = project_dir / "config" / "secrets.yaml"
476
+ _merge_secrets_file(secrets_path, adapter.secret_mappings)
477
+ console.print(f"[green]✓[/green] {secrets_path.relative_to(project_dir)}")
478
+
462
479
  for file_template in adapter.file_templates:
463
480
  generated_path = project_dir / render(file_template.path, params)
464
481
  generated_path.parent.mkdir(parents=True, exist_ok=True)
@@ -538,6 +555,10 @@ def _file_template_vars(
538
555
  def _update_active_capability(project_dir: Path, capability: CapabilitySpec, adapter: AdapterSpec) -> None:
539
556
  if capability.activation_config_key is None:
540
557
  return
558
+ if capability.name == "observability":
559
+ _enable_observability_adapter(project_dir, adapter)
560
+ return
561
+
541
562
  cfg = project_dir / "config" / "adapters" / "adapters.yaml"
542
563
  key = capability.activation_config_key
543
564
  escaped_key = re.escape(key)
@@ -554,6 +575,29 @@ def _update_active_capability(project_dir: Path, capability: CapabilitySpec, ada
554
575
  console.print(f"[cyan]↺[/cyan] config/adapters/adapters.yaml → {key}: {adapter.name}")
555
576
 
556
577
 
578
+ def _enable_observability_adapter(project_dir: Path, adapter: AdapterSpec) -> None:
579
+ cfg = project_dir / "config" / "adapters" / "adapters.yaml"
580
+ data = _read_yaml_mapping(cfg)
581
+ existing = data.get("observability")
582
+ if isinstance(existing, dict) and isinstance(existing.get("enabled"), list):
583
+ enabled = []
584
+ for name in existing["enabled"]:
585
+ if isinstance(name, str) and name not in enabled:
586
+ enabled.append(name)
587
+ else:
588
+ enabled = []
589
+
590
+ if adapter.name not in enabled:
591
+ enabled.append(adapter.name)
592
+
593
+ data["observability"] = {"enabled": enabled}
594
+ cfg.parent.mkdir(parents=True, exist_ok=True)
595
+ cfg.write_text(yaml.safe_dump(data, sort_keys=False, allow_unicode=True), encoding="utf-8")
596
+ console.print(
597
+ f"[cyan]↺[/cyan] config/adapters/adapters.yaml → observability.enabled += {adapter.name}"
598
+ )
599
+
600
+
557
601
  def _parse_env_template(rendered: str) -> dict[str, str]:
558
602
  values: dict[str, str] = {}
559
603
  for line in rendered.splitlines():
@@ -595,6 +639,36 @@ def _merge_env_file(env_path: Path, updates: dict[str, str]) -> None:
595
639
  env_path.write_text("\n".join(merged_lines).rstrip("\n") + "\n", encoding="utf-8")
596
640
 
597
641
 
642
+ def _merge_secrets_file(secrets_path: Path, mappings: tuple[SecretMappingSpec, ...]) -> None:
643
+ secrets_path.parent.mkdir(parents=True, exist_ok=True)
644
+ data = _read_yaml_mapping(secrets_path)
645
+ resolver = data.get("resolver")
646
+ if not isinstance(resolver, str) or not resolver.strip():
647
+ data["resolver"] = "env"
648
+
649
+ existing_mappings = data.get("mappings")
650
+ if not isinstance(existing_mappings, dict):
651
+ existing_mappings = {}
652
+
653
+ merged_mappings = dict(existing_mappings)
654
+ for mapping in mappings:
655
+ merged_mappings[mapping.field_path] = mapping.secret_key
656
+ data["mappings"] = merged_mappings
657
+
658
+ rendered = yaml.safe_dump(data, sort_keys=False, allow_unicode=True)
659
+ secrets_path.write_text(rendered, encoding="utf-8")
660
+
661
+
662
+ def _read_yaml_mapping(path: Path) -> dict[str, Any]:
663
+ if not path.exists():
664
+ return {}
665
+
666
+ loaded = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
667
+ if isinstance(loaded, dict):
668
+ return dict(loaded)
669
+ return {}
670
+
671
+
598
672
  def _ensure_env_is_ignored(project_dir: Path) -> None:
599
673
  gitignore = project_dir / ".gitignore"
600
674
  if gitignore.exists():