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.
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/PKG-INFO +2 -2
- arclith_cli-0.20.0/arclith_cli/__init__.py +1 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/adapter_templates.py +2 -1
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/capabilities.py +5 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/capability_models.py +41 -0
- arclith_cli-0.20.0/arclith_cli/catalogs/channels.py +197 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/catalogs/persistence.py +12 -0
- arclith_cli-0.20.0/arclith_cli/catalogs/repository_facets.py +94 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/init_project.py +1 -1
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/project_paths.py +18 -3
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/pyproject.toml +2 -2
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/tests/e2e_manual.sh +1 -1
- arclith_cli-0.20.0/tests/test_adapter_templates.py +20 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/tests/test_add_adapter.py +166 -7
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/tests/test_capabilities.py +174 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/tests/test_core_scaffold.py +1 -1
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/tests/test_embedding_capability.py +3 -3
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/tests/test_project_paths.py +1 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/tests/test_vector_store_capability.py +1 -1
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/uv.lock +4 -3
- arclith_cli-0.19.0/arclith_cli/__init__.py +0 -1
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/.gitignore +0 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/LICENSE +0 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/Makefile +0 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/README.md +0 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/adapter_config.py +0 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/adapter_generator.py +0 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/adapter_parameters.py +0 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/adapter_rendering.py +0 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/adapter_selection.py +0 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/add_adapter.py +0 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/catalogs/__init__.py +0 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/catalogs/ai.py +0 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/catalogs/core.py +0 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/catalogs/observability.py +0 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/catalogs/security.py +0 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/catalogs/transports.py +0 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/core_scaffold.py +0 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/entity_scanner.py +0 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/export_config.py +0 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/main.py +0 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/new_project.py +0 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/recipe.py +0 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/recipe_cli.py +0 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/recipe_models.py +0 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/rename.py +0 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/runtime_templates.py +0 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/scaffold.py +0 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/arclith_cli/updater.py +0 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/tests/__init__.py +0 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/tests/test_e2e_scaffold.py +0 -0
- {arclith_cli-0.19.0 → arclith_cli-0.20.0}/tests/test_recipe.py +0 -0
- {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.
|
|
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.
|
|
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
|
-
|
|
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
|
+
]
|
|
@@ -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(
|
|
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(
|
|
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(
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|