arclith-cli 0.13.0__tar.gz → 0.14.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.13.0 → arclith_cli-0.14.0}/PKG-INFO +2 -2
  2. {arclith_cli-0.13.0 → arclith_cli-0.14.0}/README.md +5 -3
  3. arclith_cli-0.14.0/arclith_cli/__init__.py +1 -0
  4. {arclith_cli-0.13.0 → arclith_cli-0.14.0}/arclith_cli/add_adapter.py +8 -0
  5. {arclith_cli-0.13.0 → arclith_cli-0.14.0}/arclith_cli/capabilities.py +233 -0
  6. {arclith_cli-0.13.0 → arclith_cli-0.14.0}/pyproject.toml +2 -2
  7. {arclith_cli-0.13.0 → arclith_cli-0.14.0}/tests/e2e_manual.sh +1 -1
  8. {arclith_cli-0.13.0 → arclith_cli-0.14.0}/tests/test_add_adapter.py +142 -1
  9. {arclith_cli-0.13.0 → arclith_cli-0.14.0}/tests/test_capabilities.py +108 -1
  10. {arclith_cli-0.13.0 → arclith_cli-0.14.0}/tests/test_core_scaffold.py +1 -1
  11. {arclith_cli-0.13.0 → arclith_cli-0.14.0}/uv.lock +11 -3
  12. arclith_cli-0.13.0/arclith_cli/__init__.py +0 -1
  13. {arclith_cli-0.13.0 → arclith_cli-0.14.0}/.gitignore +0 -0
  14. {arclith_cli-0.13.0 → arclith_cli-0.14.0}/LICENSE +0 -0
  15. {arclith_cli-0.13.0 → arclith_cli-0.14.0}/Makefile +0 -0
  16. {arclith_cli-0.13.0 → arclith_cli-0.14.0}/arclith_cli/adapter_templates.py +0 -0
  17. {arclith_cli-0.13.0 → arclith_cli-0.14.0}/arclith_cli/core_scaffold.py +0 -0
  18. {arclith_cli-0.13.0 → arclith_cli-0.14.0}/arclith_cli/entity_scanner.py +0 -0
  19. {arclith_cli-0.13.0 → arclith_cli-0.14.0}/arclith_cli/export_config.py +0 -0
  20. {arclith_cli-0.13.0 → arclith_cli-0.14.0}/arclith_cli/init_project.py +0 -0
  21. {arclith_cli-0.13.0 → arclith_cli-0.14.0}/arclith_cli/main.py +0 -0
  22. {arclith_cli-0.13.0 → arclith_cli-0.14.0}/arclith_cli/project_paths.py +0 -0
  23. {arclith_cli-0.13.0 → arclith_cli-0.14.0}/arclith_cli/rename.py +0 -0
  24. {arclith_cli-0.13.0 → arclith_cli-0.14.0}/arclith_cli/runtime_templates.py +0 -0
  25. {arclith_cli-0.13.0 → arclith_cli-0.14.0}/arclith_cli/scaffold.py +0 -0
  26. {arclith_cli-0.13.0 → arclith_cli-0.14.0}/arclith_cli/updater.py +0 -0
  27. {arclith_cli-0.13.0 → arclith_cli-0.14.0}/tests/__init__.py +0 -0
  28. {arclith_cli-0.13.0 → arclith_cli-0.14.0}/tests/test_e2e_scaffold.py +0 -0
  29. {arclith_cli-0.13.0 → arclith_cli-0.14.0}/tests/test_project_paths.py +0 -0
  30. {arclith_cli-0.13.0 → arclith_cli-0.14.0}/tests/test_rename.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: arclith-cli
3
- Version: 0.13.0
3
+ Version: 0.14.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.16.0
189
+ Requires-Dist: arclith>=0.17.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
@@ -141,7 +141,7 @@ arclith-cli add-adapter --adapter mariadb --entity Recipe --param database=my_re
141
141
  arclith-cli add-adapter --capability api --adapter fastapi --param port=8080 --yes
142
142
  arclith-cli add-adapter --capability mcp --adapter fastmcp --param port=8081 --yes
143
143
  arclith-cli add-adapter --capability llm --adapter lmstudio --param model_name=qwen/qwen3.5-9b --yes
144
- arclith-cli add-adapter --capability agent --adapter langgraph --param graph_name=recipe_agent --yes
144
+ arclith-cli add-adapter --capability agent --adapter langgraph --param graph_name=recipe_agent --param stream_mode=updates,custom --yes
145
145
  arclith-cli add-adapter --capability observability --adapter langsmith
146
146
  arclith-cli add-adapter --capability observability --adapter opentelemetry --param service_name=my_recipe_service --yes
147
147
  arclith-cli add-adapter --capability runtime --adapter docker-image --yes
