arclith-cli 0.20.0__tar.gz → 0.21.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.20.0 → arclith_cli-0.21.0}/PKG-INFO +41 -10
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/README.md +39 -8
- arclith_cli-0.21.0/arclith_cli/__init__.py +1 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/arclith_cli/catalogs/persistence.py +4 -81
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/arclith_cli/catalogs/repository_facets.py +3 -3
- arclith_cli-0.21.0/arclith_cli/catalogs/repository_postgresql.py +107 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/arclith_cli/core_scaffold.py +155 -40
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/arclith_cli/init_project.py +1 -1
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/arclith_cli/main.py +41 -3
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/arclith_cli/recipe.py +14 -1
- arclith_cli-0.21.0/arclith_cli/scaffold_interactive.py +105 -0
- arclith_cli-0.21.0/arclith_cli/scaffold_templates.py +140 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/pyproject.toml +2 -2
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/tests/e2e_manual.sh +1 -1
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/tests/test_add_adapter.py +17 -11
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/tests/test_capabilities.py +2 -0
- arclith_cli-0.21.0/tests/test_core_scaffold.py +566 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/tests/test_e2e_scaffold.py +3 -1
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/tests/test_embedding_capability.py +3 -3
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/tests/test_recipe.py +2 -2
- arclith_cli-0.21.0/tests/test_scaffold_cli.py +216 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/tests/test_vector_store_capability.py +1 -1
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/uv.lock +2 -2
- arclith_cli-0.20.0/arclith_cli/__init__.py +0 -1
- arclith_cli-0.20.0/tests/test_core_scaffold.py +0 -248
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/.gitignore +0 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/LICENSE +0 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/Makefile +0 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/arclith_cli/adapter_config.py +0 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/arclith_cli/adapter_generator.py +0 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/arclith_cli/adapter_parameters.py +0 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/arclith_cli/adapter_rendering.py +0 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/arclith_cli/adapter_selection.py +0 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/arclith_cli/adapter_templates.py +0 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/arclith_cli/add_adapter.py +0 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/arclith_cli/capabilities.py +0 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/arclith_cli/capability_models.py +0 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/arclith_cli/catalogs/__init__.py +0 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/arclith_cli/catalogs/ai.py +0 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/arclith_cli/catalogs/channels.py +0 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/arclith_cli/catalogs/core.py +0 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/arclith_cli/catalogs/observability.py +0 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/arclith_cli/catalogs/security.py +0 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/arclith_cli/catalogs/transports.py +0 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/arclith_cli/entity_scanner.py +0 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/arclith_cli/export_config.py +0 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/arclith_cli/new_project.py +0 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/arclith_cli/project_paths.py +0 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/arclith_cli/recipe_cli.py +0 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/arclith_cli/recipe_models.py +0 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/arclith_cli/rename.py +0 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/arclith_cli/runtime_templates.py +0 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/arclith_cli/scaffold.py +0 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/arclith_cli/updater.py +0 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/tests/__init__.py +0 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/tests/test_adapter_templates.py +0 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.0}/tests/test_project_paths.py +0 -0
- {arclith_cli-0.20.0 → arclith_cli-0.21.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.21.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.24.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
|
|
@@ -252,7 +252,10 @@ template est régénéré côté CLI pour appliquer le contrat `runtime/docker-i
|
|
|
252
252
|
|
|
253
253
|
### `add-entity` — Ajouter une entité métier
|
|
254
254
|
|
|
255
|
-
Crée
|
|
255
|
+
Crée un squelette minimal et guidé dans `src/<package>/domain/models/`. Le fichier
|
|
256
|
+
signale où déclarer les champs et invariants, rappelle les champs déjà fournis par
|
|
257
|
+
`Entity` et renvoie vers les guides Arclith et Pydantic. L'exemple `Field(...)`
|
|
258
|
+
reste commenté, donc aucun import inutilisé n'est ajouté.
|
|
256
259
|
|
|
257
260
|
```bash
|
|
258
261
|
cd my-recipe-service
|
|
@@ -265,21 +268,41 @@ Fichier généré :
|
|
|
265
268
|
src/<package>/domain/models/shopping_item.py
|
|
266
269
|
```
|
|
267
270
|
|
|
268
|
-
La commande ne génère aucun CRUD, aucun port repository, aucun adapter et aucun
|
|
271
|
+
La commande ne génère aucun CRUD, aucun port repository, aucun adapter et aucun
|
|
272
|
+
endpoint. Elle pose seulement le point d'ancrage du modèle métier ; le
|
|
273
|
+
développeur complète ensuite les champs et invariants de l'entité.
|
|
269
274
|
|
|
270
275
|
---
|
|
271
276
|
|
|
272
277
|
### `add-usecase` — Ajouter un cas d'usage
|
|
273
278
|
|
|
274
|
-
Crée
|
|
275
|
-
cas d'usage dans `src/<package>/application/use_cases/`.
|
|
279
|
+
Crée un port inbound guidé dans `src/<package>/domain/ports/inbound/`, puis le
|
|
280
|
+
cas d'usage dans `src/<package>/application/use_cases/`. Sans option de liaison,
|
|
281
|
+
la commande interactive propose les entités détectées par analyse AST, la
|
|
282
|
+
création d'une nouvelle entité ou un cas d'usage transverse.
|
|
276
283
|
|
|
277
284
|
```bash
|
|
278
285
|
cd my-recipe-service
|
|
286
|
+
|
|
287
|
+
# Mode interactif : choisir une entité détectée, en créer une ou rester transverse
|
|
279
288
|
arclith-cli add-usecase PlanShoppingList
|
|
280
|
-
|
|
289
|
+
|
|
290
|
+
# Modes directs, complets pour les agents et la CI
|
|
291
|
+
arclith-cli add-usecase CreateTodo --entity Todo
|
|
292
|
+
arclith-cli add-usecase CreateRecipe --new-entity Recipe
|
|
293
|
+
arclith-cli add-usecase RunMaintenance --no-entity
|
|
281
294
|
```
|
|
282
295
|
|
|
296
|
+
| Option | Effet |
|
|
297
|
+
|---|---|
|
|
298
|
+
| `--entity Todo` | lie le use case à une entité détectée et échoue si elle est absente |
|
|
299
|
+
| `--new-entity Todo` | crée l'entité si nécessaire, puis génère le use case lié |
|
|
300
|
+
| `--no-entity` | génère un `Command`, un `Result` et un use case transverse sans repository |
|
|
301
|
+
|
|
302
|
+
Ces options sont mutuellement exclusives. `--new-entity` réutilise une entité
|
|
303
|
+
valide déjà présente, mais refuse un fichier homonyme qui ne déclare pas la
|
|
304
|
+
classe `Entity` attendue.
|
|
305
|
+
|
|
283
306
|
Fichier généré :
|
|
284
307
|
|
|
285
308
|
```text
|
|
@@ -287,9 +310,17 @@ src/<package>/domain/ports/inbound/plan_shopping_list.py
|
|
|
287
310
|
src/<package>/application/use_cases/plan_shopping_list.py
|
|
288
311
|
```
|
|
289
312
|
|
|
290
|
-
Le nom peut être fourni en PascalCase, snake_case ou kebab-case. Le suffixe
|
|
291
|
-
|
|
292
|
-
|
|
313
|
+
Le nom peut être fourni en PascalCase, snake_case ou kebab-case. Le suffixe
|
|
314
|
+
`UseCase` est normalisé : `PlanShoppingListUseCase` et
|
|
315
|
+
`plan-shopping-list-use-case` génèrent tous les deux
|
|
316
|
+
`PlanShoppingListUseCase`.
|
|
317
|
+
|
|
318
|
+
Pour une entité principale, le squelette injecte explicitement
|
|
319
|
+
`Repository[Entity]` et type `execute` avec un `Command` Pydantic et l'entité en
|
|
320
|
+
retour. Le mode transverse génère plutôt un `Command` et un `Result` Pydantic,
|
|
321
|
+
sans repository implicite. Aucun mode ne câble FastAPI, FastMCP ou LangGraph.
|
|
322
|
+
Les exemples complets et les règles de séparation sont dans le
|
|
323
|
+
[deep dive du scaffold CLI](https://karned-rekipe.github.io/arclith/deep-dives/cli-scaffold/).
|
|
293
324
|
|
|
294
325
|
---
|
|
295
326
|
|
|
@@ -58,7 +58,10 @@ template est régénéré côté CLI pour appliquer le contrat `runtime/docker-i
|
|
|
58
58
|
|
|
59
59
|
### `add-entity` — Ajouter une entité métier
|
|
60
60
|
|
|
61
|
-
Crée
|
|
61
|
+
Crée un squelette minimal et guidé dans `src/<package>/domain/models/`. Le fichier
|
|
62
|
+
signale où déclarer les champs et invariants, rappelle les champs déjà fournis par
|
|
63
|
+
`Entity` et renvoie vers les guides Arclith et Pydantic. L'exemple `Field(...)`
|
|
64
|
+
reste commenté, donc aucun import inutilisé n'est ajouté.
|
|
62
65
|
|
|
63
66
|
```bash
|
|
64
67
|
cd my-recipe-service
|
|
@@ -71,21 +74,41 @@ Fichier généré :
|
|
|
71
74
|
src/<package>/domain/models/shopping_item.py
|
|
72
75
|
```
|
|
73
76
|
|
|
74
|
-
La commande ne génère aucun CRUD, aucun port repository, aucun adapter et aucun
|
|
77
|
+
La commande ne génère aucun CRUD, aucun port repository, aucun adapter et aucun
|
|
78
|
+
endpoint. Elle pose seulement le point d'ancrage du modèle métier ; le
|
|
79
|
+
développeur complète ensuite les champs et invariants de l'entité.
|
|
75
80
|
|
|
76
81
|
---
|
|
77
82
|
|
|
78
83
|
### `add-usecase` — Ajouter un cas d'usage
|
|
79
84
|
|
|
80
|
-
Crée
|
|
81
|
-
cas d'usage dans `src/<package>/application/use_cases/`.
|
|
85
|
+
Crée un port inbound guidé dans `src/<package>/domain/ports/inbound/`, puis le
|
|
86
|
+
cas d'usage dans `src/<package>/application/use_cases/`. Sans option de liaison,
|
|
87
|
+
la commande interactive propose les entités détectées par analyse AST, la
|
|
88
|
+
création d'une nouvelle entité ou un cas d'usage transverse.
|
|
82
89
|
|
|
83
90
|
```bash
|
|
84
91
|
cd my-recipe-service
|
|
92
|
+
|
|
93
|
+
# Mode interactif : choisir une entité détectée, en créer une ou rester transverse
|
|
85
94
|
arclith-cli add-usecase PlanShoppingList
|
|
86
|
-
|
|
95
|
+
|
|
96
|
+
# Modes directs, complets pour les agents et la CI
|
|
97
|
+
arclith-cli add-usecase CreateTodo --entity Todo
|
|
98
|
+
arclith-cli add-usecase CreateRecipe --new-entity Recipe
|
|
99
|
+
arclith-cli add-usecase RunMaintenance --no-entity
|
|
87
100
|
```
|
|
88
101
|
|
|
102
|
+
| Option | Effet |
|
|
103
|
+
|---|---|
|
|
104
|
+
| `--entity Todo` | lie le use case à une entité détectée et échoue si elle est absente |
|
|
105
|
+
| `--new-entity Todo` | crée l'entité si nécessaire, puis génère le use case lié |
|
|
106
|
+
| `--no-entity` | génère un `Command`, un `Result` et un use case transverse sans repository |
|
|
107
|
+
|
|
108
|
+
Ces options sont mutuellement exclusives. `--new-entity` réutilise une entité
|
|
109
|
+
valide déjà présente, mais refuse un fichier homonyme qui ne déclare pas la
|
|
110
|
+
classe `Entity` attendue.
|
|
111
|
+
|
|
89
112
|
Fichier généré :
|
|
90
113
|
|
|
91
114
|
```text
|
|
@@ -93,9 +116,17 @@ src/<package>/domain/ports/inbound/plan_shopping_list.py
|
|
|
93
116
|
src/<package>/application/use_cases/plan_shopping_list.py
|
|
94
117
|
```
|
|
95
118
|
|
|
96
|
-
Le nom peut être fourni en PascalCase, snake_case ou kebab-case. Le suffixe
|
|
97
|
-
|
|
98
|
-
|
|
119
|
+
Le nom peut être fourni en PascalCase, snake_case ou kebab-case. Le suffixe
|
|
120
|
+
`UseCase` est normalisé : `PlanShoppingListUseCase` et
|
|
121
|
+
`plan-shopping-list-use-case` génèrent tous les deux
|
|
122
|
+
`PlanShoppingListUseCase`.
|
|
123
|
+
|
|
124
|
+
Pour une entité principale, le squelette injecte explicitement
|
|
125
|
+
`Repository[Entity]` et type `execute` avec un `Command` Pydantic et l'entité en
|
|
126
|
+
retour. Le mode transverse génère plutôt un `Command` et un `Result` Pydantic,
|
|
127
|
+
sans repository implicite. Aucun mode ne câble FastAPI, FastMCP ou LangGraph.
|
|
128
|
+
Les exemples complets et les règles de séparation sont dans le
|
|
129
|
+
[deep dive du scaffold CLI](https://karned-rekipe.github.io/arclith/deep-dives/cli-scaffold/).
|
|
99
130
|
|
|
100
131
|
---
|
|
101
132
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.21.0"
|
|
@@ -11,7 +11,9 @@ from arclith_cli.catalogs.repository_facets import (
|
|
|
11
11
|
MARIADB_FACETS,
|
|
12
12
|
MEMORY_FACETS,
|
|
13
13
|
MONGODB_FACETS,
|
|
14
|
-
|
|
14
|
+
)
|
|
15
|
+
from arclith_cli.catalogs.repository_postgresql import (
|
|
16
|
+
POSTGRESQL_REPOSITORY_ADAPTER,
|
|
15
17
|
)
|
|
16
18
|
|
|
17
19
|
REPOSITORY_CAPABILITY = CapabilitySpec(
|
|
@@ -154,86 +156,7 @@ multitenant: false
|
|
|
154
156
|
),
|
|
155
157
|
facets=MARIADB_FACETS,
|
|
156
158
|
),
|
|
157
|
-
|
|
158
|
-
name="postgresql",
|
|
159
|
-
capability="repository",
|
|
160
|
-
layer="outbound",
|
|
161
|
-
description="Repository PostgreSQL async optionnel, pilote par SQLAlchemy et asyncpg, avec payload JSONB generique.",
|
|
162
|
-
config_path="config/adapters/outbound/postgresql.yaml",
|
|
163
|
-
config_template="""\
|
|
164
|
-
url: null # à mapper via config/secrets.yaml ou resolver env/vault si vous fournissez une URL complète
|
|
165
|
-
host: {host}
|
|
166
|
-
port: {port}
|
|
167
|
-
database: {database}
|
|
168
|
-
user: {user}
|
|
169
|
-
password: null # à mapper via config/secrets.yaml ou resolver env/vault
|
|
170
|
-
schema: {schema}
|
|
171
|
-
driver: {driver}
|
|
172
|
-
table_prefix: "{table_prefix}"
|
|
173
|
-
multitenant: {multitenant}
|
|
174
|
-
""",
|
|
175
|
-
secret_mappings=(
|
|
176
|
-
SecretMappingSpec(
|
|
177
|
-
field_path="adapters.postgresql.url",
|
|
178
|
-
secret_key="POSTGRESQL_URL",
|
|
179
|
-
),
|
|
180
|
-
SecretMappingSpec(
|
|
181
|
-
field_path="adapters.postgresql.password",
|
|
182
|
-
secret_key="POSTGRESQL_PASSWORD",
|
|
183
|
-
),
|
|
184
|
-
),
|
|
185
|
-
parameters=(
|
|
186
|
-
ParameterSpec(
|
|
187
|
-
name="host",
|
|
188
|
-
kind="string",
|
|
189
|
-
prompt="host",
|
|
190
|
-
default="127.0.0.1",
|
|
191
|
-
),
|
|
192
|
-
ParameterSpec(
|
|
193
|
-
name="port",
|
|
194
|
-
kind="string",
|
|
195
|
-
prompt="port",
|
|
196
|
-
default="5432",
|
|
197
|
-
),
|
|
198
|
-
ParameterSpec(
|
|
199
|
-
name="database",
|
|
200
|
-
kind="string",
|
|
201
|
-
prompt="database",
|
|
202
|
-
default_from_project_name=True,
|
|
203
|
-
),
|
|
204
|
-
ParameterSpec(
|
|
205
|
-
name="user",
|
|
206
|
-
kind="string",
|
|
207
|
-
prompt="user",
|
|
208
|
-
default="app",
|
|
209
|
-
),
|
|
210
|
-
ParameterSpec(
|
|
211
|
-
name="schema",
|
|
212
|
-
kind="string",
|
|
213
|
-
prompt="schema",
|
|
214
|
-
default="public",
|
|
215
|
-
),
|
|
216
|
-
ParameterSpec(
|
|
217
|
-
name="driver",
|
|
218
|
-
kind="string",
|
|
219
|
-
prompt="driver",
|
|
220
|
-
default="asyncpg",
|
|
221
|
-
),
|
|
222
|
-
ParameterSpec(
|
|
223
|
-
name="table_prefix",
|
|
224
|
-
kind="string",
|
|
225
|
-
prompt="table_prefix",
|
|
226
|
-
default="",
|
|
227
|
-
),
|
|
228
|
-
ParameterSpec(
|
|
229
|
-
name="multitenant",
|
|
230
|
-
kind="boolean",
|
|
231
|
-
prompt="multitenant",
|
|
232
|
-
default=False,
|
|
233
|
-
),
|
|
234
|
-
),
|
|
235
|
-
facets=POSTGRESQL_FACETS,
|
|
236
|
-
),
|
|
159
|
+
POSTGRESQL_REPOSITORY_ADAPTER,
|
|
237
160
|
),
|
|
238
161
|
)
|
|
239
162
|
|
|
@@ -77,11 +77,11 @@ POSTGRESQL_FACETS = AdapterFacets(
|
|
|
77
77
|
schema_strategy="json_table",
|
|
78
78
|
recommended_for=(
|
|
79
79
|
"services nécessitant un SQL serveur robuste",
|
|
80
|
-
"
|
|
80
|
+
"payload JSONB par défaut ou colonnes typées explicitement mappées",
|
|
81
81
|
),
|
|
82
82
|
limits=(
|
|
83
|
-
"
|
|
84
|
-
"pas de relations, joins ou migrations
|
|
83
|
+
"le mapping structuré est opt-in et fourni par l'application",
|
|
84
|
+
"pas de relations, joins ou migrations automatiques via le port",
|
|
85
85
|
),
|
|
86
86
|
)
|
|
87
87
|
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from arclith_cli.capability_models import (
|
|
2
|
+
AdapterSpec,
|
|
3
|
+
ParameterSpec,
|
|
4
|
+
SecretMappingSpec,
|
|
5
|
+
)
|
|
6
|
+
from arclith_cli.catalogs.repository_facets import POSTGRESQL_FACETS
|
|
7
|
+
|
|
8
|
+
POSTGRESQL_REPOSITORY_ADAPTER = AdapterSpec(
|
|
9
|
+
name="postgresql",
|
|
10
|
+
capability="repository",
|
|
11
|
+
layer="outbound",
|
|
12
|
+
description=(
|
|
13
|
+
"Repository PostgreSQL async optionnel avec JSONB par défaut ou "
|
|
14
|
+
"mapping structuré explicite."
|
|
15
|
+
),
|
|
16
|
+
config_path="config/adapters/outbound/postgresql.yaml",
|
|
17
|
+
config_template="""\
|
|
18
|
+
url: null # à mapper via config/secrets.yaml ou resolver env/vault si vous fournissez une URL complète
|
|
19
|
+
host: {host}
|
|
20
|
+
port: {port}
|
|
21
|
+
database: {database}
|
|
22
|
+
user: {user}
|
|
23
|
+
password: null # à mapper via config/secrets.yaml ou resolver env/vault
|
|
24
|
+
schema: {schema}
|
|
25
|
+
driver: {driver}
|
|
26
|
+
table_prefix: "{table_prefix}"
|
|
27
|
+
mapping_strategy: {mapping_strategy}
|
|
28
|
+
auto_create_schema: {auto_create_schema}
|
|
29
|
+
multitenant: {multitenant}
|
|
30
|
+
""",
|
|
31
|
+
secret_mappings=(
|
|
32
|
+
SecretMappingSpec(
|
|
33
|
+
field_path="adapters.postgresql.url",
|
|
34
|
+
secret_key="POSTGRESQL_URL",
|
|
35
|
+
),
|
|
36
|
+
SecretMappingSpec(
|
|
37
|
+
field_path="adapters.postgresql.password",
|
|
38
|
+
secret_key="POSTGRESQL_PASSWORD",
|
|
39
|
+
),
|
|
40
|
+
),
|
|
41
|
+
parameters=(
|
|
42
|
+
ParameterSpec(
|
|
43
|
+
name="host",
|
|
44
|
+
kind="string",
|
|
45
|
+
prompt="host",
|
|
46
|
+
default="127.0.0.1",
|
|
47
|
+
),
|
|
48
|
+
ParameterSpec(
|
|
49
|
+
name="port",
|
|
50
|
+
kind="string",
|
|
51
|
+
prompt="port",
|
|
52
|
+
default="5432",
|
|
53
|
+
),
|
|
54
|
+
ParameterSpec(
|
|
55
|
+
name="database",
|
|
56
|
+
kind="string",
|
|
57
|
+
prompt="database",
|
|
58
|
+
default_from_project_name=True,
|
|
59
|
+
),
|
|
60
|
+
ParameterSpec(
|
|
61
|
+
name="user",
|
|
62
|
+
kind="string",
|
|
63
|
+
prompt="user",
|
|
64
|
+
default="app",
|
|
65
|
+
),
|
|
66
|
+
ParameterSpec(
|
|
67
|
+
name="schema",
|
|
68
|
+
kind="string",
|
|
69
|
+
prompt="schema",
|
|
70
|
+
default="public",
|
|
71
|
+
),
|
|
72
|
+
ParameterSpec(
|
|
73
|
+
name="driver",
|
|
74
|
+
kind="string",
|
|
75
|
+
prompt="driver",
|
|
76
|
+
default="asyncpg",
|
|
77
|
+
),
|
|
78
|
+
ParameterSpec(
|
|
79
|
+
name="table_prefix",
|
|
80
|
+
kind="string",
|
|
81
|
+
prompt="table_prefix",
|
|
82
|
+
default="",
|
|
83
|
+
),
|
|
84
|
+
ParameterSpec(
|
|
85
|
+
name="mapping_strategy",
|
|
86
|
+
kind="string",
|
|
87
|
+
prompt="mapping_strategy",
|
|
88
|
+
default="generic_json",
|
|
89
|
+
choices=("generic_json", "structured"),
|
|
90
|
+
),
|
|
91
|
+
ParameterSpec(
|
|
92
|
+
name="auto_create_schema",
|
|
93
|
+
kind="boolean",
|
|
94
|
+
prompt="auto_create_schema",
|
|
95
|
+
default=True,
|
|
96
|
+
),
|
|
97
|
+
ParameterSpec(
|
|
98
|
+
name="multitenant",
|
|
99
|
+
kind="boolean",
|
|
100
|
+
prompt="multitenant",
|
|
101
|
+
default=False,
|
|
102
|
+
),
|
|
103
|
+
),
|
|
104
|
+
facets=POSTGRESQL_FACETS,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
__all__ = ["POSTGRESQL_REPOSITORY_ADAPTER"]
|
|
@@ -7,37 +7,21 @@ from pathlib import Path
|
|
|
7
7
|
import typer
|
|
8
8
|
from rich.console import Console
|
|
9
9
|
|
|
10
|
+
from .entity_scanner import EntityInfo, scan_entities
|
|
10
11
|
from .project_paths import ProjectPaths, detect_project_paths
|
|
11
12
|
from .rename import EntityNames
|
|
13
|
+
from .scaffold_templates import (
|
|
14
|
+
render_entity_inbound_port_template,
|
|
15
|
+
render_entity_template,
|
|
16
|
+
render_entity_use_case_template,
|
|
17
|
+
render_transverse_inbound_port_template,
|
|
18
|
+
render_transverse_use_case_template,
|
|
19
|
+
)
|
|
12
20
|
|
|
13
21
|
console = Console()
|
|
14
22
|
|
|
15
23
|
_NAME_RE = re.compile(r"^[A-Za-z][A-Za-z0-9_\-]*$")
|
|
16
24
|
|
|
17
|
-
_ENTITY_TEMPLATE = """from arclith.domain.models.entity import Entity
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class {class_name}(Entity):
|
|
21
|
-
pass
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
_INBOUND_PORT_TEMPLATE = """from abc import ABC, abstractmethod
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class {class_name}Port(ABC):
|
|
28
|
-
@abstractmethod
|
|
29
|
-
async def execute(self) -> None:
|
|
30
|
-
raise NotImplementedError
|
|
31
|
-
"""
|
|
32
|
-
|
|
33
|
-
_USE_CASE_TEMPLATE = """from {inbound_port_module} import {class_name}Port
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class {class_name}UseCase({class_name}Port):
|
|
37
|
-
async def execute(self) -> None:
|
|
38
|
-
raise NotImplementedError("Implement {class_name}UseCase.execute")
|
|
39
|
-
"""
|
|
40
|
-
|
|
41
25
|
_INTENT_INTERPRETER_TEMPLATE = """class {class_name}Interpreter:
|
|
42
26
|
async def interpret(self, prompt: str) -> None:
|
|
43
27
|
raise NotImplementedError("Implement {class_name}Interpreter.interpret")
|
|
@@ -55,7 +39,9 @@ class UseCaseNames:
|
|
|
55
39
|
pascal = _strip_pascal_suffix(names.pascal)
|
|
56
40
|
snake = _strip_snake_suffix(names.snake)
|
|
57
41
|
if not pascal or not snake:
|
|
58
|
-
console.print(
|
|
42
|
+
console.print(
|
|
43
|
+
f"[red]✗[/red] Nom de cas d'usage invalide : [bold]{raw}[/bold]."
|
|
44
|
+
)
|
|
59
45
|
raise typer.Exit(1)
|
|
60
46
|
return cls(pascal=pascal, snake=snake)
|
|
61
47
|
|
|
@@ -71,7 +57,9 @@ class IntentInterpreterNames:
|
|
|
71
57
|
pascal = _strip_pascal_intent_interpreter_suffix(names.pascal)
|
|
72
58
|
snake = _strip_snake_intent_interpreter_suffix(names.snake)
|
|
73
59
|
if not pascal or not snake:
|
|
74
|
-
console.print(
|
|
60
|
+
console.print(
|
|
61
|
+
f"[red]✗[/red] Nom d'interpréteur d'intention invalide : [bold]{raw}[/bold]."
|
|
62
|
+
)
|
|
75
63
|
raise typer.Exit(1)
|
|
76
64
|
return cls(pascal=pascal, snake=snake)
|
|
77
65
|
|
|
@@ -88,7 +76,9 @@ def add_entity_cmd(*, project_dir: Path | None = None, entity_name: str) -> Path
|
|
|
88
76
|
_assert_missing(entity_file, project_dir)
|
|
89
77
|
|
|
90
78
|
_ensure_package_dirs(paths, "domain", "models")
|
|
91
|
-
entity_file.write_text(
|
|
79
|
+
entity_file.write_text(
|
|
80
|
+
render_entity_template(class_name=names.pascal), encoding="utf-8"
|
|
81
|
+
)
|
|
92
82
|
console.print(
|
|
93
83
|
f"[green]✓[/green] Entité {names.pascal} créée : "
|
|
94
84
|
f"[bold]{entity_file.relative_to(project_dir)}[/bold]"
|
|
@@ -96,7 +86,13 @@ def add_entity_cmd(*, project_dir: Path | None = None, entity_name: str) -> Path
|
|
|
96
86
|
return entity_file
|
|
97
87
|
|
|
98
88
|
|
|
99
|
-
def add_usecase_cmd(
|
|
89
|
+
def add_usecase_cmd(
|
|
90
|
+
*,
|
|
91
|
+
project_dir: Path | None = None,
|
|
92
|
+
usecase_name: str,
|
|
93
|
+
entity_name: str | None = None,
|
|
94
|
+
new_entity_name: str | None = None,
|
|
95
|
+
) -> Path:
|
|
100
96
|
project_dir = project_dir or Path.cwd()
|
|
101
97
|
usecase_name = usecase_name.strip()
|
|
102
98
|
_assert_project_root(project_dir, command="add-usecase")
|
|
@@ -108,17 +104,43 @@ def add_usecase_cmd(*, project_dir: Path | None = None, usecase_name: str) -> Pa
|
|
|
108
104
|
inbound_port_file = paths.inbound_ports / f"{names.snake}.py"
|
|
109
105
|
_assert_missing(usecase_file, project_dir)
|
|
110
106
|
_assert_missing(inbound_port_file, project_dir)
|
|
107
|
+
entity = _resolve_usecase_entity(
|
|
108
|
+
project_dir=project_dir,
|
|
109
|
+
paths=paths,
|
|
110
|
+
entity_name=entity_name,
|
|
111
|
+
new_entity_name=new_entity_name,
|
|
112
|
+
)
|
|
111
113
|
|
|
112
114
|
_ensure_package_dirs(paths, "domain", "ports", "inbound")
|
|
113
115
|
_ensure_package_dirs(paths, "application", "use_cases")
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
116
|
+
inbound_port_module = paths.import_path("domain", "ports", "inbound", names.snake)
|
|
117
|
+
if entity is None:
|
|
118
|
+
inbound_port_content = render_transverse_inbound_port_template(
|
|
119
|
+
class_name=names.pascal
|
|
120
|
+
)
|
|
121
|
+
usecase_content = render_transverse_use_case_template(
|
|
117
122
|
class_name=names.pascal,
|
|
118
|
-
inbound_port_module=
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
|
|
123
|
+
inbound_port_module=inbound_port_module,
|
|
124
|
+
)
|
|
125
|
+
else:
|
|
126
|
+
entity_module = paths.import_path(
|
|
127
|
+
"domain",
|
|
128
|
+
"models",
|
|
129
|
+
entity.file_path.stem,
|
|
130
|
+
)
|
|
131
|
+
inbound_port_content = render_entity_inbound_port_template(
|
|
132
|
+
class_name=names.pascal,
|
|
133
|
+
entity_class=entity.pascal,
|
|
134
|
+
entity_module=entity_module,
|
|
135
|
+
)
|
|
136
|
+
usecase_content = render_entity_use_case_template(
|
|
137
|
+
class_name=names.pascal,
|
|
138
|
+
entity_class=entity.pascal,
|
|
139
|
+
entity_module=entity_module,
|
|
140
|
+
inbound_port_module=inbound_port_module,
|
|
141
|
+
)
|
|
142
|
+
inbound_port_file.write_text(inbound_port_content, encoding="utf-8")
|
|
143
|
+
usecase_file.write_text(usecase_content, encoding="utf-8")
|
|
122
144
|
console.print(
|
|
123
145
|
f"[green]✓[/green] Port inbound {names.pascal}Port créé : "
|
|
124
146
|
f"[bold]{inbound_port_file.relative_to(project_dir)}[/bold]"
|
|
@@ -127,10 +149,93 @@ def add_usecase_cmd(*, project_dir: Path | None = None, usecase_name: str) -> Pa
|
|
|
127
149
|
f"[green]✓[/green] Cas d'usage {names.pascal}UseCase créé : "
|
|
128
150
|
f"[bold]{usecase_file.relative_to(project_dir)}[/bold]"
|
|
129
151
|
)
|
|
152
|
+
if entity is not None:
|
|
153
|
+
console.print(
|
|
154
|
+
f"[green]✓[/green] Entité principale : [bold]{entity.pascal}[/bold]"
|
|
155
|
+
)
|
|
130
156
|
return usecase_file
|
|
131
157
|
|
|
132
158
|
|
|
133
|
-
def
|
|
159
|
+
def _resolve_usecase_entity(
|
|
160
|
+
*,
|
|
161
|
+
project_dir: Path,
|
|
162
|
+
paths: ProjectPaths,
|
|
163
|
+
entity_name: str | None,
|
|
164
|
+
new_entity_name: str | None,
|
|
165
|
+
) -> EntityInfo | None:
|
|
166
|
+
if entity_name is not None and new_entity_name is not None:
|
|
167
|
+
console.print(
|
|
168
|
+
"[red]✗[/red] Une seule entité peut être choisie : "
|
|
169
|
+
"utilisez [bold]entity_name[/bold] ou [bold]new_entity_name[/bold]."
|
|
170
|
+
)
|
|
171
|
+
raise typer.Exit(1)
|
|
172
|
+
if entity_name is not None:
|
|
173
|
+
return _require_existing_entity(project_dir, entity_name)
|
|
174
|
+
if new_entity_name is None:
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
raw_name = new_entity_name.strip()
|
|
178
|
+
_assert_valid_name(raw_name, label="entité")
|
|
179
|
+
names = EntityNames.from_input(raw_name)
|
|
180
|
+
existing_entity = _find_entity(project_dir, names.pascal)
|
|
181
|
+
if existing_entity is not None:
|
|
182
|
+
return existing_entity
|
|
183
|
+
|
|
184
|
+
expected_file = paths.domain_models / f"{names.snake}.py"
|
|
185
|
+
if expected_file.exists():
|
|
186
|
+
console.print(
|
|
187
|
+
f"[red]✗[/red] Le fichier [bold]{expected_file.relative_to(project_dir)}[/bold] "
|
|
188
|
+
f"existe mais ne déclare pas [bold]class {names.pascal}(Entity)[/bold]."
|
|
189
|
+
)
|
|
190
|
+
raise typer.Exit(1)
|
|
191
|
+
|
|
192
|
+
add_entity_cmd(project_dir=project_dir, entity_name=raw_name)
|
|
193
|
+
created_entity = _find_entity(project_dir, names.pascal)
|
|
194
|
+
if (
|
|
195
|
+
created_entity is None
|
|
196
|
+
): # pragma: no cover - defensive check after our own template
|
|
197
|
+
console.print(
|
|
198
|
+
f"[red]✗[/red] Impossible de détecter l'entité créée {names.pascal}."
|
|
199
|
+
)
|
|
200
|
+
raise typer.Exit(1)
|
|
201
|
+
return created_entity
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _require_existing_entity(project_dir: Path, raw_name: str) -> EntityInfo:
|
|
205
|
+
normalized_name = raw_name.strip()
|
|
206
|
+
_assert_valid_name(normalized_name, label="entité")
|
|
207
|
+
names = EntityNames.from_input(normalized_name)
|
|
208
|
+
entities = scan_entities(project_dir)
|
|
209
|
+
entity = next(
|
|
210
|
+
(item for item in entities if item.pascal == names.pascal),
|
|
211
|
+
None,
|
|
212
|
+
)
|
|
213
|
+
if entity is not None:
|
|
214
|
+
return entity
|
|
215
|
+
|
|
216
|
+
detected = ", ".join(item.pascal for item in entities) or "aucune"
|
|
217
|
+
console.print(
|
|
218
|
+
f"[red]✗[/red] Entité introuvable : [bold]{names.pascal}[/bold]. "
|
|
219
|
+
f"Entités détectées : {detected}. Utilisez [bold]--new-entity {names.pascal}[/bold] "
|
|
220
|
+
"pour la créer."
|
|
221
|
+
)
|
|
222
|
+
raise typer.Exit(1)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _find_entity(project_dir: Path, pascal_name: str) -> EntityInfo | None:
|
|
226
|
+
return next(
|
|
227
|
+
(
|
|
228
|
+
entity
|
|
229
|
+
for entity in scan_entities(project_dir)
|
|
230
|
+
if entity.pascal == pascal_name
|
|
231
|
+
),
|
|
232
|
+
None,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def add_intent_interpreter_cmd(
|
|
237
|
+
*, project_dir: Path | None = None, intent_name: str
|
|
238
|
+
) -> Path:
|
|
134
239
|
project_dir = project_dir or Path.cwd()
|
|
135
240
|
intent_name = intent_name.strip()
|
|
136
241
|
_assert_project_root(project_dir, command="add-intent-interpreter")
|
|
@@ -142,7 +247,9 @@ def add_intent_interpreter_cmd(*, project_dir: Path | None = None, intent_name:
|
|
|
142
247
|
_assert_missing(intent_file, project_dir)
|
|
143
248
|
|
|
144
249
|
_ensure_package_dirs(paths, "application", "intent_interpreters")
|
|
145
|
-
intent_file.write_text(
|
|
250
|
+
intent_file.write_text(
|
|
251
|
+
_INTENT_INTERPRETER_TEMPLATE.format(class_name=names.pascal), encoding="utf-8"
|
|
252
|
+
)
|
|
146
253
|
console.print(
|
|
147
254
|
f"[green]✓[/green] Interpréteur d'intention {names.pascal}Interpreter créé : "
|
|
148
255
|
f"[bold]{intent_file.relative_to(project_dir)}[/bold]"
|
|
@@ -152,7 +259,9 @@ def add_intent_interpreter_cmd(*, project_dir: Path | None = None, intent_name:
|
|
|
152
259
|
|
|
153
260
|
def _assert_project_root(project_dir: Path, *, command: str) -> None:
|
|
154
261
|
if not project_dir.exists() or not project_dir.is_dir():
|
|
155
|
-
console.print(
|
|
262
|
+
console.print(
|
|
263
|
+
f"[red]✗[/red] Répertoire introuvable : [bold]{project_dir}[/bold]."
|
|
264
|
+
)
|
|
156
265
|
raise typer.Exit(1)
|
|
157
266
|
|
|
158
267
|
has_project_marker = (
|
|
@@ -185,7 +294,9 @@ def _assert_missing(path: Path, project_dir: Path) -> None:
|
|
|
185
294
|
if not path.exists():
|
|
186
295
|
return
|
|
187
296
|
|
|
188
|
-
console.print(
|
|
297
|
+
console.print(
|
|
298
|
+
f"[red]✗[/red] Le fichier existe déjà : [bold]{path.relative_to(project_dir)}[/bold]."
|
|
299
|
+
)
|
|
189
300
|
raise typer.Exit(1)
|
|
190
301
|
|
|
191
302
|
|
|
@@ -193,7 +304,11 @@ def _ensure_package_dirs(paths: ProjectPaths, *relative_parts: str) -> None:
|
|
|
193
304
|
target_dir = paths.package_root.joinpath(*relative_parts)
|
|
194
305
|
target_dir.mkdir(parents=True, exist_ok=True)
|
|
195
306
|
|
|
196
|
-
stop_at =
|
|
307
|
+
stop_at = (
|
|
308
|
+
paths.package_root.parent
|
|
309
|
+
if paths.package_name is not None
|
|
310
|
+
else paths.package_root
|
|
311
|
+
)
|
|
197
312
|
init_dirs = [target_dir]
|
|
198
313
|
for parent in target_dir.parents:
|
|
199
314
|
if parent == stop_at:
|