arclith-cli 0.19.0__tar.gz → 0.20.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 (53) hide show
  1. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/PKG-INFO +2 -2
  2. arclith_cli-0.20.0/arclith_cli/__init__.py +1 -0
  3. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/adapter_templates.py +2 -1
  4. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/capabilities.py +5 -0
  5. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/capability_models.py +41 -0
  6. arclith_cli-0.20.0/arclith_cli/catalogs/channels.py +197 -0
  7. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/catalogs/persistence.py +12 -0
  8. arclith_cli-0.20.0/arclith_cli/catalogs/repository_facets.py +94 -0
  9. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/init_project.py +1 -1
  10. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/project_paths.py +18 -3
  11. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/pyproject.toml +2 -2
  12. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/tests/e2e_manual.sh +1 -1
  13. arclith_cli-0.20.0/tests/test_adapter_templates.py +20 -0
  14. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/tests/test_add_adapter.py +166 -7
  15. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/tests/test_capabilities.py +174 -0
  16. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/tests/test_core_scaffold.py +1 -1
  17. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/tests/test_embedding_capability.py +3 -3
  18. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/tests/test_project_paths.py +1 -0
  19. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/tests/test_vector_store_capability.py +1 -1
  20. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/uv.lock +4 -3
  21. arclith_cli-0.19.0/arclith_cli/__init__.py +0 -1
  22. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/.gitignore +0 -0
  23. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/LICENSE +0 -0
  24. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/Makefile +0 -0
  25. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/README.md +0 -0
  26. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/adapter_config.py +0 -0
  27. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/adapter_generator.py +0 -0
  28. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/adapter_parameters.py +0 -0
  29. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/adapter_rendering.py +0 -0
  30. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/adapter_selection.py +0 -0
  31. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/add_adapter.py +0 -0
  32. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/catalogs/__init__.py +0 -0
  33. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/catalogs/ai.py +0 -0
  34. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/catalogs/core.py +0 -0
  35. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/catalogs/observability.py +0 -0
  36. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/catalogs/security.py +0 -0
  37. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/catalogs/transports.py +0 -0
  38. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/core_scaffold.py +0 -0
  39. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/entity_scanner.py +0 -0
  40. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/export_config.py +0 -0
  41. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/main.py +0 -0
  42. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/new_project.py +0 -0
  43. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/recipe.py +0 -0
  44. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/recipe_cli.py +0 -0
  45. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/recipe_models.py +0 -0
  46. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/rename.py +0 -0
  47. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/runtime_templates.py +0 -0
  48. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/scaffold.py +0 -0
  49. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/updater.py +0 -0
  50. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/tests/__init__.py +0 -0
  51. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/tests/test_e2e_scaffold.py +0 -0
  52. {arclith_cli-0.19.0 → arclith_cli-0.20.0}/tests/test_recipe.py +0 -0
  53. {arclith_cli-0.19.0 → arclith_cli-0.20.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.19.0
3
+ Version: 0.20.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.22.0
189
+ Requires-Dist: arclith>=0.23.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
@@ -0,0 +1 @@
1
+ __version__ = "0.20.0"
@@ -205,7 +205,8 @@ _repository_registry: RepositoryRegistry[{pascal}, {pascal}Repository] = (
205
205
 
206
206
 
207
207
  def build_{snake}_service(arclith: Arclith) -> tuple[{pascal}Service, Logger]:
208
- arclith.logger.info("🗄️ Repository adapter selected", adapter=arclith.config.adapters.repository)
208
+ adapter = arclith.config.adapters.repository_adapter_for({pascal})
209
+ arclith.logger.info("🗄️ Repository adapter selected", adapter=adapter)
209
210
  repo: {pascal}Repository = arclith.repository({pascal}, registry=_repository_registry)
210
211
  return {pascal}Service(repo, arclith.logger, arclith.config.soft_delete.retention_days), arclith.logger
211
212
  """
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from typing import Any
4
4
 
5
5
  from arclith_cli.capability_models import (
6
+ AdapterFacets,
6
7
  AdapterProfileSpec,
7
8
  AdapterSpec,
8
9
  CapabilitySpec,
@@ -18,6 +19,7 @@ from arclith_cli.catalogs.ai import (
18
19
  EMBEDDING_CAPABILITY,
19
20
  LLM_CAPABILITY,
20
21
  )
22
+ from arclith_cli.catalogs.channels import CHANNEL_CAPABILITY
21
23
  from arclith_cli.catalogs.core import (
22
24
  CACHE_CAPABILITY,
23
25
  LOGGER_CAPABILITY,
@@ -48,9 +50,11 @@ __all__ = [
48
50
  "AGENT_PERSISTENCE_CAPABILITY",
49
51
  "API_CAPABILITY",
50
52
  "AUTH_CAPABILITY",
53
+ "AdapterFacets",
51
54
  "AdapterProfileSpec",
52
55
  "AdapterSpec",
53
56
  "CACHE_CAPABILITY",
57
+ "CHANNEL_CAPABILITY",
54
58
  "CAPABILITY_CATALOG",
55
59
  "COMMAND_BUS_CAPABILITY",
56
60
  "CapabilitySpec",
@@ -91,6 +95,7 @@ CAPABILITY_CATALOG = (
91
95
  PROBE_CAPABILITY,
92
96
  HTTP_CAPABILITY,
93
97
  COMMAND_BUS_CAPABILITY,
98
+ CHANNEL_CAPABILITY,
94
99
  RUNTIME_CAPABILITY,
95
100
  AUTH_CAPABILITY,
96
101
  TENANT_CAPABILITY,
@@ -5,6 +5,16 @@ from typing import Any, Literal
5
5
 
6
6
  ParameterKind = Literal["string", "boolean"]
7
7
  LayerKind = Literal["inbound", "outbound", "bidirectional", "runtime"]
8
+ StorageModel = Literal[
9
+ "memory",
10
+ "document",
11
+ "relational_json",
12
+ "embedded_analytics",
13
+ "relational_structured",
14
+ ]
15
+ RuntimeKind = Literal["in_process", "file", "server"]
16
+ TransactionSupport = Literal["none", "limited", "strong"]
17
+ SchemaStrategy = Literal["flexible", "json_table", "structured_tables"]
8
18
 
9
19
 
10
20
  @dataclass(frozen=True)
@@ -70,6 +80,30 @@ class AdapterProfileSpec:
70
80
  return {"name": self.name, "parameters": dict(self.parameters)}
71
81
 
72
82
 
83
+ @dataclass(frozen=True)
84
+ class AdapterFacets:
85
+ storage_model: StorageModel
86
+ runtime: RuntimeKind
87
+ production_ready: bool
88
+ multi_process: bool
89
+ transactions: TransactionSupport
90
+ schema_strategy: SchemaStrategy
91
+ recommended_for: tuple[str, ...]
92
+ limits: tuple[str, ...]
93
+
94
+ def to_dict(self) -> dict[str, Any]:
95
+ return {
96
+ "storage_model": self.storage_model,
97
+ "runtime": self.runtime,
98
+ "production_ready": self.production_ready,
99
+ "multi_process": self.multi_process,
100
+ "transactions": self.transactions,
101
+ "schema_strategy": self.schema_strategy,
102
+ "recommended_for": list(self.recommended_for),
103
+ "limits": list(self.limits),
104
+ }
105
+
106
+
73
107
  @dataclass(frozen=True)
74
108
  class AdapterSpec:
75
109
  name: str
@@ -90,6 +124,7 @@ class AdapterSpec:
90
124
  profiles: tuple[AdapterProfileSpec, ...] = ()
91
125
  dependency_extra: str | None = None
92
126
  entity_scoped: bool = True
127
+ facets: AdapterFacets | None = None
93
128
 
94
129
  def has_config(self) -> bool:
95
130
  return self.config_path is not None and bool(self.config_template)
@@ -137,6 +172,7 @@ class AdapterSpec:
137
172
  "profiles": [profile.to_dict() for profile in self.profiles],
138
173
  "dependency_extra": self.dependency_extra,
139
174
  "entity_scoped": self.entity_scoped,
175
+ "facets": self.facets.to_dict() if self.facets is not None else None,
140
176
  }
141
177
 
142
178
 
@@ -169,6 +205,7 @@ class CapabilitySpec:
169
205
 
170
206
 
171
207
  __all__ = [
208
+ "AdapterFacets",
172
209
  "AdapterProfileSpec",
173
210
  "AdapterSpec",
174
211
  "CapabilitySpec",
@@ -176,5 +213,9 @@ __all__ = [
176
213
  "LayerKind",
177
214
  "ParameterKind",
178
215
  "ParameterSpec",
216
+ "RuntimeKind",
217
+ "SchemaStrategy",
179
218
  "SecretMappingSpec",
219
+ "StorageModel",
220
+ "TransactionSupport",
180
221
  ]
@@ -0,0 +1,197 @@
1
+ from arclith_cli.capability_models import (
2
+ AdapterSpec,
3
+ CapabilitySpec,
4
+ ParameterSpec,
5
+ SecretMappingSpec,
6
+ )
7
+
8
+ CHANNEL_CAPABILITY = CapabilitySpec(
9
+ name="channel",
10
+ layer="bidirectional",
11
+ description="Messages conversationnels entrants et sortants normalisés.",
12
+ activation_config_key=None,
13
+ adapters=(
14
+ AdapterSpec(
15
+ name="memory",
16
+ capability="channel",
17
+ layer="bidirectional",
18
+ description="Fake déterministe en mémoire pour tests et prototypes locaux.",
19
+ config_path="config/adapters/bidirectional/memory.yaml",
20
+ config_template="""\
21
+ enabled: true
22
+ """,
23
+ entity_scoped=False,
24
+ ),
25
+ AdapterSpec(
26
+ name="webhook",
27
+ capability="channel",
28
+ layer="bidirectional",
29
+ description="Webhook HTTP générique signé, idempotent et provider-neutral.",
30
+ config_path="config/adapters/bidirectional/webhook.yaml",
31
+ config_template="""\
32
+ enabled: true
33
+ path: "{path}"
34
+ secret: null
35
+ signature_header: "{signature_header}"
36
+ timestamp_header: "{timestamp_header}"
37
+ signature_tolerance_seconds: {signature_tolerance_seconds}
38
+ idempotency_header: "{idempotency_header}"
39
+ event_ttl_seconds: {event_ttl_seconds}
40
+ max_payload_bytes: {max_payload_bytes}
41
+ metadata_allowlist: []
42
+ response_mode: "{response_mode}"
43
+ callback_url: {callback_url}
44
+ callback_allowed_host: {callback_allowed_host}
45
+ callback_timeout_seconds: {callback_timeout_seconds}
46
+ """,
47
+ secret_mappings=(
48
+ SecretMappingSpec(
49
+ field_path="adapters.channel.webhook.secret",
50
+ secret_key="ARCLITH_WEBHOOK_SECRET",
51
+ ),
52
+ ),
53
+ parameters=(
54
+ ParameterSpec(
55
+ name="path",
56
+ kind="string",
57
+ prompt="Route HTTP statique du webhook",
58
+ default="/channels/webhook",
59
+ ),
60
+ ParameterSpec(
61
+ name="signature_header",
62
+ kind="string",
63
+ prompt="Header de signature HMAC",
64
+ default="X-Arclith-Signature",
65
+ ),
66
+ ParameterSpec(
67
+ name="timestamp_header",
68
+ kind="string",
69
+ prompt="Header du timestamp signé",
70
+ default="X-Arclith-Timestamp",
71
+ ),
72
+ ParameterSpec(
73
+ name="signature_tolerance_seconds",
74
+ kind="string",
75
+ prompt="Tolérance de fraîcheur HMAC en secondes",
76
+ default="300",
77
+ ),
78
+ ParameterSpec(
79
+ name="idempotency_header",
80
+ kind="string",
81
+ prompt="Header de l'identifiant stable de l'événement",
82
+ default="X-Arclith-Event-Id",
83
+ ),
84
+ ParameterSpec(
85
+ name="event_ttl_seconds",
86
+ kind="string",
87
+ prompt="Durée de déduplication en secondes",
88
+ default="86400",
89
+ ),
90
+ ParameterSpec(
91
+ name="max_payload_bytes",
92
+ kind="string",
93
+ prompt="Taille maximale du payload en octets",
94
+ default="1048576",
95
+ ),
96
+ ParameterSpec(
97
+ name="response_mode",
98
+ kind="string",
99
+ prompt="Mode de réponse HTTP",
100
+ default="sync",
101
+ choices=("sync", "accepted", "callback"),
102
+ ),
103
+ ParameterSpec(
104
+ name="callback_url",
105
+ kind="string",
106
+ prompt="URL HTTPS du callback côté serveur (null hors mode callback)",
107
+ default="null",
108
+ ),
109
+ ParameterSpec(
110
+ name="callback_allowed_host",
111
+ kind="string",
112
+ prompt="Hostname exact autorisé pour le callback (null hors mode callback)",
113
+ default="null",
114
+ ),
115
+ ParameterSpec(
116
+ name="callback_timeout_seconds",
117
+ kind="string",
118
+ prompt="Timeout du callback sortant en secondes",
119
+ default="5.0",
120
+ ),
121
+ ),
122
+ dependency_extra="channel",
123
+ entity_scoped=False,
124
+ ),
125
+ AdapterSpec(
126
+ name="slack",
127
+ capability="channel",
128
+ layer="bidirectional",
129
+ description="Slack Events API signé avec réponses chat.postMessage.",
130
+ config_path="config/adapters/bidirectional/slack.yaml",
131
+ config_template="""\
132
+ enabled: true
133
+ path: "{path}"
134
+ signing_secret: null
135
+ bot_token: null
136
+ workspace_id: {workspace_id}
137
+ allowed_channel_ids: []
138
+ signature_tolerance_seconds: {signature_tolerance_seconds}
139
+ event_ttl_seconds: {event_ttl_seconds}
140
+ max_payload_bytes: {max_payload_bytes}
141
+ request_timeout_seconds: {request_timeout_seconds}
142
+ """,
143
+ secret_mappings=(
144
+ SecretMappingSpec(
145
+ field_path="adapters.channel.slack.signing_secret",
146
+ secret_key="ARCLITH_SLACK_SIGNING_SECRET",
147
+ ),
148
+ SecretMappingSpec(
149
+ field_path="adapters.channel.slack.bot_token",
150
+ secret_key="ARCLITH_SLACK_BOT_TOKEN",
151
+ ),
152
+ ),
153
+ parameters=(
154
+ ParameterSpec(
155
+ name="path",
156
+ kind="string",
157
+ prompt="Route HTTP statique des événements Slack",
158
+ default="/channels/slack/events",
159
+ ),
160
+ ParameterSpec(
161
+ name="workspace_id",
162
+ kind="string",
163
+ prompt="Workspace Slack exact autorisé (null pour tout accepter)",
164
+ default="null",
165
+ ),
166
+ ParameterSpec(
167
+ name="signature_tolerance_seconds",
168
+ kind="string",
169
+ prompt="Tolérance de fraîcheur de signature en secondes",
170
+ default="300",
171
+ ),
172
+ ParameterSpec(
173
+ name="event_ttl_seconds",
174
+ kind="string",
175
+ prompt="Durée de déduplication des event_id en secondes",
176
+ default="86400",
177
+ ),
178
+ ParameterSpec(
179
+ name="max_payload_bytes",
180
+ kind="string",
181
+ prompt="Taille maximale du payload en octets",
182
+ default="1048576",
183
+ ),
184
+ ParameterSpec(
185
+ name="request_timeout_seconds",
186
+ kind="string",
187
+ prompt="Timeout de chat.postMessage en secondes",
188
+ default="5.0",
189
+ ),
190
+ ),
191
+ dependency_extra="channel",
192
+ entity_scoped=False,
193
+ ),
194
+ ),
195
+ )
196
+
197
+ __all__ = ["CHANNEL_CAPABILITY"]
@@ -6,6 +6,13 @@ from arclith_cli.capability_models import (
6
6
  ParameterSpec,
7
7
  SecretMappingSpec,
8
8
  )
9
+ from arclith_cli.catalogs.repository_facets import (
10
+ DUCKDB_FACETS,
11
+ MARIADB_FACETS,
12
+ MEMORY_FACETS,
13
+ MONGODB_FACETS,
14
+ POSTGRESQL_FACETS,
15
+ )
9
16
 
10
17
  REPOSITORY_CAPABILITY = CapabilitySpec(
11
18
  name="repository",
@@ -18,6 +25,7 @@ REPOSITORY_CAPABILITY = CapabilitySpec(
18
25
  capability="repository",
19
26
  layer="outbound",
20
27
  description="Stockage volatile en mémoire pour dev, tests et smoke locaux.",
28
+ facets=MEMORY_FACETS,
21
29
  ),
22
30
  AdapterSpec(
23
31
  name="mongodb",
@@ -57,6 +65,7 @@ multitenant: {multitenant} # true = uri/db_name résolus par requête via JWT
57
65
  default=False,
58
66
  ),
59
67
  ),
68
+ facets=MONGODB_FACETS,
60
69
  ),
61
70
  AdapterSpec(
62
71
  name="duckdb",
@@ -76,6 +85,7 @@ path: {path}
76
85
  default="data/",
77
86
  ),
78
87
  ),
88
+ facets=DUCKDB_FACETS,
79
89
  ),
80
90
  AdapterSpec(
81
91
  name="mariadb",
@@ -142,6 +152,7 @@ multitenant: false
142
152
  default="",
143
153
  ),
144
154
  ),
155
+ facets=MARIADB_FACETS,
145
156
  ),
146
157
  AdapterSpec(
147
158
  name="postgresql",
@@ -221,6 +232,7 @@ multitenant: {multitenant}
221
232
  default=False,
222
233
  ),
223
234
  ),
235
+ facets=POSTGRESQL_FACETS,
224
236
  ),
225
237
  ),
226
238
  )
@@ -0,0 +1,94 @@
1
+ from arclith_cli.capability_models import AdapterFacets
2
+
3
+ MEMORY_FACETS = AdapterFacets(
4
+ storage_model="memory",
5
+ runtime="in_process",
6
+ production_ready=False,
7
+ multi_process=False,
8
+ transactions="none",
9
+ schema_strategy="flexible",
10
+ recommended_for=(
11
+ "tests unitaires",
12
+ "développement et smoke dans un seul processus",
13
+ ),
14
+ limits=(
15
+ "données perdues à l'arrêt",
16
+ "état non partagé entre processus",
17
+ ),
18
+ )
19
+
20
+ MONGODB_FACETS = AdapterFacets(
21
+ storage_model="document",
22
+ runtime="server",
23
+ production_ready=True,
24
+ multi_process=True,
25
+ transactions="limited",
26
+ schema_strategy="flexible",
27
+ recommended_for=(
28
+ "API, MCP et agents avec état partagé",
29
+ "données métier document-first ou à schéma évolutif",
30
+ ),
31
+ limits=(
32
+ "pas de modèle relationnel riche via Repository[T]",
33
+ "pas de transaction multi-opérations exposée par le port",
34
+ ),
35
+ )
36
+
37
+ DUCKDB_FACETS = AdapterFacets(
38
+ storage_model="embedded_analytics",
39
+ runtime="file",
40
+ production_ready=False,
41
+ multi_process=False,
42
+ transactions="limited",
43
+ schema_strategy="structured_tables",
44
+ recommended_for=(
45
+ "démonstrations et traitements analytiques locaux",
46
+ "données persistées dans des fichiers locaux",
47
+ ),
48
+ limits=(
49
+ "pas conçu comme base applicative serveur multi-process",
50
+ "concurrence d'écriture et formats de fichier à encadrer",
51
+ ),
52
+ )
53
+
54
+ MARIADB_FACETS = AdapterFacets(
55
+ storage_model="relational_json",
56
+ runtime="server",
57
+ production_ready=True,
58
+ multi_process=True,
59
+ transactions="strong",
60
+ schema_strategy="json_table",
61
+ recommended_for=(
62
+ "SI existant basé sur MariaDB",
63
+ "état partagé nécessitant un serveur SQL",
64
+ ),
65
+ limits=(
66
+ "entité stockée comme payload JSON générique",
67
+ "pas de relations, joins ou migrations métier via le port",
68
+ ),
69
+ )
70
+
71
+ POSTGRESQL_FACETS = AdapterFacets(
72
+ storage_model="relational_json",
73
+ runtime="server",
74
+ production_ready=True,
75
+ multi_process=True,
76
+ transactions="strong",
77
+ schema_strategy="json_table",
78
+ recommended_for=(
79
+ "services nécessitant un SQL serveur robuste",
80
+ "état partagé multi-process avec payload JSONB",
81
+ ),
82
+ limits=(
83
+ "entité stockée comme payload JSONB générique",
84
+ "pas de relations, joins ou migrations métier via le port",
85
+ ),
86
+ )
87
+
88
+ __all__ = [
89
+ "DUCKDB_FACETS",
90
+ "MARIADB_FACETS",
91
+ "MEMORY_FACETS",
92
+ "MONGODB_FACETS",
93
+ "POSTGRESQL_FACETS",
94
+ ]
@@ -81,7 +81,7 @@ def _framework_version() -> str:
81
81
  try:
82
82
  return version("arclith")
83
83
  except PackageNotFoundError:
84
- return "0.22.0"
84
+ return "0.23.0"
85
85
 
86
86
 
87
87
  def _create_package_layout(package_root: Path) -> None:
@@ -32,6 +32,10 @@ class ProjectPaths:
32
32
  def adapters_inbound(self) -> Path:
33
33
  return self.package_root / "adapters" / "inbound"
34
34
 
35
+ @property
36
+ def adapters_bidirectional(self) -> Path:
37
+ return self.package_root / "adapters" / "bidirectional"
38
+
35
39
  @property
36
40
  def containers(self) -> Path:
37
41
  return self.package_root / "infrastructure" / "containers"
@@ -52,7 +56,11 @@ def detect_project_paths(project_dir: Path) -> ProjectPaths:
52
56
  )
53
57
  if model_candidates:
54
58
  package_root = model_candidates[0]
55
- return ProjectPaths(root=project_dir, package_root=package_root, package_name=package_root.name)
59
+ return ProjectPaths(
60
+ root=project_dir,
61
+ package_root=package_root,
62
+ package_name=package_root.name,
63
+ )
56
64
 
57
65
  package_candidates = sorted(
58
66
  child
@@ -61,7 +69,11 @@ def detect_project_paths(project_dir: Path) -> ProjectPaths:
61
69
  )
62
70
  if package_candidates:
63
71
  package_root = package_candidates[0]
64
- return ProjectPaths(root=project_dir, package_root=package_root, package_name=package_root.name)
72
+ return ProjectPaths(
73
+ root=project_dir,
74
+ package_root=package_root,
75
+ package_name=package_root.name,
76
+ )
65
77
 
66
78
  return ProjectPaths(root=project_dir, package_root=project_dir, package_name=None)
67
79
 
@@ -70,4 +82,7 @@ def _looks_like_package_root(path: Path) -> bool:
70
82
  if (path / "__init__.py").exists():
71
83
  return True
72
84
 
73
- return any((path / layer).is_dir() for layer in ("domain", "application", "adapters", "infrastructure"))
85
+ return any(
86
+ (path / layer).is_dir()
87
+ for layer in ("domain", "application", "adapters", "infrastructure")
88
+ )
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "arclith-cli"
7
- version = "0.19.0"
7
+ version = "0.20.0"
8
8
  description = "CLI scaffolding tool for arclith — hexagonal architecture framework"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -17,7 +17,7 @@ dependencies = [
17
17
  "typer>=0.15.0",
18
18
  "rich>=13.0.0",
19
19
  "httpx>=0.27.0",
20
- "arclith>=0.22.0",
20
+ "arclith>=0.23.0",
21
21
  ]
22
22
 
23
23
  [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.22.0"
42
+ MIN_ARCLITH_VERSION="0.23.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
@@ -0,0 +1,20 @@
1
+ from arclith_cli.adapter_templates import render_container
2
+
3
+
4
+ def test_generated_container_logs_entity_repository_binding() -> None:
5
+ rendered = render_container(
6
+ "ChatThread",
7
+ "chat_thread",
8
+ ["mongodb"],
9
+ {
10
+ "application_import": "chat_service.application",
11
+ "domain_import": "chat_service.domain",
12
+ "adapters_import": "chat_service.adapters",
13
+ },
14
+ )
15
+
16
+ assert (
17
+ "adapter = arclith.config.adapters.repository_adapter_for(ChatThread)"
18
+ in rendered
19
+ )
20
+ assert "adapter=adapter)" in rendered
@@ -520,7 +520,7 @@ def test_add_langsmith_adds_dependency_extra_idempotently(tmp_path: Path) -> Non
520
520
  project_dir = _minimal_project(tmp_path)
521
521
  pyproject = project_dir / "pyproject.toml"
522
522
  pyproject.write_text(
523
- '[project]\ndependencies = ["arclith[fastapi,mcp]>=0.22.0"]\n',
523
+ '[project]\ndependencies = ["arclith[fastapi,mcp]>=0.23.0"]\n',
524
524
  encoding="utf-8",
525
525
  )
526
526
 
@@ -533,7 +533,7 @@ def test_add_langsmith_adds_dependency_extra_idempotently(tmp_path: Path) -> Non
533
533
  yes=True,
534
534
  )
535
535
 
536
- assert "arclith[fastapi,mcp,langsmith]>=0.22.0" in pyproject.read_text()
536
+ assert "arclith[fastapi,mcp,langsmith]>=0.23.0" in pyproject.read_text()
537
537
 
538
538
 
539
539
  def test_add_opentelemetry_adds_dependency_extra_idempotently(
@@ -542,7 +542,7 @@ def test_add_opentelemetry_adds_dependency_extra_idempotently(
542
542
  project_dir = _minimal_project(tmp_path)
543
543
  pyproject = project_dir / "pyproject.toml"
544
544
  pyproject.write_text(
545
- '[project]\ndependencies = ["arclith[fastapi,mcp]>=0.22.0"]\n',
545
+ '[project]\ndependencies = ["arclith[fastapi,mcp]>=0.23.0"]\n',
546
546
  encoding="utf-8",
547
547
  )
548
548
 
@@ -555,7 +555,7 @@ def test_add_opentelemetry_adds_dependency_extra_idempotently(
555
555
  yes=True,
556
556
  )
557
557
 
558
- assert "arclith[fastapi,mcp,opentelemetry]>=0.22.0" in pyproject.read_text()
558
+ assert "arclith[fastapi,mcp,opentelemetry]>=0.23.0" in pyproject.read_text()
559
559
 
560
560
 
561
561
  @pytest.mark.parametrize(
@@ -1032,6 +1032,165 @@ def test_add_command_bus_rabbitmq_adapter_rejects_unbounded_prefetch(
1032
1032
  assert not command_bus_path.exists()
1033
1033
 
1034
1034
 
1035
+ def test_add_memory_channel_generates_loadable_bidirectional_config(
1036
+ tmp_path: Path,
1037
+ ) -> None:
1038
+ project_dir = _minimal_project(tmp_path)
1039
+
1040
+ for _ in range(2):
1041
+ add_adapter_cmd(
1042
+ project_dir=project_dir,
1043
+ capability_name="channel",
1044
+ adapter="memory",
1045
+ yes=True,
1046
+ )
1047
+
1048
+ from arclith import Arclith, MemoryChannel
1049
+
1050
+ config_path = project_dir / "config" / "adapters" / "bidirectional" / "memory.yaml"
1051
+ app = Arclith(project_dir / "config")
1052
+
1053
+ assert config_path.read_text(encoding="utf-8") == "enabled: true\n"
1054
+ assert app.config.adapters.channel.configured_adapters() == ("memory",)
1055
+ assert isinstance(app.channel_sender("memory"), MemoryChannel)
1056
+ assert not (
1057
+ project_dir / "src" / "demo_service" / "adapters" / "bidirectional" / "memory"
1058
+ ).exists()
1059
+
1060
+
1061
+ def test_add_webhook_channel_generates_safe_loadable_config(
1062
+ tmp_path: Path,
1063
+ ) -> None:
1064
+ project_dir = _minimal_project(tmp_path)
1065
+ pyproject = project_dir / "pyproject.toml"
1066
+ pyproject.write_text(
1067
+ '[project]\ndependencies = ["arclith[fastapi]>=0.23.0"]\n',
1068
+ encoding="utf-8",
1069
+ )
1070
+
1071
+ for _ in range(2):
1072
+ add_adapter_cmd(
1073
+ project_dir=project_dir,
1074
+ capability_name="channel",
1075
+ adapter="webhook",
1076
+ adapter_params={
1077
+ "path": "/integrations/arclith",
1078
+ "signature_tolerance_seconds": "120",
1079
+ "event_ttl_seconds": "7200",
1080
+ "max_payload_bytes": "65536",
1081
+ },
1082
+ yes=True,
1083
+ )
1084
+
1085
+ from arclith import Arclith
1086
+
1087
+ config_path = project_dir / "config" / "adapters" / "bidirectional" / "webhook.yaml"
1088
+ config = yaml.safe_load(config_path.read_text(encoding="utf-8"))
1089
+ app = Arclith(project_dir / "config")
1090
+ secrets = yaml.safe_load(
1091
+ (project_dir / "config" / "secrets.yaml").read_text(encoding="utf-8")
1092
+ )
1093
+
1094
+ assert config["path"] == "/integrations/arclith"
1095
+ assert config["secret"] is None
1096
+ assert config["signature_tolerance_seconds"] == 120
1097
+ assert config["event_ttl_seconds"] == 7200
1098
+ assert config["max_payload_bytes"] == 65536
1099
+ assert config["response_mode"] == "sync"
1100
+ assert app.config.adapters.channel.configured_adapters() == ("webhook",)
1101
+ assert app.config.adapters.channel.webhook is not None
1102
+ assert secrets == {
1103
+ "resolver": "env",
1104
+ "mappings": {
1105
+ "adapters.channel.webhook.secret": "ARCLITH_WEBHOOK_SECRET",
1106
+ },
1107
+ }
1108
+ assert "arclith[fastapi,channel]>=0.23.0" in pyproject.read_text()
1109
+ assert "a-secure-webhook-secret" not in config_path.read_text()
1110
+
1111
+
1112
+ def test_add_webhook_channel_generates_loadable_callback_config(
1113
+ tmp_path: Path,
1114
+ ) -> None:
1115
+ project_dir = _minimal_project(tmp_path)
1116
+
1117
+ add_adapter_cmd(
1118
+ project_dir=project_dir,
1119
+ capability_name="channel",
1120
+ adapter="webhook",
1121
+ adapter_params={
1122
+ "response_mode": "callback",
1123
+ "callback_url": "https://hooks.example.test/arclith",
1124
+ "callback_allowed_host": "hooks.example.test",
1125
+ },
1126
+ yes=True,
1127
+ )
1128
+
1129
+ from arclith import Arclith, WebhookCallbackSender
1130
+
1131
+ app = Arclith(project_dir / "config")
1132
+ settings = app.config.adapters.channel.webhook
1133
+
1134
+ assert settings is not None
1135
+ assert settings.response_mode == "callback"
1136
+ assert settings.callback_url == "https://hooks.example.test/arclith"
1137
+ assert isinstance(app.channel_sender("webhook"), WebhookCallbackSender)
1138
+
1139
+
1140
+ def test_add_slack_channel_generates_safe_loadable_config(tmp_path: Path) -> None:
1141
+ project_dir = _minimal_project(tmp_path)
1142
+ pyproject = project_dir / "pyproject.toml"
1143
+ pyproject.write_text(
1144
+ '[project]\ndependencies = ["arclith[fastapi]>=0.23.0"]\n',
1145
+ encoding="utf-8",
1146
+ )
1147
+
1148
+ for _ in range(2):
1149
+ add_adapter_cmd(
1150
+ project_dir=project_dir,
1151
+ capability_name="channel",
1152
+ adapter="slack",
1153
+ adapter_params={
1154
+ "path": "/integrations/slack/events",
1155
+ "workspace_id": "T123ABC456",
1156
+ "signature_tolerance_seconds": "120",
1157
+ "event_ttl_seconds": "7200",
1158
+ "max_payload_bytes": "65536",
1159
+ "request_timeout_seconds": "2.5",
1160
+ },
1161
+ yes=True,
1162
+ )
1163
+
1164
+ from arclith import Arclith
1165
+
1166
+ config_path = project_dir / "config" / "adapters" / "bidirectional" / "slack.yaml"
1167
+ config = yaml.safe_load(config_path.read_text(encoding="utf-8"))
1168
+ app = Arclith(project_dir / "config")
1169
+ secrets = yaml.safe_load(
1170
+ (project_dir / "config" / "secrets.yaml").read_text(encoding="utf-8")
1171
+ )
1172
+
1173
+ assert config["path"] == "/integrations/slack/events"
1174
+ assert config["signing_secret"] is None
1175
+ assert config["bot_token"] is None
1176
+ assert config["workspace_id"] == "T123ABC456"
1177
+ assert config["allowed_channel_ids"] == []
1178
+ assert config["signature_tolerance_seconds"] == 120
1179
+ assert config["event_ttl_seconds"] == 7200
1180
+ assert config["max_payload_bytes"] == 65536
1181
+ assert config["request_timeout_seconds"] == 2.5
1182
+ assert app.config.adapters.channel.configured_adapters() == ("slack",)
1183
+ assert secrets == {
1184
+ "resolver": "env",
1185
+ "mappings": {
1186
+ "adapters.channel.slack.signing_secret": ("ARCLITH_SLACK_SIGNING_SECRET"),
1187
+ "adapters.channel.slack.bot_token": "ARCLITH_SLACK_BOT_TOKEN",
1188
+ },
1189
+ }
1190
+ assert "arclith[fastapi,channel]>=0.23.0" in pyproject.read_text()
1191
+ assert "xoxb-" not in config_path.read_text()
1192
+
1193
+
1035
1194
  def test_add_keycloak_auth_adapter_generates_loadable_inbound_config(
1036
1195
  tmp_path: Path,
1037
1196
  ) -> None:
@@ -1701,7 +1860,7 @@ def test_add_langgraph_persistence_enriches_config_and_dependency_without_overwr
1701
1860
  (project_dir / "pyproject.toml").write_text(
1702
1861
  """[project]
1703
1862
  name = "demo-service"
1704
- dependencies = ["arclith[fastapi,mcp]>=0.22.0"]
1863
+ dependencies = ["arclith[fastapi,mcp]>=0.23.0"]
1705
1864
  """,
1706
1865
  encoding="utf-8",
1707
1866
  )
@@ -1753,7 +1912,7 @@ dependencies = ["arclith[fastapi,mcp]>=0.22.0"]
1753
1912
  )
1754
1913
  pyproject = (project_dir / "pyproject.toml").read_text(encoding="utf-8")
1755
1914
  assert (
1756
- "arclith[fastapi,mcp,langgraph,langgraph-persistence-mongodb]>=0.22.0"
1915
+ "arclith[fastapi,mcp,langgraph,langgraph-persistence-mongodb]>=0.23.0"
1757
1916
  in pyproject
1758
1917
  )
1759
1918
 
@@ -1775,7 +1934,7 @@ def test_add_langgraph_persistence_requires_agent_capability(
1775
1934
  ) -> None:
1776
1935
  project_dir = _minimal_project(tmp_path)
1777
1936
  (project_dir / "pyproject.toml").write_text(
1778
- '[project]\ndependencies = ["arclith>=0.22.0"]\n', encoding="utf-8"
1937
+ '[project]\ndependencies = ["arclith>=0.23.0"]\n', encoding="utf-8"
1779
1938
  )
1780
1939
 
1781
1940
  with pytest.raises(typer.Exit):
@@ -49,6 +49,80 @@ def test_repository_capability_catalog_declares_standard_adapters() -> None:
49
49
  ]
50
50
 
51
51
 
52
+ def test_repository_adapter_facets_have_a_stable_serializable_shape() -> None:
53
+ capability = get_capability("repository")
54
+ assert capability is not None
55
+
56
+ expected_facets = {
57
+ "memory": (
58
+ "memory",
59
+ "in_process",
60
+ False,
61
+ False,
62
+ "none",
63
+ "flexible",
64
+ ),
65
+ "mongodb": (
66
+ "document",
67
+ "server",
68
+ True,
69
+ True,
70
+ "limited",
71
+ "flexible",
72
+ ),
73
+ "duckdb": (
74
+ "embedded_analytics",
75
+ "file",
76
+ False,
77
+ False,
78
+ "limited",
79
+ "structured_tables",
80
+ ),
81
+ "mariadb": (
82
+ "relational_json",
83
+ "server",
84
+ True,
85
+ True,
86
+ "strong",
87
+ "json_table",
88
+ ),
89
+ "postgresql": (
90
+ "relational_json",
91
+ "server",
92
+ True,
93
+ True,
94
+ "strong",
95
+ "json_table",
96
+ ),
97
+ }
98
+
99
+ for adapter in capability.adapters:
100
+ assert adapter.facets is not None
101
+ payload = adapter.facets.to_dict()
102
+ assert set(payload) == {
103
+ "storage_model",
104
+ "runtime",
105
+ "production_ready",
106
+ "multi_process",
107
+ "transactions",
108
+ "schema_strategy",
109
+ "recommended_for",
110
+ "limits",
111
+ }
112
+ assert (
113
+ payload["storage_model"],
114
+ payload["runtime"],
115
+ payload["production_ready"],
116
+ payload["multi_process"],
117
+ payload["transactions"],
118
+ payload["schema_strategy"],
119
+ ) == expected_facets[adapter.name]
120
+ assert payload["recommended_for"]
121
+ assert all(isinstance(value, str) for value in payload["recommended_for"])
122
+ assert payload["limits"]
123
+ assert all(isinstance(value, str) for value in payload["limits"])
124
+
125
+
52
126
  def test_cache_capability_catalog_declares_memory_adapter() -> None:
53
127
  capability = get_capability("cache")
54
128
 
@@ -317,6 +391,69 @@ def test_command_bus_capability_catalog_declares_rabbitmq() -> None:
317
391
  ]
318
392
 
319
393
 
394
+ def test_channel_capability_catalog_declares_supported_adapters() -> None:
395
+ capability = get_capability("channel")
396
+
397
+ assert capability is not None
398
+ assert capability.layer == "bidirectional"
399
+ assert capability.activation_config_key is None
400
+ assert capability.adapter_names() == ("memory", "webhook", "slack")
401
+ memory = capability.get_adapter("memory")
402
+ assert memory is not None
403
+ assert memory.layer == "bidirectional"
404
+ assert memory.config_path == "config/adapters/bidirectional/memory.yaml"
405
+ assert memory.dependency_extra is None
406
+ assert memory.entity_scoped is False
407
+ assert memory.parameters == ()
408
+ webhook = capability.get_adapter("webhook")
409
+ assert webhook is not None
410
+ assert webhook.layer == "bidirectional"
411
+ assert webhook.config_path == "config/adapters/bidirectional/webhook.yaml"
412
+ assert webhook.dependency_extra == "channel"
413
+ assert webhook.entity_scoped is False
414
+ assert [parameter.name for parameter in webhook.parameters] == [
415
+ "path",
416
+ "signature_header",
417
+ "timestamp_header",
418
+ "signature_tolerance_seconds",
419
+ "idempotency_header",
420
+ "event_ttl_seconds",
421
+ "max_payload_bytes",
422
+ "response_mode",
423
+ "callback_url",
424
+ "callback_allowed_host",
425
+ "callback_timeout_seconds",
426
+ ]
427
+ assert [mapping.field_path for mapping in webhook.secret_mappings] == [
428
+ "adapters.channel.webhook.secret"
429
+ ]
430
+ assert [mapping.secret_key for mapping in webhook.secret_mappings] == [
431
+ "ARCLITH_WEBHOOK_SECRET"
432
+ ]
433
+ slack = capability.get_adapter("slack")
434
+ assert slack is not None
435
+ assert slack.layer == "bidirectional"
436
+ assert slack.config_path == "config/adapters/bidirectional/slack.yaml"
437
+ assert slack.dependency_extra == "channel"
438
+ assert slack.entity_scoped is False
439
+ assert [parameter.name for parameter in slack.parameters] == [
440
+ "path",
441
+ "workspace_id",
442
+ "signature_tolerance_seconds",
443
+ "event_ttl_seconds",
444
+ "max_payload_bytes",
445
+ "request_timeout_seconds",
446
+ ]
447
+ assert [mapping.field_path for mapping in slack.secret_mappings] == [
448
+ "adapters.channel.slack.signing_secret",
449
+ "adapters.channel.slack.bot_token",
450
+ ]
451
+ assert [mapping.secret_key for mapping in slack.secret_mappings] == [
452
+ "ARCLITH_SLACK_SIGNING_SECRET",
453
+ "ARCLITH_SLACK_BOT_TOKEN",
454
+ ]
455
+
456
+
320
457
  def test_runtime_capability_catalog_declares_docker_image() -> None:
321
458
  capability = get_capability("runtime")
322
459
 
@@ -704,6 +841,22 @@ def test_capabilities_command_outputs_json_catalog() -> None:
704
841
  "postgresql",
705
842
  ]
706
843
  assert repository["adapters"][0]["entity_scoped"] is True
844
+ assert repository["adapters"][0]["facets"] == {
845
+ "storage_model": "memory",
846
+ "runtime": "in_process",
847
+ "production_ready": False,
848
+ "multi_process": False,
849
+ "transactions": "none",
850
+ "schema_strategy": "flexible",
851
+ "recommended_for": [
852
+ "tests unitaires",
853
+ "développement et smoke dans un seul processus",
854
+ ],
855
+ "limits": [
856
+ "données perdues à l'arrêt",
857
+ "état non partagé entre processus",
858
+ ],
859
+ }
707
860
  duckdb = repository["adapters"][2]
708
861
  assert duckdb["name"] == "duckdb"
709
862
  assert duckdb["config_path"] == "config/adapters/outbound/duckdb.yaml"
@@ -730,6 +883,7 @@ def test_capabilities_command_outputs_json_catalog() -> None:
730
883
  filesystem = storage["adapters"][0]
731
884
  assert filesystem["config_path"] == "config/adapters/outbound/storage.yaml"
732
885
  assert filesystem["entity_scoped"] is False
886
+ assert filesystem["facets"] is None
733
887
  assert [parameter["name"] for parameter in filesystem["parameters"]] == [
734
888
  "root_path",
735
889
  "prefix",
@@ -888,6 +1042,26 @@ def test_capabilities_command_outputs_json_catalog() -> None:
888
1042
  "dead_letter_exchange",
889
1043
  "dead_letter_routing_key",
890
1044
  ]
1045
+ channel = payload_by_name["channel"]
1046
+ assert channel["layer"] == "bidirectional"
1047
+ assert [adapter["name"] for adapter in channel["adapters"]] == [
1048
+ "memory",
1049
+ "webhook",
1050
+ "slack",
1051
+ ]
1052
+ memory_channel = channel["adapters"][0]
1053
+ assert memory_channel["config_path"] == (
1054
+ "config/adapters/bidirectional/memory.yaml"
1055
+ )
1056
+ assert memory_channel["dependency_extra"] is None
1057
+ webhook_channel = channel["adapters"][1]
1058
+ assert webhook_channel["config_path"] == (
1059
+ "config/adapters/bidirectional/webhook.yaml"
1060
+ )
1061
+ assert webhook_channel["dependency_extra"] == "channel"
1062
+ slack_channel = channel["adapters"][2]
1063
+ assert slack_channel["config_path"] == ("config/adapters/bidirectional/slack.yaml")
1064
+ assert slack_channel["dependency_extra"] == "channel"
891
1065
  runtime = payload_by_name["runtime"]
892
1066
  assert runtime["layer"] == "runtime"
893
1067
  assert [adapter["name"] for adapter in runtime["adapters"]] == ["docker-image"]
@@ -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.22.0" in (generated / "pyproject.toml").read_text(encoding="utf-8")
55
+ assert "arclith[fastapi,mcp]>=0.23.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()
@@ -15,7 +15,7 @@ def _minimal_project(tmp_path: Path) -> Path:
15
15
  )
16
16
  (project_dir / "pyproject.toml").write_text(
17
17
  '[project]\nname = "demo-service"\nversion = "0.1.0"\n'
18
- 'dependencies = ["arclith>=0.22.0"]\n',
18
+ 'dependencies = ["arclith>=0.23.0"]\n',
19
19
  encoding="utf-8",
20
20
  )
21
21
  return project_dir
@@ -197,7 +197,7 @@ def test_add_openai_compatible_embedding_generates_loadable_config_idempotently(
197
197
  assert app.config.adapters.embedding is not None
198
198
  assert app.config.adapters.embedding.model_name == "nomic-embed-text"
199
199
  assert isinstance(app.embedding(), OpenAICompatibleEmbeddingAdapter)
200
- assert "arclith[embedding]>=0.22.0" in (project_dir / "pyproject.toml").read_text(
200
+ assert "arclith[embedding]>=0.23.0" in (project_dir / "pyproject.toml").read_text(
201
201
  encoding="utf-8"
202
202
  )
203
203
 
@@ -244,7 +244,7 @@ def test_add_openai_embedding_generates_safe_loadable_config_idempotently(
244
244
  assert env_text == "EXISTING=value\n"
245
245
  assert "sk-" not in config_text + secrets_text + env_text
246
246
  assert ".env" in (project_dir / ".gitignore").read_text(encoding="utf-8")
247
- assert "arclith[embedding]>=0.22.0" in (project_dir / "pyproject.toml").read_text(
247
+ assert "arclith[embedding]>=0.23.0" in (project_dir / "pyproject.toml").read_text(
248
248
  encoding="utf-8"
249
249
  )
250
250
 
@@ -14,6 +14,7 @@ def test_detect_project_paths_uses_src_package(tmp_path: Path):
14
14
  assert paths.domain_models == package_root / "domain" / "models"
15
15
  assert paths.application_use_cases == package_root / "application" / "use_cases"
16
16
  assert paths.adapters_outbound == package_root / "adapters" / "outbound"
17
+ assert paths.adapters_bidirectional == package_root / "adapters" / "bidirectional"
17
18
  assert paths.containers == package_root / "infrastructure" / "containers"
18
19
  assert paths.import_path("domain", "models", "recipe") == "my_service.domain.models.recipe"
19
20
 
@@ -15,7 +15,7 @@ def _minimal_project(tmp_path: Path) -> Path:
15
15
  )
16
16
  (project_dir / "pyproject.toml").write_text(
17
17
  '[project]\nname = "demo-service"\nversion = "0.1.0"\n'
18
- 'dependencies = ["arclith>=0.22.0"]\n',
18
+ 'dependencies = ["arclith>=0.23.0"]\n',
19
19
  encoding="utf-8",
20
20
  )
21
21
  return project_dir
@@ -46,7 +46,7 @@ wheels = [
46
46
 
47
47
  [[package]]
48
48
  name = "arclith"
49
- version = "0.22.0"
49
+ version = "0.23.0"
50
50
  source = { editable = "../" }
51
51
  dependencies = [
52
52
  { name = "fastapi" },
@@ -83,6 +83,7 @@ requires-dist = [
83
83
  { name = "google-cloud-storage", marker = "extra == 'gcs'", specifier = ">=3.3.0,<4.0.0" },
84
84
  { name = "httpx", marker = "extra == 'all'", specifier = ">=0.27.0" },
85
85
  { name = "httpx", marker = "extra == 'auth'", specifier = ">=0.27.0" },
86
+ { name = "httpx", marker = "extra == 'channel'", specifier = ">=0.27.0" },
86
87
  { name = "httpx", marker = "extra == 'embedding'", specifier = ">=0.27.0" },
87
88
  { name = "hvac", marker = "extra == 'all'", specifier = "==2.3.0" },
88
89
  { name = "hvac", marker = "extra == 'vault'", specifier = "==2.3.0" },
@@ -143,7 +144,7 @@ requires-dist = [
143
144
  { name = "uvicorn", marker = "extra == 'fastapi'", specifier = "==0.41.0" },
144
145
  { name = "uvicorn", marker = "extra == 'langgraph-runtime'", specifier = "==0.41.0" },
145
146
  ]
146
- provides-extras = ["mongodb", "duckdb", "mariadb", "postgresql", "fastapi", "mcp", "vault", "cache", "auth", "embedding", "rabbitmq", "langgraph", "langgraph-runtime", "langgraph-persistence-sqlite", "langgraph-persistence-postgresql", "langgraph-persistence-mongodb", "langgraph-persistence-redis", "langsmith", "opentelemetry", "all", "azure-blob", "s3", "gcs", "qdrant"]
147
+ provides-extras = ["mongodb", "duckdb", "mariadb", "postgresql", "fastapi", "mcp", "vault", "cache", "auth", "embedding", "channel", "rabbitmq", "langgraph", "langgraph-runtime", "langgraph-persistence-sqlite", "langgraph-persistence-postgresql", "langgraph-persistence-mongodb", "langgraph-persistence-redis", "langsmith", "opentelemetry", "all", "azure-blob", "s3", "gcs", "qdrant"]
147
148
 
148
149
  [package.metadata.requires-dev]
149
150
  dev = [
@@ -165,7 +166,7 @@ docs = [
165
166
 
166
167
  [[package]]
167
168
  name = "arclith-cli"
168
- version = "0.19.0"
169
+ version = "0.20.0"
169
170
  source = { editable = "." }
170
171
  dependencies = [
171
172
  { name = "arclith" },
@@ -1 +0,0 @@
1
- __version__ = "0.19.0"
File without changes
File without changes
File without changes
File without changes