@@ -166,7 +166,7 @@ arclith-cli add-adapter --capability repository --adapter memory --entity Recipe
166
166
  - `lmstudio` → `model_name`, `base_url`, `api_key`
167
167
  - `openai` → `model_name`, `base_url`, `OPENAI_API_KEY`
168
168
  - `anthropic` → `model_name`, `ANTHROPIC_API_KEY`
169
- - `langgraph` → `graph_name`
169
+ - `langgraph` → `graph_name`, `stream_mode`
170
170
  - `langsmith` → `tracing`, `project`, `endpoint`, `LANGSMITH_API_KEY`
171
171
  - `opentelemetry` → `service_name`, `endpoint`, `traces_endpoint`, `metrics_endpoint`, `protocol`, `traces`, `metrics`, `instrument_fastapi`
172
172
  - `command-bus/rabbitmq` → `url`, `exchange`, `exchange_type`, `queue`, `routing_key`, `prefetch`, `consumer_name`, `concurrency`, `publisher_confirms`, `durable`, `retry_enabled`, `retry_requeue`, `dead_letter_exchange`, `dead_letter_routing_key`
@@ -240,7 +240,9 @@ l'environnement, un fichier local de secrets ou Vault selon le resolver choisi.
240
240
  L'adapter `agent/langgraph` génère `langgraph.json`, `config/adapters/inbound/langgraph.yaml` et
241
241
  `src/<package>/adapters/inbound/langgraph/agent.py`. Le projet ne modifie ensuite que ce fichier pour
242
242
  son agent. Comme `fastapi` et `fastmcp`, LangGraph est configuré par son nom produit dans
243
- `AppConfig.langgraph`, sans `adapters.agent`. L'adapter `observability/langsmith` génère
243
+ `AppConfig.langgraph`, sans `adapters.agent`. `stream_mode` vaut `updates` par défaut et accepte une
244
+ liste CSV comme `updates,custom` pour exposer les sorties de nodes et les événements
245
+ `get_stream_writer()`. L'adapter `observability/langsmith` génère
244
246
  `config/adapters/outbound/langsmith.yaml`, l'ajoute à `observability.enabled`, met
245
247
  à jour `.env` et ajoute `.env` au `.gitignore` si besoin. LangSmith Studio devient l'endroit standard
246
248
  pour tester les agents. Une `LANGSMITH_API_KEY` déjà présente est conservée si aucune nouvelle valeur
@@ -0,0 +1 @@
1
+ __version__ = "0.14.0"
@@ -686,11 +686,19 @@ def _file_template_vars(
686
686
  "package_path": package_path,
687
687
  "langgraph_entrypoint": langgraph_entrypoint,
688
688
  "graph_name": graph_name,
689
+ "stream_mode_yaml": _langgraph_stream_mode_yaml(str(params.get("stream_mode") or "updates")),
689
690
  "secret_template_yaml": _secret_template_yaml(str(params.get("field_path") or "")),
690
691
  "secret_chain_yaml": _secret_chain_yaml(str(params.get("resolvers") or "")),
691
692
  }
692
693
 
693
694
 
695
+ def _langgraph_stream_mode_yaml(stream_mode: str) -> str:
696
+ values = _split_csv_values(stream_mode)
697
+ if len(values) == 1:
698
+ return f'"{values[0]}"'
699
+ return yaml.safe_dump(values, default_flow_style=True, sort_keys=False).strip()
700
+
701
+
694
702
  def _secret_template_yaml(field_path: str) -> str:
695
703
  keys = [key for key in field_path.split(".") if key]
696
704
  if not keys:
@@ -287,6 +287,226 @@ multitenant: false
287
287
  ),
288
288
  )
289
289
 
290
+ STORAGE_CAPABILITY = CapabilitySpec(
291
+ name="storage",
292
+ layer="outbound",
293
+ description="Stockage de fichiers et blobs derrière un port FileStoragePort.",
294
+ activation_config_key=None,
295
+ adapters=(
296
+ AdapterSpec(
297
+ name="filesystem",
298
+ capability="storage",
299
+ layer="outbound",
300
+ description="Stockage fichier local, typiquement monté dans Docker via un volume.",
301
+ config_path="config/adapters/outbound/storage.yaml",
302
+ config_template="""\
303
+ adapter: filesystem
304
+ root_path: "{root_path}"
305
+ prefix: "{prefix}"
306
+ create_root: {create_root}
307
+ multitenant: {multitenant}
308
+ """,
309
+ parameters=(
310
+ ParameterSpec(
311
+ name="root_path",
312
+ kind="string",
313
+ prompt="Chemin conteneur du dossier de fichiers",
314
+ default="/data/files",
315
+ ),
316
+ ParameterSpec(
317
+ name="prefix",
318
+ kind="string",
319
+ prompt="Préfixe d'objets",
320
+ default="",
321
+ ),
322
+ ParameterSpec(
323
+ name="create_root",
324
+ kind="boolean",
325
+ prompt="Créer le dossier racine au démarrage",
326
+ default=True,
327
+ ),
328
+ ParameterSpec(
329
+ name="multitenant",
330
+ kind="boolean",
331
+ prompt="Résoudre le stockage par tenant",
332
+ default=False,
333
+ ),
334
+ ),
335
+ entity_scoped=False,
336
+ ),
337
+ AdapterSpec(
338
+ name="s3",
339
+ capability="storage",
340
+ layer="outbound",
341
+ description="Stockage objet AWS S3 compatible MinIO via endpoint custom.",
342
+ config_path="config/adapters/outbound/storage.yaml",
343
+ config_template="""\
344
+ adapter: s3
345
+ # Credentials: chaîne AWS par défaut, variables d'environnement, rôle IAM ou TenantContext.
346
+ bucket_name: "{bucket_name}"
347
+ prefix: "{prefix}"
348
+ region_name: "{region_name}"
349
+ endpoint_url: {endpoint_url}
350
+ force_path_style: {force_path_style}
351
+ multitenant: {multitenant}
352
+ """,
353
+ parameters=(
354
+ ParameterSpec(
355
+ name="bucket_name",
356
+ kind="string",
357
+ prompt="Bucket S3",
358
+ default="my-bucket",
359
+ ),
360
+ ParameterSpec(
361
+ name="prefix",
362
+ kind="string",
363
+ prompt="Préfixe d'objets",
364
+ default="",
365
+ ),
366
+ ParameterSpec(
367
+ name="region_name",
368
+ kind="string",
369
+ prompt="Région AWS",
370
+ default="eu-west-3",
371
+ ),
372
+ ParameterSpec(
373
+ name="endpoint_url",
374
+ kind="string",
375
+ prompt="Endpoint S3 custom ou null",
376
+ default="null",
377
+ ),
378
+ ParameterSpec(
379
+ name="force_path_style",
380
+ kind="boolean",
381
+ prompt="Forcer le path-style S3",
382
+ default=False,
383
+ ),
384
+ ParameterSpec(
385
+ name="multitenant",
386
+ kind="boolean",
387
+ prompt="Résoudre le stockage par tenant",
388
+ default=False,
389
+ ),
390
+ ),
391
+ entity_scoped=False,
392
+ ),
393
+ AdapterSpec(
394
+ name="azure-blob",
395
+ capability="storage",
396
+ layer="outbound",
397
+ description="Stockage objet Azure Blob Storage.",
398
+ config_path="config/adapters/outbound/storage.yaml",
399
+ config_template="""\
400
+ adapter: azure-blob
401
+ account_url: "{account_url}"
402
+ container_name: "{container_name}"
403
+ prefix: "{prefix}"
404
+ connection_string: null
405
+ account_key: null
406
+ sas_token: null
407
+ use_default_credential: {use_default_credential}
408
+ multitenant: {multitenant}
409
+ # Credentials: mappez connection_string, account_key ou sas_token via
410
+ # config/secrets.yaml, Vault, ou TenantContext. use_default_credential=true
411
+ # délègue à DefaultAzureCredential du SDK Azure.
412
+ """,
413
+ secret_mappings=(
414
+ SecretMappingSpec(
415
+ field_path="adapters.storage.connection_string",
416
+ secret_key="AZURE_STORAGE_CONNECTION_STRING",
417
+ ),
418
+ SecretMappingSpec(
419
+ field_path="adapters.storage.account_key",
420
+ secret_key="AZURE_STORAGE_ACCOUNT_KEY",
421
+ ),
422
+ SecretMappingSpec(
423
+ field_path="adapters.storage.sas_token",
424
+ secret_key="AZURE_STORAGE_SAS_TOKEN",
425
+ ),
426
+ ),
427
+ parameters=(
428
+ ParameterSpec(
429
+ name="account_url",
430
+ kind="string",
431
+ prompt="URL du compte Azure Blob",
432
+ default="https://<account>.blob.core.windows.net",
433
+ ),
434
+ ParameterSpec(
435
+ name="container_name",
436
+ kind="string",
437
+ prompt="Container Azure Blob",
438
+ default="my-container",
439
+ ),
440
+ ParameterSpec(
441
+ name="prefix",
442
+ kind="string",
443
+ prompt="Préfixe d'objets",
444
+ default="",
445
+ ),
446
+ ParameterSpec(
447
+ name="use_default_credential",
448
+ kind="boolean",
449
+ prompt="Utiliser DefaultAzureCredential",
450
+ default=False,
451
+ ),
452
+ ParameterSpec(
453
+ name="multitenant",
454
+ kind="boolean",
455
+ prompt="Résoudre le stockage par tenant",
456
+ default=False,
457
+ ),
458
+ ),
459
+ entity_scoped=False,
460
+ ),
461
+ AdapterSpec(
462
+ name="gcs",
463
+ capability="storage",
464
+ layer="outbound",
465
+ description="Stockage objet Google Cloud Storage.",
466
+ config_path="config/adapters/outbound/storage.yaml",
467
+ config_template="""\
468
+ adapter: gcs
469
+ bucket_name: "{bucket_name}"
470
+ prefix: "{prefix}"
471
+ project_id: {project_id}
472
+ credentials_path: null
473
+ credentials_json: null
474
+ credentials_json_b64: null
475
+ multitenant: {multitenant}
476
+ # Credentials: use ADC/GOOGLE_APPLICATION_CREDENTIALS, config/secrets.yaml,
477
+ # Vault, or TenantContext. Never commit service account JSON.
478
+ """,
479
+ parameters=(
480
+ ParameterSpec(
481
+ name="bucket_name",
482
+ kind="string",
483
+ prompt="Bucket Google Cloud Storage",
484
+ default="my-bucket",
485
+ ),
486
+ ParameterSpec(
487
+ name="prefix",
488
+ kind="string",
489
+ prompt="Préfixe d'objets",
490
+ default="",
491
+ ),
492
+ ParameterSpec(
493
+ name="project_id",
494
+ kind="string",
495
+ prompt="Project ID GCP ou null",
496
+ default="null",
497
+ ),
498
+ ParameterSpec(
499
+ name="multitenant",
500
+ kind="boolean",
501
+ prompt="Résoudre le stockage par tenant",
502
+ default=False,
503
+ ),
504
+ ),
505
+ entity_scoped=False,
506
+ ),
507
+ ),
508
+ )
509
+
290
510
  CACHE_CAPABILITY = CapabilitySpec(
291
511
  name="cache",
292
512
  layer="outbound",
@@ -1263,6 +1483,7 @@ name: "{graph_name}"
1263
1483
  graph: "{graph_name}"
1264
1484
  entrypoint: "{langgraph_entrypoint}"
1265
1485
  env: ".env"
1486
+ stream_mode: {stream_mode_yaml}
1266
1487
  """,
1267
1488
  file_templates=(
1268
1489
  FileTemplateSpec(
@@ -1287,6 +1508,7 @@ env: ".env"
1287
1508
  from typing import Any, TypedDict
1288
1509
 
1289
1510
  from arclith import Arclith
1511
+ from langgraph.config import get_stream_writer
1290
1512
  from langgraph.graph import END, START
1291
1513
 
1292
1514
 
@@ -1300,6 +1522,8 @@ arclith = Arclith("config")
1300
1522
  # Template minimal volontaire: remplacer AgentState, run_agent et les edges par
1301
1523
  # l'état, les noeuds et les transitions propres au projet.
1302
1524
  async def run_agent(state: AgentState) -> AgentState:
1525
+ writer = get_stream_writer()
1526
+ writer({{"kind": "progress", "stage": "agent.started", "message": "Agent node started."}})
1303
1527
  return state
1304
1528
 
1305
1529
 
@@ -1320,6 +1544,14 @@ agent = arclith.langgraph(AgentState, register_agent, name="{graph_name}")
1320
1544
  prompt="Nom du graphe LangGraph",
1321
1545
  default="agent",
1322
1546
  ),
1547
+ ParameterSpec(
1548
+ name="stream_mode",
1549
+ kind="string",
1550
+ prompt="Mode(s) de streaming LangGraph",
1551
+ default="updates",
1552
+ choices=("values", "updates", "custom", "messages", "checkpoints", "tasks", "debug"),
1553
+ csv_choices=True,
1554
+ ),
1323
1555
  ),
1324
1556
  entity_scoped=False,
1325
1557
  ),
@@ -1478,6 +1710,7 @@ OTEL_EXPORTER_OTLP_HEADERS={headers}
1478
1710
 
1479
1711
  CAPABILITY_CATALOG = (
1480
1712
  REPOSITORY_CAPABILITY,
1713
+ STORAGE_CAPABILITY,
1481
1714
  CACHE_CAPABILITY,
1482
1715
  LOGGER_CAPABILITY,
1483
1716
  SECRETS_CAPABILITY,
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "arclith-cli"
7
- version = "0.13.0"
7
+ version = "0.14.0"
8
8
  description = "CLI scaffolding tool for arclith — hexagonal architecture framework"
9
9
  license = { file = "LICENSE" }
10
10
  requires-python = ">=3.13"
@@ -16,7 +16,7 @@ dependencies = [
16
16
  "typer>=0.15.0",
17
17
  "rich>=13.0.0",
18
18
  "httpx>=0.27.0",
19
- "arclith>=0.16.0",
19
+ "arclith>=0.17.0",
20
20
  ]
21
21
 
22
22
  [tool.uv.sources]
@@ -39,7 +39,7 @@ if grep -q '\[tool\.uv\.sources\]' pyproject.toml; then
39
39
  fi
40
40
 
41
41
  ARCLITH_VERSION=$(grep 'arclith\[' pyproject.toml | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
42
- MIN_ARCLITH_VERSION="0.16.0"
42
+ MIN_ARCLITH_VERSION="0.17.0"
43
43
  if version_lt "$ARCLITH_VERSION" "$MIN_ARCLITH_VERSION"; then
44
44
  echo -e "${RED}✗ FAIL: arclith version < $MIN_ARCLITH_VERSION (found: $ARCLITH_VERSION)${NC}"
45
45
  exit 1
@@ -868,6 +868,141 @@ def test_add_lmstudio_llm_adapter_generates_lm_config_only(tmp_path: Path) -> No
868
868
  assert arclith.config.adapters.lm.base_url == "http://127.0.0.1:1234/v1"
869
869
 
870
870
 
871
+ @pytest.mark.parametrize(
872
+ ("adapter", "params", "expected"),
873
+ [
874
+ (
875
+ "filesystem",
876
+ {
877
+ "root_path": "/data/files",
878
+ "prefix": "uploads",
879
+ "create_root": "true",
880
+ "multitenant": "false",
881
+ },
882
+ {
883
+ "adapter": "filesystem",
884
+ "root_path": "/data/files",
885
+ "prefix": "uploads",
886
+ "create_root": True,
887
+ "multitenant": False,
888
+ },
889
+ ),
890
+ (
891
+ "s3",
892
+ {
893
+ "bucket_name": "arclith-files",
894
+ "prefix": "uploads",
895
+ "region_name": "eu-west-3",
896
+ "endpoint_url": "null",
897
+ "force_path_style": "false",
898
+ "multitenant": "false",
899
+ },
900
+ {
901
+ "adapter": "s3",
902
+ "bucket_name": "arclith-files",
903
+ "prefix": "uploads",
904
+ "region_name": "eu-west-3",
905
+ "endpoint_url": None,
906
+ "force_path_style": False,
907
+ "multitenant": False,
908
+ },
909
+ ),
910
+ (
911
+ "azure-blob",
912
+ {
913
+ "account_url": "https://account.blob.core.windows.net",
914
+ "container_name": "arclith-files",
915
+ "prefix": "uploads",
916
+ "use_default_credential": "false",
917
+ "multitenant": "false",
918
+ },
919
+ {
920
+ "adapter": "azure-blob",
921
+ "account_url": "https://account.blob.core.windows.net",
922
+ "container_name": "arclith-files",
923
+ "prefix": "uploads",
924
+ "connection_string": None,
925
+ "account_key": None,
926
+ "sas_token": None,
927
+ "use_default_credential": False,
928
+ "multitenant": False,
929
+ },
930
+ ),
931
+ (
932
+ "gcs",
933
+ {
934
+ "bucket_name": "arclith-files",
935
+ "prefix": "uploads",
936
+ "project_id": "null",
937
+ "multitenant": "false",
938
+ },
939
+ {
940
+ "adapter": "gcs",
941
+ "bucket_name": "arclith-files",
942
+ "prefix": "uploads",
943
+ "project_id": None,
944
+ "credentials_path": None,
945
+ "credentials_json": None,
946
+ "credentials_json_b64": None,
947
+ "multitenant": False,
948
+ },
949
+ ),
950
+ ],
951
+ )
952
+ def test_add_storage_adapter_generates_loadable_config_only(
953
+ tmp_path: Path,
954
+ adapter: str,
955
+ params: dict[str, str],
956
+ expected: dict[str, object],
957
+ ) -> None:
958
+ project_dir = _minimal_project(tmp_path)
959
+ config_path = project_dir / "config" / "adapters" / "outbound" / "storage.yaml"
960
+ adapter_params = dict(params)
961
+ expected_config = dict(expected)
962
+ if adapter == "filesystem":
963
+ root_path = project_dir / "files"
964
+ adapter_params["root_path"] = str(root_path)
965
+ expected_config["root_path"] = str(root_path)
966
+
967
+ add_adapter_cmd(
968
+ project_dir=project_dir,
969
+ capability_name="storage",
970
+ adapter=adapter,
971
+ adapter_params=adapter_params,
972
+ yes=True,
973
+ )
974
+ config = yaml.safe_load(config_path.read_text(encoding="utf-8"))
975
+
976
+ assert config == expected_config
977
+ assert "storage:" not in (project_dir / "config" / "adapters" / "adapters.yaml").read_text(
978
+ encoding="utf-8"
979
+ )
980
+ if adapter == "azure-blob":
981
+ secrets = yaml.safe_load(
982
+ (project_dir / "config" / "secrets.yaml").read_text(encoding="utf-8")
983
+ )
984
+ assert secrets["resolver"] == "env"
985
+ assert secrets["mappings"] == {
986
+ "adapters.storage.connection_string": "AZURE_STORAGE_CONNECTION_STRING",
987
+ "adapters.storage.account_key": "AZURE_STORAGE_ACCOUNT_KEY",
988
+ "adapters.storage.sas_token": "AZURE_STORAGE_SAS_TOKEN",
989
+ }
990
+ package_root = project_dir / "src" / "demo_service"
991
+ assert not (package_root / "adapters" / "outbound" / adapter).exists()
992
+
993
+ from arclith import Arclith
994
+
995
+ arclith = Arclith(project_dir / "config")
996
+
997
+ assert arclith.config.adapters.storage is not None
998
+ assert arclith.config.adapters.storage.adapter == adapter
999
+ assert arclith.config.adapters.storage.prefix == "uploads"
1000
+ if adapter == "filesystem":
1001
+ from arclith.adapters.outbound.filesystem import FilesystemFileStorage
1002
+
1003
+ assert isinstance(arclith.file_storage(), FilesystemFileStorage)
1004
+
1005
+
871
1006
  def test_add_openai_llm_adapter_generates_secret_mapping(
872
1007
  tmp_path: Path,
873
1008
  monkeypatch: pytest.MonkeyPatch,
@@ -1147,7 +1282,7 @@ def test_add_langgraph_agent_adapter_generates_runtime_entrypoint(tmp_path: Path
1147
1282
  project_dir=project_dir,
1148
1283
  capability_name="agent",
1149
1284
  adapter="langgraph",
1150
- adapter_params={"graph_name": "todo_agent"},
1285
+ adapter_params={"graph_name": "todo_agent", "stream_mode": "updates,custom"},
1151
1286
  yes=True,
1152
1287
  )
1153
1288
 
@@ -1173,8 +1308,10 @@ def test_add_langgraph_agent_adapter_generates_runtime_entrypoint(tmp_path: Path
1173
1308
  assert "agent:" not in adapters_yaml
1174
1309
  assert 'name: "todo_agent"' in langgraph_config
1175
1310
  assert 'entrypoint: "./src/demo_service/adapters/inbound/langgraph/agent.py:agent"' in langgraph_config
1311
+ assert "stream_mode: [updates, custom]" in langgraph_config
1176
1312
  generated_agent = agent_file.read_text(encoding="utf-8")
1177
1313
  assert "Template minimal volontaire" in generated_agent
1314
+ assert "get_stream_writer" in generated_agent
1178
1315
  assert "agent = arclith.langgraph(AgentState, register_agent, name=\"todo_agent\")" in generated_agent
1179
1316
  assert not (package_root / "adapters" / "outbound" / "langgraph").exists()
1180
1317
 
@@ -1209,6 +1346,10 @@ async def test_add_langgraph_agent_adapter_generates_compilable_minimal_agent(
1209
1346
  try:
1210
1347
  spec.loader.exec_module(module)
1211
1348
  assert await module.agent.ainvoke({"messages": []}) == {"messages": []}
1349
+ events = [event async for event in module.agent.astream({"messages": []}, stream_mode="custom")]
1350
+ assert events == [
1351
+ {"kind": "progress", "stage": "agent.started", "message": "Agent node started."}
1352
+ ]
1212
1353
  finally:
1213
1354
  sys.modules.pop(spec.name, None)
1214
1355
 
@@ -393,7 +393,7 @@ def test_agent_capability_catalog_declares_langgraph() -> None:
393
393
  "{package_path}/adapters/inbound/langgraph/__init__.py",
394
394
  "{package_path}/adapters/inbound/langgraph/agent.py",
395
395
  ]
396
- assert [parameter.name for parameter in langgraph.parameters] == ["graph_name"]
396
+ assert [parameter.name for parameter in langgraph.parameters] == ["graph_name", "stream_mode"]
397
397
 
398
398
 
399
399
  def test_repository_adapter_specs_include_config_and_parameters() -> None:
@@ -432,6 +432,67 @@ def test_repository_adapter_specs_include_config_and_parameters() -> None:
432
432
  ]
433
433
 
434
434
 
435
+ def test_storage_capability_catalog_declares_object_storage_adapters() -> None:
436
+ capability = get_capability("storage")
437
+
438
+ assert capability is not None
439
+ assert capability.layer == "outbound"
440
+ assert capability.activation_config_key is None
441
+ assert capability.adapter_names() == ("filesystem", "s3", "azure-blob", "gcs")
442
+
443
+ filesystem = capability.get_adapter("filesystem")
444
+ s3 = capability.get_adapter("s3")
445
+ azure_blob = capability.get_adapter("azure-blob")
446
+ gcs = capability.get_adapter("gcs")
447
+
448
+ assert filesystem is not None
449
+ assert filesystem.config_path == "config/adapters/outbound/storage.yaml"
450
+ assert filesystem.entity_scoped is False
451
+ assert [parameter.name for parameter in filesystem.parameters] == [
452
+ "root_path",
453
+ "prefix",
454
+ "create_root",
455
+ "multitenant",
456
+ ]
457
+ assert s3 is not None
458
+ assert s3.config_path == "config/adapters/outbound/storage.yaml"
459
+ assert [parameter.name for parameter in s3.parameters] == [
460
+ "bucket_name",
461
+ "prefix",
462
+ "region_name",
463
+ "endpoint_url",
464
+ "force_path_style",
465
+ "multitenant",
466
+ ]
467
+ assert azure_blob is not None
468
+ assert azure_blob.config_path == "config/adapters/outbound/storage.yaml"
469
+ assert [parameter.name for parameter in azure_blob.parameters] == [
470
+ "account_url",
471
+ "container_name",
472
+ "prefix",
473
+ "use_default_credential",
474
+ "multitenant",
475
+ ]
476
+ assert [mapping.field_path for mapping in azure_blob.secret_mappings] == [
477
+ "adapters.storage.connection_string",
478
+ "adapters.storage.account_key",
479
+ "adapters.storage.sas_token",
480
+ ]
481
+ assert [mapping.secret_key for mapping in azure_blob.secret_mappings] == [
482
+ "AZURE_STORAGE_CONNECTION_STRING",
483
+ "AZURE_STORAGE_ACCOUNT_KEY",
484
+ "AZURE_STORAGE_SAS_TOKEN",
485
+ ]
486
+ assert gcs is not None
487
+ assert gcs.config_path == "config/adapters/outbound/storage.yaml"
488
+ assert [parameter.name for parameter in gcs.parameters] == [
489
+ "bucket_name",
490
+ "prefix",
491
+ "project_id",
492
+ "multitenant",
493
+ ]
494
+
495
+
435
496
  def test_capability_catalog_is_json_serializable() -> None:
436
497
  payload = capability_catalog_as_dict()
437
498
 
@@ -439,6 +500,10 @@ def test_capability_catalog_is_json_serializable() -> None:
439
500
 
440
501
  assert "repository" in encoded
441
502
  assert "mongodb" in encoded
503
+ assert "storage" in encoded
504
+ assert "filesystem" in encoded
505
+ assert "azure-blob" in encoded
506
+ assert "gcs" in encoded
442
507
  assert "cache" in encoded
443
508
  assert "redis" in encoded
444
509
  assert "logger" in encoded
@@ -501,6 +566,48 @@ def test_capabilities_command_outputs_json_catalog() -> None:
501
566
  "MARIADB_URL",
502
567
  "MARIADB_PASSWORD",
503
568
  ]
569
+ storage = payload_by_name["storage"]
570
+ assert storage["layer"] == "outbound"
571
+ assert storage["activation_config_key"] is None
572
+ assert [adapter["name"] for adapter in storage["adapters"]] == [
573
+ "filesystem",
574
+ "s3",
575
+ "azure-blob",
576
+ "gcs",
577
+ ]
578
+ filesystem = storage["adapters"][0]
579
+ assert filesystem["config_path"] == "config/adapters/outbound/storage.yaml"
580
+ assert filesystem["entity_scoped"] is False
581
+ assert [parameter["name"] for parameter in filesystem["parameters"]] == [
582
+ "root_path",
583
+ "prefix",
584
+ "create_root",
585
+ "multitenant",
586
+ ]
587
+ s3 = storage["adapters"][1]
588
+ assert s3["config_path"] == "config/adapters/outbound/storage.yaml"
589
+ assert [parameter["name"] for parameter in s3["parameters"]] == [
590
+ "bucket_name",
591
+ "prefix",
592
+ "region_name",
593
+ "endpoint_url",
594
+ "force_path_style",
595
+ "multitenant",
596
+ ]
597
+ azure_blob = storage["adapters"][2]
598
+ assert azure_blob["config_path"] == "config/adapters/outbound/storage.yaml"
599
+ assert [parameter["name"] for parameter in azure_blob["parameters"]] == [
600
+ "account_url",
601
+ "container_name",
602
+ "prefix",
603
+ "use_default_credential",
604
+ "multitenant",
605
+ ]
606
+ assert [mapping["field_path"] for mapping in azure_blob["secret_mappings"]] == [
607
+ "adapters.storage.connection_string",
608
+ "adapters.storage.account_key",
609
+ "adapters.storage.sas_token",
610
+ ]
504
611
  cache = payload_by_name["cache"]
505
612
  assert [adapter["name"] for adapter in cache["adapters"]] == ["memory", "redis"]
506
613
  assert cache["adapters"][0]["capability"] == "cache"
@@ -52,7 +52,7 @@ def test_init_project_creates_minimal_src_layout_without_entity(tmp_path: Path)
52
52
  "observability:\n"
53
53
  " enabled: []\n"
54
54
  )
55
- assert "arclith[fastapi,mcp]>=0.16.0" in (generated / "pyproject.toml").read_text(encoding="utf-8")
55
+ assert "arclith[fastapi,mcp]>=0.17.0" in (generated / "pyproject.toml").read_text(encoding="utf-8")
56
56
  assert (package_root / "domain" / "models" / "__init__.py").exists()
57
57
  assert (package_root / "domain" / "ports" / "inbound" / "__init__.py").exists()
58
58
  assert (package_root / "domain" / "ports" / "outbound" / "__init__.py").exists()
@@ -46,7 +46,7 @@ wheels = [
46
46
 
47
47
  [[package]]
48
48
  name = "arclith"
49
- version = "0.16.0"
49
+ version = "0.17.0"
50
50
  source = { editable = "../" }
51
51
  dependencies = [
52
52
  { name = "fastapi" },
@@ -63,6 +63,12 @@ requires-dist = [
63
63
  { name = "aio-pika", marker = "extra == 'rabbitmq'", specifier = ">=10.0.1,<11.0.0" },
64
64
  { name = "asyncmy", marker = "extra == 'all'", specifier = ">=0.2.10" },
65
65
  { name = "asyncmy", marker = "extra == 'mariadb'", specifier = ">=0.2.10" },
66
+ { name = "azure-identity", marker = "extra == 'all'", specifier = ">=1.25.1,<2.0.0" },
67
+ { name = "azure-identity", marker = "extra == 'azure-blob'", specifier = ">=1.25.1,<2.0.0" },
68
+ { name = "azure-storage-blob", marker = "extra == 'all'", specifier = ">=12.27.1,<13.0.0" },
69
+ { name = "azure-storage-blob", marker = "extra == 'azure-blob'", specifier = ">=12.27.1,<13.0.0" },
70
+ { name = "boto3", marker = "extra == 'all'", specifier = ">=1.43.70" },
71
+ { name = "boto3", marker = "extra == 's3'", specifier = ">=1.43.70" },
66
72
  { name = "duckdb", marker = "extra == 'all'", specifier = "==1.5.0" },
67
73
  { name = "duckdb", marker = "extra == 'duckdb'", specifier = "==1.5.0" },
68
74
  { name = "fastapi", specifier = ">=0.135.1" },
@@ -71,6 +77,8 @@ requires-dist = [
71
77
  { name = "fastmcp", specifier = ">=3.1.0" },
72
78
  { name = "fastmcp", marker = "extra == 'all'", specifier = "==3.1.0" },
73
79
  { name = "fastmcp", marker = "extra == 'mcp'", specifier = "==3.1.0" },
80
+ { name = "google-cloud-storage", marker = "extra == 'all'", specifier = ">=3.3.0,<4.0.0" },
81
+ { name = "google-cloud-storage", marker = "extra == 'gcs'", specifier = ">=3.3.0,<4.0.0" },
74
82
  { name = "httpx", marker = "extra == 'all'", specifier = ">=0.27.0" },
75
83
  { name = "httpx", marker = "extra == 'auth'", specifier = ">=0.27.0" },
76
84
  { name = "hvac", marker = "extra == 'all'", specifier = "==2.3.0" },
@@ -112,7 +120,7 @@ requires-dist = [
112
120
  { name = "uvicorn", marker = "extra == 'all'", specifier = "==0.41.0" },
113
121
  { name = "uvicorn", marker = "extra == 'fastapi'", specifier = "==0.41.0" },
114
122
  ]
115
- provides-extras = ["mongodb", "duckdb", "mariadb", "fastapi", "mcp", "vault", "cache", "auth", "rabbitmq", "langgraph", "opentelemetry", "all"]
123
+ provides-extras = ["mongodb", "duckdb", "mariadb", "fastapi", "mcp", "vault", "cache", "auth", "rabbitmq", "langgraph", "opentelemetry", "all", "azure-blob", "s3", "gcs"]
116
124
 
117
125
  [package.metadata.requires-dev]
118
126
  dev = [
@@ -134,7 +142,7 @@ docs = [
134
142
 
135
143
  [[package]]
136
144
  name = "arclith-cli"
137
- version = "0.13.0"
145
+ version = "0.14.0"
138
146
  source = { editable = "." }
139
147
  dependencies = [
140
148
  { name = "arclith" },
@@ -1 +0,0 @@
1
- __version__ = "0.13.0"
File without changes
File without changes
File without changes