arclith-cli 0.9.0__tar.gz → 0.10.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.9.0 → arclith_cli-0.10.0}/PKG-INFO +2 -2
- {arclith_cli-0.9.0 → arclith_cli-0.10.0}/README.md +65 -6
- arclith_cli-0.10.0/arclith_cli/__init__.py +1 -0
- {arclith_cli-0.9.0 → arclith_cli-0.10.0}/arclith_cli/capabilities.py +167 -1
- arclith_cli-0.10.0/arclith_cli/core_scaffold.py +165 -0
- {arclith_cli-0.9.0 → arclith_cli-0.10.0}/arclith_cli/main.py +43 -1
- {arclith_cli-0.9.0 → arclith_cli-0.10.0}/arclith_cli/project_paths.py +23 -3
- {arclith_cli-0.9.0 → arclith_cli-0.10.0}/arclith_cli/rename.py +23 -2
- {arclith_cli-0.9.0 → arclith_cli-0.10.0}/pyproject.toml +2 -2
- {arclith_cli-0.9.0 → arclith_cli-0.10.0}/tests/e2e_manual.sh +1 -1
- {arclith_cli-0.9.0 → arclith_cli-0.10.0}/tests/test_add_adapter.py +113 -0
- {arclith_cli-0.9.0 → arclith_cli-0.10.0}/tests/test_capabilities.py +64 -5
- arclith_cli-0.10.0/tests/test_core_scaffold.py +91 -0
- {arclith_cli-0.9.0 → arclith_cli-0.10.0}/tests/test_e2e_scaffold.py +57 -1
- {arclith_cli-0.9.0 → arclith_cli-0.10.0}/tests/test_project_paths.py +16 -0
- {arclith_cli-0.9.0 → arclith_cli-0.10.0}/tests/test_rename.py +12 -1
- {arclith_cli-0.9.0 → arclith_cli-0.10.0}/uv.lock +13 -3
- arclith_cli-0.9.0/arclith_cli/__init__.py +0 -1
- {arclith_cli-0.9.0 → arclith_cli-0.10.0}/.gitignore +0 -0
- {arclith_cli-0.9.0 → arclith_cli-0.10.0}/LICENSE +0 -0
- {arclith_cli-0.9.0 → arclith_cli-0.10.0}/Makefile +0 -0
- {arclith_cli-0.9.0 → arclith_cli-0.10.0}/arclith_cli/adapter_templates.py +0 -0
- {arclith_cli-0.9.0 → arclith_cli-0.10.0}/arclith_cli/add_adapter.py +0 -0
- {arclith_cli-0.9.0 → arclith_cli-0.10.0}/arclith_cli/entity_scanner.py +0 -0
- {arclith_cli-0.9.0 → arclith_cli-0.10.0}/arclith_cli/export_config.py +0 -0
- {arclith_cli-0.9.0 → arclith_cli-0.10.0}/arclith_cli/scaffold.py +0 -0
- {arclith_cli-0.9.0 → arclith_cli-0.10.0}/arclith_cli/updater.py +0 -0
- {arclith_cli-0.9.0 → arclith_cli-0.10.0}/tests/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arclith-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.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.13.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
|
|
@@ -34,6 +34,47 @@ Le projet généré utilise un layout `src/<package>/...` pour le code applicati
|
|
|
34
34
|
|
|
35
35
|
---
|
|
36
36
|
|
|
37
|
+
### `add-entity` — Ajouter une entité métier
|
|
38
|
+
|
|
39
|
+
Crée uniquement le fichier minimal d'une entité dans `src/<package>/domain/models/`.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
cd my-recipe-service
|
|
43
|
+
arclith-cli add-entity ShoppingItem
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Fichier généré :
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
src/<package>/domain/models/shopping_item.py
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
La commande ne génère aucun CRUD, aucun port repository, aucun adapter et aucun endpoint. Elle pose seulement le point d'ancrage du modèle métier ; le développeur complète ensuite les champs et invariants de l'entité.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
### `add-usecase` — Ajouter un cas d'usage
|
|
57
|
+
|
|
58
|
+
Crée uniquement le fichier minimal d'un cas d'usage dans `src/<package>/application/use_cases/`.
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
cd my-recipe-service
|
|
62
|
+
arclith-cli add-usecase PlanShoppingList
|
|
63
|
+
arclith-cli add-usecase find-by-name
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Fichier généré :
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
src/<package>/application/use_cases/plan_shopping_list.py
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Le nom peut être fourni en PascalCase, snake_case ou kebab-case. Le suffixe `UseCase` est normalisé : `PlanShoppingListUseCase` et `plan-shopping-list-use-case` génèrent tous les deux `PlanShoppingListUseCase`.
|
|
73
|
+
|
|
74
|
+
Comme `add-entity`, cette commande ne câble pas FastAPI, FastMCP, LangGraph, un repository ou un service. Les adapters se branchent ensuite explicitement avec `add-adapter`.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
37
78
|
### `add-adapter` — Ajouter un adapter
|
|
38
79
|
|
|
39
80
|
Wizard interactif à lancer **depuis la racine du projet cible**. Scaffold le code Python et/ou les fichiers de configuration pour un nouvel adapter. Par défaut, la capacité cible est `repository`.
|
|
@@ -49,29 +90,35 @@ Mode direct, utile pour CI, scripts de migration ou commandes reproductibles :
|
|
|
49
90
|
arclith-cli add-adapter --adapter mongodb --entity Recipe --db-name my_recipe_service --yes
|
|
50
91
|
arclith-cli add-adapter --adapter duckdb --all-entities --path data/ --no-activate --yes
|
|
51
92
|
arclith-cli add-adapter --adapter mariadb --entity Recipe --param database=my_recipe_service --param user=app --yes
|
|
93
|
+
arclith-cli add-adapter --capability api --adapter fastapi --param port=8080 --yes
|
|
94
|
+
arclith-cli add-adapter --capability mcp --adapter fastmcp --param port=8081 --yes
|
|
52
95
|
arclith-cli add-adapter --capability agent --adapter langgraph --param graph_name=recipe_agent --yes
|
|
53
96
|
arclith-cli add-adapter --capability observability --adapter langsmith
|
|
97
|
+
arclith-cli add-adapter --capability observability --adapter opentelemetry --param service_name=my_recipe_service --yes
|
|
54
98
|
arclith-cli add-adapter --capability repository --adapter memory --entity Recipe --yes
|
|
55
99
|
```
|
|
56
100
|
|
|
57
101
|
**Étapes du wizard :**
|
|
58
102
|
|
|
59
|
-
1. **Type d'adapter** — selon la capacité : `memory` · `mongodb` · `duckdb` · `mariadb` · `langgraph` · `langsmith`
|
|
60
|
-
2. **Entité(s) cible(s)** — détectées automatiquement pour les adapters entity-scoped ; ignorées pour `agent/langgraph` et
|
|
103
|
+
1. **Type d'adapter** — selon la capacité : `memory` · `mongodb` · `duckdb` · `mariadb` · `fastapi` · `fastmcp` · `langgraph` · `langsmith` · `opentelemetry`
|
|
104
|
+
2. **Entité(s) cible(s)** — détectées automatiquement pour les adapters entity-scoped ; ignorées pour les transports globaux, `agent/langgraph` et les adapters d'observability
|
|
61
105
|
3. **Paramètres** — questions spécifiques à l'adapter :
|
|
62
106
|
- `mongodb` → `db_name`, `multitenant`
|
|
63
107
|
- `duckdb` → `path`
|
|
64
108
|
- `mariadb` → `host`, `port`, `database`, `user`, `driver`, `table_prefix`
|
|
109
|
+
- `fastapi` → `host`, `port`, `reload`
|
|
110
|
+
- `fastmcp` → `host`, `port`
|
|
65
111
|
- `langgraph` → `graph_name`
|
|
66
112
|
- `langsmith` → `tracing`, `project`, `endpoint`, `LANGSMITH_API_KEY`
|
|
113
|
+
- `opentelemetry` → `service_name`, `endpoint`, `protocol`, `traces`, `metrics`, `instrument_fastapi`
|
|
67
114
|
- `memory` → aucun paramètre
|
|
68
|
-
4. **Activation** — met à jour `config/adapters/adapters.yaml` pour les capacités à sélecteur (`repository: <adapter>` ou `observability:
|
|
115
|
+
4. **Activation** — met à jour `config/adapters/adapters.yaml` pour les capacités à sélecteur (`repository: <adapter>` ou `observability: <adapter>`) ; `api/fastapi`, `mcp/fastmcp` et `agent/langgraph` sont exposés par leurs fichiers `config/adapters/inbound/*.yaml`
|
|
69
116
|
5. **Récapitulatif** — liste des fichiers créés ou remplacés avant confirmation
|
|
70
117
|
|
|
71
118
|
| Option | Défaut | Description |
|
|
72
119
|
|--------|--------|-------------|
|
|
73
|
-
| `--capability` | `repository` | Capacité cible du catalogue standardisé (`repository`, `agent`, `observability`) |
|
|
74
|
-
| `--adapter` / `-a` | interactif | Adapter du catalogue : `memory`, `mongodb`, `duckdb`, `mariadb`, `langgraph`, `langsmith` |
|
|
120
|
+
| `--capability` | `repository` | Capacité cible du catalogue standardisé (`repository`, `api`, `mcp`, `agent`, `observability`) |
|
|
121
|
+
| `--adapter` / `-a` | interactif | Adapter du catalogue : `memory`, `mongodb`, `duckdb`, `mariadb`, `fastapi`, `fastmcp`, `langgraph`, `langsmith`, `opentelemetry` |
|
|
75
122
|
| `--entity` / `-e` | auto si une seule entité | Entité cible, liste séparée par virgule acceptée |
|
|
76
123
|
| `--all-entities` | `false` | Génère l'adapter pour toutes les entités détectées |
|
|
77
124
|
| `--activate/--no-activate` | `--activate` | Met à jour `config/adapters/adapters.yaml` quand la capacité expose une clé d'activation |
|
|
@@ -111,6 +158,17 @@ son agent. Comme `fastapi` et `fastmcp`, LangGraph est configuré par son nom pr
|
|
|
111
158
|
pour tester les agents. Une `LANGSMITH_API_KEY` déjà présente est conservée si aucune nouvelle valeur
|
|
112
159
|
n'est fournie.
|
|
113
160
|
|
|
161
|
+
**OpenTelemetry :**
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
uv add "arclith[opentelemetry]"
|
|
165
|
+
arclith-cli add-adapter --capability observability --adapter opentelemetry --param service_name=my-recipe-service --yes
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
L'adapter `observability/opentelemetry` génère `config/adapters/outbound/opentelemetry.yaml`, met à
|
|
169
|
+
jour `.env`, active `observability: opentelemetry` et branche l'instrumentation FastAPI quand
|
|
170
|
+
`Arclith.fastapi()` construit l'application.
|
|
171
|
+
|
|
114
172
|
Parcours complet avec entité, API, LangGraph, LangSmith et LM Studio:
|
|
115
173
|
[`docs/agent-quickstart.md`](../docs/agent-quickstart.md).
|
|
116
174
|
|
|
@@ -184,6 +242,7 @@ config/
|
|
|
184
242
|
duckdb.yaml # adapters.duckdb: { path, multitenant }
|
|
185
243
|
mariadb.yaml # adapters.mariadb: { host, port, database, user, ... }
|
|
186
244
|
langsmith.yaml # adapters.langsmith: { tracing, project, endpoint, ... }
|
|
245
|
+
opentelemetry.yaml # adapters.opentelemetry: { endpoint, protocol, traces, metrics, ... }
|
|
187
246
|
inbound/
|
|
188
247
|
fastapi.yaml # api: { host, port, reload }
|
|
189
248
|
fastmcp.yaml # mcp: { host, port }
|
|
@@ -199,7 +258,7 @@ Pour changer l'adapter actif sans passer par le wizard :
|
|
|
199
258
|
```yaml
|
|
200
259
|
# config/adapters/adapters.yaml
|
|
201
260
|
repository: duckdb # memory | mongodb | duckdb | mariadb
|
|
202
|
-
observability: langsmith
|
|
261
|
+
observability: langsmith # none | langsmith | opentelemetry
|
|
203
262
|
```
|
|
204
263
|
|
|
205
264
|
Pour MariaDB, ne committez pas le mot de passe. Mappez `adapters.mariadb.password` ou `adapters.mariadb.url` via `config/secrets.yaml`, un resolver `env` ou Vault.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.10.0"
|
|
@@ -217,6 +217,83 @@ multitenant: false
|
|
|
217
217
|
),
|
|
218
218
|
)
|
|
219
219
|
|
|
220
|
+
API_CAPABILITY = CapabilitySpec(
|
|
221
|
+
name="api",
|
|
222
|
+
layer="inbound",
|
|
223
|
+
description="Transport HTTP REST expose via FastAPI.",
|
|
224
|
+
activation_config_key=None,
|
|
225
|
+
adapters=(
|
|
226
|
+
AdapterSpec(
|
|
227
|
+
name="fastapi",
|
|
228
|
+
capability="api",
|
|
229
|
+
layer="inbound",
|
|
230
|
+
description="Application FastAPI configuree par Arclith.fastapi().",
|
|
231
|
+
config_path="config/adapters/inbound/fastapi.yaml",
|
|
232
|
+
config_template="""\
|
|
233
|
+
host: {host}
|
|
234
|
+
port: {port}
|
|
235
|
+
reload: {reload}
|
|
236
|
+
""",
|
|
237
|
+
parameters=(
|
|
238
|
+
ParameterSpec(
|
|
239
|
+
name="host",
|
|
240
|
+
kind="string",
|
|
241
|
+
prompt="Host FastAPI",
|
|
242
|
+
default="0.0.0.0",
|
|
243
|
+
),
|
|
244
|
+
ParameterSpec(
|
|
245
|
+
name="port",
|
|
246
|
+
kind="string",
|
|
247
|
+
prompt="Port FastAPI",
|
|
248
|
+
default="8000",
|
|
249
|
+
),
|
|
250
|
+
ParameterSpec(
|
|
251
|
+
name="reload",
|
|
252
|
+
kind="boolean",
|
|
253
|
+
prompt="Activer le reload FastAPI",
|
|
254
|
+
default=True,
|
|
255
|
+
),
|
|
256
|
+
),
|
|
257
|
+
entity_scoped=False,
|
|
258
|
+
),
|
|
259
|
+
),
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
MCP_CAPABILITY = CapabilitySpec(
|
|
263
|
+
name="mcp",
|
|
264
|
+
layer="inbound",
|
|
265
|
+
description="Transport MCP expose via FastMCP.",
|
|
266
|
+
activation_config_key=None,
|
|
267
|
+
adapters=(
|
|
268
|
+
AdapterSpec(
|
|
269
|
+
name="fastmcp",
|
|
270
|
+
capability="mcp",
|
|
271
|
+
layer="inbound",
|
|
272
|
+
description="Serveur FastMCP configure par Arclith.fastmcp() et les runners MCP.",
|
|
273
|
+
config_path="config/adapters/inbound/fastmcp.yaml",
|
|
274
|
+
config_template="""\
|
|
275
|
+
host: {host}
|
|
276
|
+
port: {port}
|
|
277
|
+
""",
|
|
278
|
+
parameters=(
|
|
279
|
+
ParameterSpec(
|
|
280
|
+
name="host",
|
|
281
|
+
kind="string",
|
|
282
|
+
prompt="Host FastMCP",
|
|
283
|
+
default="127.0.0.1",
|
|
284
|
+
),
|
|
285
|
+
ParameterSpec(
|
|
286
|
+
name="port",
|
|
287
|
+
kind="string",
|
|
288
|
+
prompt="Port FastMCP",
|
|
289
|
+
default="8001",
|
|
290
|
+
),
|
|
291
|
+
),
|
|
292
|
+
entity_scoped=False,
|
|
293
|
+
),
|
|
294
|
+
),
|
|
295
|
+
)
|
|
296
|
+
|
|
220
297
|
AGENT_CAPABILITY = CapabilitySpec(
|
|
221
298
|
name="agent",
|
|
222
299
|
layer="inbound",
|
|
@@ -351,10 +428,99 @@ LANGSMITH_API_KEY={api_key}
|
|
|
351
428
|
),
|
|
352
429
|
entity_scoped=False,
|
|
353
430
|
),
|
|
431
|
+
AdapterSpec(
|
|
432
|
+
name="opentelemetry",
|
|
433
|
+
capability="observability",
|
|
434
|
+
layer="outbound",
|
|
435
|
+
description="Export OTLP traces/metrics et instrumentation FastAPI.",
|
|
436
|
+
config_path="config/adapters/outbound/opentelemetry.yaml",
|
|
437
|
+
config_template="""\
|
|
438
|
+
enabled: {enabled}
|
|
439
|
+
service_name: "{service_name}"
|
|
440
|
+
endpoint: "{endpoint}"
|
|
441
|
+
protocol: "{protocol}"
|
|
442
|
+
headers_env: OTEL_EXPORTER_OTLP_HEADERS
|
|
443
|
+
traces: {traces}
|
|
444
|
+
metrics: {metrics}
|
|
445
|
+
instrument_fastapi: {instrument_fastapi}
|
|
446
|
+
metrics_export_interval_millis: {metrics_export_interval_millis}
|
|
447
|
+
""",
|
|
448
|
+
env_path=".env",
|
|
449
|
+
env_template="""\
|
|
450
|
+
OTEL_SERVICE_NAME={service_name}
|
|
451
|
+
OTEL_EXPORTER_OTLP_ENDPOINT={endpoint}
|
|
452
|
+
OTEL_EXPORTER_OTLP_PROTOCOL={protocol}
|
|
453
|
+
OTEL_EXPORTER_OTLP_HEADERS={headers}
|
|
454
|
+
""",
|
|
455
|
+
parameters=(
|
|
456
|
+
ParameterSpec(
|
|
457
|
+
name="enabled",
|
|
458
|
+
kind="boolean",
|
|
459
|
+
prompt="Activer OpenTelemetry",
|
|
460
|
+
default=True,
|
|
461
|
+
),
|
|
462
|
+
ParameterSpec(
|
|
463
|
+
name="service_name",
|
|
464
|
+
kind="string",
|
|
465
|
+
prompt="OTEL_SERVICE_NAME",
|
|
466
|
+
default_from_project_name=True,
|
|
467
|
+
),
|
|
468
|
+
ParameterSpec(
|
|
469
|
+
name="endpoint",
|
|
470
|
+
kind="string",
|
|
471
|
+
prompt="OTLP endpoint",
|
|
472
|
+
default="http://localhost:4318",
|
|
473
|
+
),
|
|
474
|
+
ParameterSpec(
|
|
475
|
+
name="protocol",
|
|
476
|
+
kind="string",
|
|
477
|
+
prompt="OTLP protocol",
|
|
478
|
+
default="http/protobuf",
|
|
479
|
+
),
|
|
480
|
+
ParameterSpec(
|
|
481
|
+
name="traces",
|
|
482
|
+
kind="boolean",
|
|
483
|
+
prompt="Exporter les traces",
|
|
484
|
+
default=True,
|
|
485
|
+
),
|
|
486
|
+
ParameterSpec(
|
|
487
|
+
name="metrics",
|
|
488
|
+
kind="boolean",
|
|
489
|
+
prompt="Exporter les metriques",
|
|
490
|
+
default=False,
|
|
491
|
+
),
|
|
492
|
+
ParameterSpec(
|
|
493
|
+
name="instrument_fastapi",
|
|
494
|
+
kind="boolean",
|
|
495
|
+
prompt="Instrumenter FastAPI",
|
|
496
|
+
default=True,
|
|
497
|
+
),
|
|
498
|
+
ParameterSpec(
|
|
499
|
+
name="metrics_export_interval_millis",
|
|
500
|
+
kind="string",
|
|
501
|
+
prompt="Intervalle export metriques en ms",
|
|
502
|
+
default="60000",
|
|
503
|
+
),
|
|
504
|
+
ParameterSpec(
|
|
505
|
+
name="headers",
|
|
506
|
+
kind="string",
|
|
507
|
+
prompt="OTEL_EXPORTER_OTLP_HEADERS",
|
|
508
|
+
default="",
|
|
509
|
+
secret=True,
|
|
510
|
+
),
|
|
511
|
+
),
|
|
512
|
+
entity_scoped=False,
|
|
513
|
+
),
|
|
354
514
|
),
|
|
355
515
|
)
|
|
356
516
|
|
|
357
|
-
CAPABILITY_CATALOG = (
|
|
517
|
+
CAPABILITY_CATALOG = (
|
|
518
|
+
REPOSITORY_CAPABILITY,
|
|
519
|
+
API_CAPABILITY,
|
|
520
|
+
MCP_CAPABILITY,
|
|
521
|
+
AGENT_CAPABILITY,
|
|
522
|
+
OBSERVABILITY_CAPABILITY,
|
|
523
|
+
)
|
|
358
524
|
|
|
359
525
|
|
|
360
526
|
def get_capability(name: str) -> CapabilitySpec | None:
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from .project_paths import ProjectPaths, detect_project_paths
|
|
11
|
+
from .rename import EntityNames
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
_NAME_RE = re.compile(r"^[A-Za-z][A-Za-z0-9_\-]*$")
|
|
16
|
+
|
|
17
|
+
_ENTITY_TEMPLATE = """from arclith.domain.models.entity import Entity
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class {class_name}(Entity):
|
|
21
|
+
pass
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
_USE_CASE_TEMPLATE = """class {class_name}UseCase:
|
|
25
|
+
async def execute(self) -> None:
|
|
26
|
+
raise NotImplementedError("Implement {class_name}UseCase.execute")
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class UseCaseNames:
|
|
32
|
+
pascal: str
|
|
33
|
+
snake: str
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def from_input(cls, raw: str) -> UseCaseNames:
|
|
37
|
+
names = EntityNames.from_input(raw)
|
|
38
|
+
pascal = _strip_pascal_suffix(names.pascal)
|
|
39
|
+
snake = _strip_snake_suffix(names.snake)
|
|
40
|
+
if not pascal or not snake:
|
|
41
|
+
console.print(f"[red]✗[/red] Nom de cas d'usage invalide : [bold]{raw}[/bold].")
|
|
42
|
+
raise typer.Exit(1)
|
|
43
|
+
return cls(pascal=pascal, snake=snake)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def add_entity_cmd(*, project_dir: Path | None = None, entity_name: str) -> Path:
|
|
47
|
+
project_dir = project_dir or Path.cwd()
|
|
48
|
+
entity_name = entity_name.strip()
|
|
49
|
+
_assert_project_root(project_dir, command="add-entity")
|
|
50
|
+
_assert_valid_name(entity_name, label="entité")
|
|
51
|
+
|
|
52
|
+
paths = detect_project_paths(project_dir)
|
|
53
|
+
names = EntityNames.from_input(entity_name)
|
|
54
|
+
entity_file = paths.domain_models / f"{names.snake}.py"
|
|
55
|
+
_assert_missing(entity_file, project_dir)
|
|
56
|
+
|
|
57
|
+
_ensure_package_dirs(paths, "domain", "models")
|
|
58
|
+
entity_file.write_text(_ENTITY_TEMPLATE.format(class_name=names.pascal), encoding="utf-8")
|
|
59
|
+
console.print(
|
|
60
|
+
f"[green]✓[/green] Entité {names.pascal} créée : "
|
|
61
|
+
f"[bold]{entity_file.relative_to(project_dir)}[/bold]"
|
|
62
|
+
)
|
|
63
|
+
return entity_file
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def add_usecase_cmd(*, project_dir: Path | None = None, usecase_name: str) -> Path:
|
|
67
|
+
project_dir = project_dir or Path.cwd()
|
|
68
|
+
usecase_name = usecase_name.strip()
|
|
69
|
+
_assert_project_root(project_dir, command="add-usecase")
|
|
70
|
+
_assert_valid_name(usecase_name, label="cas d'usage")
|
|
71
|
+
|
|
72
|
+
paths = detect_project_paths(project_dir)
|
|
73
|
+
names = UseCaseNames.from_input(usecase_name)
|
|
74
|
+
usecase_file = paths.application_use_cases / f"{names.snake}.py"
|
|
75
|
+
_assert_missing(usecase_file, project_dir)
|
|
76
|
+
|
|
77
|
+
_ensure_package_dirs(paths, "application", "use_cases")
|
|
78
|
+
usecase_file.write_text(_USE_CASE_TEMPLATE.format(class_name=names.pascal), encoding="utf-8")
|
|
79
|
+
console.print(
|
|
80
|
+
f"[green]✓[/green] Cas d'usage {names.pascal}UseCase créé : "
|
|
81
|
+
f"[bold]{usecase_file.relative_to(project_dir)}[/bold]"
|
|
82
|
+
)
|
|
83
|
+
return usecase_file
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _assert_project_root(project_dir: Path, *, command: str) -> None:
|
|
87
|
+
if not project_dir.exists() or not project_dir.is_dir():
|
|
88
|
+
console.print(f"[red]✗[/red] Répertoire introuvable : [bold]{project_dir}[/bold].")
|
|
89
|
+
raise typer.Exit(1)
|
|
90
|
+
|
|
91
|
+
has_project_marker = (
|
|
92
|
+
(project_dir / "pyproject.toml").exists()
|
|
93
|
+
or (project_dir / "src").is_dir()
|
|
94
|
+
or (project_dir / "config").is_dir()
|
|
95
|
+
)
|
|
96
|
+
if has_project_marker:
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
console.print(
|
|
100
|
+
f"[red]✗[/red] Aucun projet arclith détecté.\n"
|
|
101
|
+
f" Exécutez [bold]arclith-cli {command}[/bold] depuis la racine du projet."
|
|
102
|
+
)
|
|
103
|
+
raise typer.Exit(1)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _assert_valid_name(raw: str, *, label: str) -> None:
|
|
107
|
+
if _NAME_RE.match(raw.strip()):
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
console.print(
|
|
111
|
+
f"[red]✗[/red] Nom de {label} invalide : [bold]{raw}[/bold]. "
|
|
112
|
+
"Lettres, chiffres, _ et - uniquement. Doit commencer par une lettre."
|
|
113
|
+
)
|
|
114
|
+
raise typer.Exit(1)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _assert_missing(path: Path, project_dir: Path) -> None:
|
|
118
|
+
if not path.exists():
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
console.print(f"[red]✗[/red] Le fichier existe déjà : [bold]{path.relative_to(project_dir)}[/bold].")
|
|
122
|
+
raise typer.Exit(1)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _ensure_package_dirs(paths: ProjectPaths, *relative_parts: str) -> None:
|
|
126
|
+
target_dir = paths.package_root.joinpath(*relative_parts)
|
|
127
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
128
|
+
|
|
129
|
+
stop_at = paths.package_root.parent if paths.package_name is not None else paths.package_root
|
|
130
|
+
init_dirs = [target_dir]
|
|
131
|
+
for parent in target_dir.parents:
|
|
132
|
+
if parent == stop_at:
|
|
133
|
+
break
|
|
134
|
+
init_dirs.append(parent)
|
|
135
|
+
|
|
136
|
+
for directory in init_dirs:
|
|
137
|
+
init_file = directory / "__init__.py"
|
|
138
|
+
if not init_file.exists():
|
|
139
|
+
init_file.write_text("", encoding="utf-8")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _strip_pascal_suffix(value: str) -> str:
|
|
143
|
+
suffixes = ("UseCase", "Usecase")
|
|
144
|
+
while True:
|
|
145
|
+
stripped = value
|
|
146
|
+
for suffix in suffixes:
|
|
147
|
+
if stripped.endswith(suffix):
|
|
148
|
+
stripped = stripped[: -len(suffix)]
|
|
149
|
+
break
|
|
150
|
+
if stripped == value:
|
|
151
|
+
return value
|
|
152
|
+
value = stripped
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _strip_snake_suffix(value: str) -> str:
|
|
156
|
+
suffixes = ("_use_case", "_usecase")
|
|
157
|
+
while True:
|
|
158
|
+
stripped = value
|
|
159
|
+
for suffix in suffixes:
|
|
160
|
+
if stripped.endswith(suffix):
|
|
161
|
+
stripped = stripped[: -len(suffix)]
|
|
162
|
+
break
|
|
163
|
+
if stripped == value:
|
|
164
|
+
return value
|
|
165
|
+
value = stripped
|
|
@@ -15,6 +15,7 @@ from rich.tree import Tree
|
|
|
15
15
|
from . import __version__
|
|
16
16
|
from .add_adapter import add_adapter_cmd
|
|
17
17
|
from .capabilities import CAPABILITY_CATALOG, capability_catalog_as_dict
|
|
18
|
+
from .core_scaffold import add_entity_cmd, add_usecase_cmd
|
|
18
19
|
from .export_config import export_config_cmd
|
|
19
20
|
from .rename import EntityNames, apply_rename
|
|
20
21
|
from .scaffold import download_and_extract
|
|
@@ -121,7 +122,7 @@ def version() -> None:
|
|
|
121
122
|
def add_adapter(
|
|
122
123
|
capability: Annotated[
|
|
123
124
|
str,
|
|
124
|
-
typer.Option("--capability", help="Capacité cible: repository, agent ou observability"),
|
|
125
|
+
typer.Option("--capability", help="Capacité cible: repository, api, mcp, agent ou observability"),
|
|
125
126
|
] = "repository",
|
|
126
127
|
adapter: Annotated[
|
|
127
128
|
str | None,
|
|
@@ -178,6 +179,32 @@ def add_adapter(
|
|
|
178
179
|
)
|
|
179
180
|
|
|
180
181
|
|
|
182
|
+
@app.command(name="add-entity")
|
|
183
|
+
def add_entity(
|
|
184
|
+
entity: Annotated[
|
|
185
|
+
str | None,
|
|
186
|
+
typer.Argument(
|
|
187
|
+
help="Nom de l'entité métier au singulier. Exemple : Recipe, recipe_step, meal-plan",
|
|
188
|
+
),
|
|
189
|
+
] = None,
|
|
190
|
+
) -> None:
|
|
191
|
+
"""Créer uniquement le fichier minimal d'une entité métier dans domain/models."""
|
|
192
|
+
add_entity_cmd(entity_name=entity or _prompt_entity())
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@app.command(name="add-usecase")
|
|
196
|
+
def add_usecase(
|
|
197
|
+
usecase: Annotated[
|
|
198
|
+
str | None,
|
|
199
|
+
typer.Argument(
|
|
200
|
+
help="Nom du cas d'usage. Exemple : PlanShoppingList, find_by_name, import-catalog",
|
|
201
|
+
),
|
|
202
|
+
] = None,
|
|
203
|
+
) -> None:
|
|
204
|
+
"""Créer uniquement le fichier minimal d'un cas d'usage dans application/use_cases."""
|
|
205
|
+
add_usecase_cmd(usecase_name=usecase or _prompt_usecase())
|
|
206
|
+
|
|
207
|
+
|
|
181
208
|
@app.command(name="capabilities")
|
|
182
209
|
def capabilities(
|
|
183
210
|
as_json: Annotated[
|
|
@@ -256,6 +283,21 @@ def _prompt_project() -> str:
|
|
|
256
283
|
return value
|
|
257
284
|
|
|
258
285
|
|
|
286
|
+
def _prompt_usecase() -> str:
|
|
287
|
+
console.print("\n[bold]Cas d'usage[/bold] [dim](ex : PlanShoppingList, find_by_name)[/dim]")
|
|
288
|
+
while True:
|
|
289
|
+
value = Prompt.ask(" [bold green]Nom du cas d'usage[/bold green]").strip()
|
|
290
|
+
if not value:
|
|
291
|
+
console.print(" [red]Le nom ne peut pas être vide.[/red]")
|
|
292
|
+
elif not _ENTITY_RE.match(value):
|
|
293
|
+
console.print(
|
|
294
|
+
" [red]Caractères invalides.[/red] "
|
|
295
|
+
"[dim]Lettres, chiffres, _ et - uniquement. Doit commencer par une lettre.[/dim]"
|
|
296
|
+
)
|
|
297
|
+
else:
|
|
298
|
+
return value
|
|
299
|
+
|
|
300
|
+
|
|
259
301
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
260
302
|
|
|
261
303
|
def _split_entity_option(value: str | None) -> list[str] | None:
|
|
@@ -12,6 +12,10 @@ class ProjectPaths:
|
|
|
12
12
|
def domain_models(self) -> Path:
|
|
13
13
|
return self.package_root / "domain" / "models"
|
|
14
14
|
|
|
15
|
+
@property
|
|
16
|
+
def application_use_cases(self) -> Path:
|
|
17
|
+
return self.package_root / "application" / "use_cases"
|
|
18
|
+
|
|
15
19
|
@property
|
|
16
20
|
def adapters_outbound(self) -> Path:
|
|
17
21
|
return self.package_root / "adapters" / "outbound"
|
|
@@ -33,13 +37,29 @@ def detect_project_paths(project_dir: Path) -> ProjectPaths:
|
|
|
33
37
|
"""Detect the importable application package for an Arclith project."""
|
|
34
38
|
src_dir = project_dir / "src"
|
|
35
39
|
if src_dir.exists():
|
|
36
|
-
|
|
40
|
+
model_candidates = sorted(
|
|
37
41
|
child
|
|
38
42
|
for child in src_dir.iterdir()
|
|
39
43
|
if child.is_dir() and (child / "domain" / "models").exists()
|
|
40
44
|
)
|
|
41
|
-
if
|
|
42
|
-
package_root =
|
|
45
|
+
if model_candidates:
|
|
46
|
+
package_root = model_candidates[0]
|
|
47
|
+
return ProjectPaths(root=project_dir, package_root=package_root, package_name=package_root.name)
|
|
48
|
+
|
|
49
|
+
package_candidates = sorted(
|
|
50
|
+
child
|
|
51
|
+
for child in src_dir.iterdir()
|
|
52
|
+
if child.is_dir() and _looks_like_package_root(child)
|
|
53
|
+
)
|
|
54
|
+
if package_candidates:
|
|
55
|
+
package_root = package_candidates[0]
|
|
43
56
|
return ProjectPaths(root=project_dir, package_root=package_root, package_name=package_root.name)
|
|
44
57
|
|
|
45
58
|
return ProjectPaths(root=project_dir, package_root=project_dir, package_name=None)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _looks_like_package_root(path: Path) -> bool:
|
|
62
|
+
if (path / "__init__.py").exists():
|
|
63
|
+
return True
|
|
64
|
+
|
|
65
|
+
return any((path / layer).is_dir() for layer in ("domain", "application", "adapters", "infrastructure"))
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
from dataclasses import dataclass
|
|
5
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
|
|
7
8
|
_SOURCE_PASCAL = "Ingredient"
|
|
@@ -89,9 +90,10 @@ def _patch_pyproject(target_dir: Path, project_name: str) -> None:
|
|
|
89
90
|
p = target_dir / "pyproject.toml"
|
|
90
91
|
if not p.exists():
|
|
91
92
|
return
|
|
92
|
-
text = p.read_text()
|
|
93
|
+
text = p.read_text(encoding="utf-8")
|
|
93
94
|
# Update project name (first occurrence)
|
|
94
95
|
text = re.sub(r'(?m)^name\s*=\s*"[^"]*"', f'name = "{project_name}"', text, count=1)
|
|
96
|
+
text = _patch_arclith_dependency(text)
|
|
95
97
|
# Remove [tool.uv.sources] block (editable arclith path)
|
|
96
98
|
text = re.sub(
|
|
97
99
|
r"\[tool\.uv\.sources\]\n(?:[^\[]*)",
|
|
@@ -99,7 +101,26 @@ def _patch_pyproject(target_dir: Path, project_name: str) -> None:
|
|
|
99
101
|
text,
|
|
100
102
|
flags=re.DOTALL,
|
|
101
103
|
)
|
|
102
|
-
p.write_text(text.strip() + "\n")
|
|
104
|
+
p.write_text(text.strip() + "\n", encoding="utf-8")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _patch_arclith_dependency(text: str) -> str:
|
|
108
|
+
framework_version = _installed_arclith_version()
|
|
109
|
+
if framework_version is None:
|
|
110
|
+
return text
|
|
111
|
+
return re.sub(
|
|
112
|
+
r'("arclith(?:\[[^"]+\])?>=)[^"]+(")',
|
|
113
|
+
lambda match: f"{match.group(1)}{framework_version}{match.group(2)}",
|
|
114
|
+
text,
|
|
115
|
+
count=1,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _installed_arclith_version() -> str | None:
|
|
120
|
+
try:
|
|
121
|
+
return version("arclith")
|
|
122
|
+
except PackageNotFoundError:
|
|
123
|
+
return None
|
|
103
124
|
|
|
104
125
|
|
|
105
126
|
# ── config/ directory patching ────────────────────────────────────────────────
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "arclith-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.10.0"
|
|
8
8
|
description = "CLI scaffolding tool for arclith — hexagonal architecture framework"
|
|
9
9
|
license = { file = "LICENSE" }
|
|
10
10
|
requires-python = ">=3.13"
|
|
@@ -16,7 +16,7 @@ dependencies = [
|
|
|
16
16
|
"typer>=0.15.0",
|
|
17
17
|
"rich>=13.0.0",
|
|
18
18
|
"httpx>=0.27.0",
|
|
19
|
-
"arclith>=0.
|
|
19
|
+
"arclith>=0.13.0",
|
|
20
20
|
]
|
|
21
21
|
|
|
22
22
|
[tool.uv.sources]
|
|
@@ -39,7 +39,7 @@ if grep -q '\[tool\.uv\.sources\]' pyproject.toml; then
|
|
|
39
39
|
fi
|
|
40
40
|
|
|
41
41
|
ARCLITH_VERSION=$(grep 'arclith\[' pyproject.toml | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
|
|
42
|
-
MIN_ARCLITH_VERSION="0.
|
|
42
|
+
MIN_ARCLITH_VERSION="0.13.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
|
|
@@ -194,6 +194,106 @@ def test_add_langsmith_skips_missing_empty_api_key(tmp_path: Path) -> None:
|
|
|
194
194
|
assert "LANGSMITH_PROJECT=agent-tests" in env
|
|
195
195
|
|
|
196
196
|
|
|
197
|
+
def test_add_fastapi_api_adapter_generates_inbound_config_only(tmp_path: Path) -> None:
|
|
198
|
+
project_dir = _minimal_project(tmp_path)
|
|
199
|
+
|
|
200
|
+
add_adapter_cmd(
|
|
201
|
+
project_dir=project_dir,
|
|
202
|
+
capability_name="api",
|
|
203
|
+
adapter="fastapi",
|
|
204
|
+
adapter_params={
|
|
205
|
+
"host": "127.0.0.1",
|
|
206
|
+
"port": "8080",
|
|
207
|
+
"reload": "false",
|
|
208
|
+
},
|
|
209
|
+
yes=True,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
config = (project_dir / "config" / "adapters" / "inbound" / "fastapi.yaml").read_text(
|
|
213
|
+
encoding="utf-8"
|
|
214
|
+
)
|
|
215
|
+
assert "host: 127.0.0.1" in config
|
|
216
|
+
assert "port: 8080" in config
|
|
217
|
+
assert "reload: false" in config
|
|
218
|
+
assert "repository: memory" in (project_dir / "config" / "adapters" / "adapters.yaml").read_text(
|
|
219
|
+
encoding="utf-8"
|
|
220
|
+
)
|
|
221
|
+
package_root = project_dir / "src" / "demo_service"
|
|
222
|
+
assert not (package_root / "adapters" / "outbound" / "fastapi").exists()
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_add_fastmcp_mcp_adapter_generates_inbound_config_only(tmp_path: Path) -> None:
|
|
226
|
+
project_dir = _minimal_project(tmp_path)
|
|
227
|
+
|
|
228
|
+
add_adapter_cmd(
|
|
229
|
+
project_dir=project_dir,
|
|
230
|
+
capability_name="mcp",
|
|
231
|
+
adapter="fastmcp",
|
|
232
|
+
adapter_params={
|
|
233
|
+
"host": "127.0.0.1",
|
|
234
|
+
"port": "9001",
|
|
235
|
+
},
|
|
236
|
+
yes=True,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
config = (project_dir / "config" / "adapters" / "inbound" / "fastmcp.yaml").read_text(
|
|
240
|
+
encoding="utf-8"
|
|
241
|
+
)
|
|
242
|
+
assert "host: 127.0.0.1" in config
|
|
243
|
+
assert "port: 9001" in config
|
|
244
|
+
assert "repository: memory" in (project_dir / "config" / "adapters" / "adapters.yaml").read_text(
|
|
245
|
+
encoding="utf-8"
|
|
246
|
+
)
|
|
247
|
+
package_root = project_dir / "src" / "demo_service"
|
|
248
|
+
assert not (package_root / "adapters" / "outbound" / "fastmcp").exists()
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def test_add_opentelemetry_observability_adapter_uses_catalog_params(tmp_path: Path) -> None:
|
|
252
|
+
project_dir = _minimal_project(tmp_path)
|
|
253
|
+
(project_dir / ".env").write_text("EXISTING=value\n", encoding="utf-8")
|
|
254
|
+
|
|
255
|
+
add_adapter_cmd(
|
|
256
|
+
project_dir=project_dir,
|
|
257
|
+
capability_name="observability",
|
|
258
|
+
adapter="opentelemetry",
|
|
259
|
+
adapter_params={
|
|
260
|
+
"service_name": "demo-api",
|
|
261
|
+
"endpoint": "http://otel-collector:4318",
|
|
262
|
+
"protocol": "http/protobuf",
|
|
263
|
+
"traces": "true",
|
|
264
|
+
"metrics": "true",
|
|
265
|
+
"instrument_fastapi": "true",
|
|
266
|
+
"metrics_export_interval_millis": "15000",
|
|
267
|
+
"headers": "authorization=Bearer test",
|
|
268
|
+
},
|
|
269
|
+
yes=True,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
config = (project_dir / "config" / "adapters" / "outbound" / "opentelemetry.yaml").read_text(
|
|
273
|
+
encoding="utf-8"
|
|
274
|
+
)
|
|
275
|
+
assert "enabled: true" in config
|
|
276
|
+
assert 'service_name: "demo-api"' in config
|
|
277
|
+
assert 'endpoint: "http://otel-collector:4318"' in config
|
|
278
|
+
assert 'protocol: "http/protobuf"' in config
|
|
279
|
+
assert "headers_env: OTEL_EXPORTER_OTLP_HEADERS" in config
|
|
280
|
+
assert "traces: true" in config
|
|
281
|
+
assert "metrics: true" in config
|
|
282
|
+
assert "instrument_fastapi: true" in config
|
|
283
|
+
assert "metrics_export_interval_millis: 15000" in config
|
|
284
|
+
assert "observability: opentelemetry" in (
|
|
285
|
+
project_dir / "config" / "adapters" / "adapters.yaml"
|
|
286
|
+
).read_text(encoding="utf-8")
|
|
287
|
+
|
|
288
|
+
env = (project_dir / ".env").read_text(encoding="utf-8")
|
|
289
|
+
assert "EXISTING=value" in env
|
|
290
|
+
assert "OTEL_SERVICE_NAME=demo-api" in env
|
|
291
|
+
assert "OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318" in env
|
|
292
|
+
assert "OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf" in env
|
|
293
|
+
assert "OTEL_EXPORTER_OTLP_HEADERS=authorization=Bearer test" in env
|
|
294
|
+
assert ".env" in (project_dir / ".gitignore").read_text(encoding="utf-8")
|
|
295
|
+
|
|
296
|
+
|
|
197
297
|
def test_add_langgraph_agent_adapter_generates_runtime_entrypoint(tmp_path: Path) -> None:
|
|
198
298
|
project_dir = _minimal_project(tmp_path)
|
|
199
299
|
|
|
@@ -242,6 +342,19 @@ def test_add_non_entity_adapter_rejects_entity_selection(tmp_path: Path) -> None
|
|
|
242
342
|
)
|
|
243
343
|
|
|
244
344
|
|
|
345
|
+
def test_add_transport_adapter_rejects_entity_selection(tmp_path: Path) -> None:
|
|
346
|
+
project_dir = _minimal_project(tmp_path)
|
|
347
|
+
|
|
348
|
+
with pytest.raises(typer.Exit):
|
|
349
|
+
add_adapter_cmd(
|
|
350
|
+
project_dir=project_dir,
|
|
351
|
+
capability_name="api",
|
|
352
|
+
adapter="fastapi",
|
|
353
|
+
entity_names=["Widget"],
|
|
354
|
+
yes=True,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
|
|
245
358
|
def test_add_adapter_rejects_unknown_catalog_param(tmp_path: Path) -> None:
|
|
246
359
|
project_dir = _minimal_project(tmp_path)
|
|
247
360
|
|
|
@@ -23,7 +23,7 @@ def test_observability_capability_catalog_declares_langsmith() -> None:
|
|
|
23
23
|
assert capability is not None
|
|
24
24
|
assert capability.layer == "outbound"
|
|
25
25
|
assert capability.activation_config_key == "observability"
|
|
26
|
-
assert capability.adapter_names() == ("langsmith",)
|
|
26
|
+
assert capability.adapter_names() == ("langsmith", "opentelemetry")
|
|
27
27
|
langsmith = capability.get_adapter("langsmith")
|
|
28
28
|
assert langsmith is not None
|
|
29
29
|
assert langsmith.config_path == "config/adapters/outbound/langsmith.yaml"
|
|
@@ -37,6 +37,56 @@ def test_observability_capability_catalog_declares_langsmith() -> None:
|
|
|
37
37
|
]
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
def test_api_capability_catalog_declares_fastapi() -> None:
|
|
41
|
+
capability = get_capability("api")
|
|
42
|
+
|
|
43
|
+
assert capability is not None
|
|
44
|
+
assert capability.layer == "inbound"
|
|
45
|
+
assert capability.activation_config_key is None
|
|
46
|
+
assert capability.adapter_names() == ("fastapi",)
|
|
47
|
+
fastapi = capability.get_adapter("fastapi")
|
|
48
|
+
assert fastapi is not None
|
|
49
|
+
assert fastapi.config_path == "config/adapters/inbound/fastapi.yaml"
|
|
50
|
+
assert fastapi.entity_scoped is False
|
|
51
|
+
assert [parameter.name for parameter in fastapi.parameters] == ["host", "port", "reload"]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_mcp_capability_catalog_declares_fastmcp() -> None:
|
|
55
|
+
capability = get_capability("mcp")
|
|
56
|
+
|
|
57
|
+
assert capability is not None
|
|
58
|
+
assert capability.layer == "inbound"
|
|
59
|
+
assert capability.activation_config_key is None
|
|
60
|
+
assert capability.adapter_names() == ("fastmcp",)
|
|
61
|
+
fastmcp = capability.get_adapter("fastmcp")
|
|
62
|
+
assert fastmcp is not None
|
|
63
|
+
assert fastmcp.config_path == "config/adapters/inbound/fastmcp.yaml"
|
|
64
|
+
assert fastmcp.entity_scoped is False
|
|
65
|
+
assert [parameter.name for parameter in fastmcp.parameters] == ["host", "port"]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_observability_capability_catalog_declares_opentelemetry() -> None:
|
|
69
|
+
capability = get_capability("observability")
|
|
70
|
+
|
|
71
|
+
assert capability is not None
|
|
72
|
+
opentelemetry = capability.get_adapter("opentelemetry")
|
|
73
|
+
assert opentelemetry is not None
|
|
74
|
+
assert opentelemetry.config_path == "config/adapters/outbound/opentelemetry.yaml"
|
|
75
|
+
assert opentelemetry.env_path == ".env"
|
|
76
|
+
assert opentelemetry.entity_scoped is False
|
|
77
|
+
assert [parameter.name for parameter in opentelemetry.parameters] == [
|
|
78
|
+
"enabled",
|
|
79
|
+
"service_name",
|
|
80
|
+
"endpoint",
|
|
81
|
+
"protocol",
|
|
82
|
+
"traces",
|
|
83
|
+
"metrics",
|
|
84
|
+
"instrument_fastapi",
|
|
85
|
+
"metrics_export_interval_millis",
|
|
86
|
+
"headers",
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
|
|
40
90
|
def test_agent_capability_catalog_declares_langgraph() -> None:
|
|
41
91
|
capability = get_capability("agent")
|
|
42
92
|
|
|
@@ -89,10 +139,15 @@ def test_capability_catalog_is_json_serializable() -> None:
|
|
|
89
139
|
|
|
90
140
|
assert "repository" in encoded
|
|
91
141
|
assert "mongodb" in encoded
|
|
142
|
+
assert "api" in encoded
|
|
143
|
+
assert "fastapi" in encoded
|
|
144
|
+
assert "mcp" in encoded
|
|
145
|
+
assert "fastmcp" in encoded
|
|
92
146
|
assert "agent" in encoded
|
|
93
147
|
assert "langgraph" in encoded
|
|
94
148
|
assert "observability" in encoded
|
|
95
149
|
assert "langsmith" in encoded
|
|
150
|
+
assert "opentelemetry" in encoded
|
|
96
151
|
|
|
97
152
|
|
|
98
153
|
def test_capabilities_command_outputs_json_catalog() -> None:
|
|
@@ -107,7 +162,11 @@ def test_capabilities_command_outputs_json_catalog() -> None:
|
|
|
107
162
|
payload = json.loads(result.stdout)
|
|
108
163
|
assert payload[0]["name"] == "repository"
|
|
109
164
|
assert [adapter["name"] for adapter in payload[0]["adapters"]] == ["memory", "mongodb", "duckdb", "mariadb"]
|
|
110
|
-
assert payload[1]["name"] == "
|
|
111
|
-
assert [adapter["name"] for adapter in payload[1]["adapters"]] == ["
|
|
112
|
-
assert payload[2]["name"] == "
|
|
113
|
-
assert [adapter["name"] for adapter in payload[2]["adapters"]] == ["
|
|
165
|
+
assert payload[1]["name"] == "api"
|
|
166
|
+
assert [adapter["name"] for adapter in payload[1]["adapters"]] == ["fastapi"]
|
|
167
|
+
assert payload[2]["name"] == "mcp"
|
|
168
|
+
assert [adapter["name"] for adapter in payload[2]["adapters"]] == ["fastmcp"]
|
|
169
|
+
assert payload[3]["name"] == "agent"
|
|
170
|
+
assert [adapter["name"] for adapter in payload[3]["adapters"]] == ["langgraph"]
|
|
171
|
+
assert payload[4]["name"] == "observability"
|
|
172
|
+
assert [adapter["name"] for adapter in payload[4]["adapters"]] == ["langsmith", "opentelemetry"]
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from arclith_cli.core_scaffold import add_entity_cmd, add_usecase_cmd
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _src_project(tmp_path: Path, package_name: str = "demo_service") -> Path:
|
|
10
|
+
project_dir = tmp_path / "demo-service"
|
|
11
|
+
package_root = project_dir / "src" / package_name
|
|
12
|
+
package_root.mkdir(parents=True)
|
|
13
|
+
(package_root / "__init__.py").write_text("", encoding="utf-8")
|
|
14
|
+
(project_dir / "pyproject.toml").write_text('[project]\nname = "demo-service"\n', encoding="utf-8")
|
|
15
|
+
return project_dir
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_add_entity_creates_minimal_entity_in_src_package(tmp_path: Path) -> None:
|
|
19
|
+
project_dir = _src_project(tmp_path)
|
|
20
|
+
|
|
21
|
+
generated = add_entity_cmd(project_dir=project_dir, entity_name="ShoppingItem")
|
|
22
|
+
|
|
23
|
+
assert generated == project_dir / "src" / "demo_service" / "domain" / "models" / "shopping_item.py"
|
|
24
|
+
assert generated.read_text(encoding="utf-8") == (
|
|
25
|
+
"from arclith.domain.models.entity import Entity\n"
|
|
26
|
+
"\n"
|
|
27
|
+
"\n"
|
|
28
|
+
"class ShoppingItem(Entity):\n"
|
|
29
|
+
" pass\n"
|
|
30
|
+
)
|
|
31
|
+
assert (project_dir / "src" / "demo_service" / "domain" / "__init__.py").exists()
|
|
32
|
+
assert (project_dir / "src" / "demo_service" / "domain" / "models" / "__init__.py").exists()
|
|
33
|
+
assert not (project_dir / "src" / "demo_service" / "adapters").exists()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_add_usecase_creates_minimal_usecase_in_src_package(tmp_path: Path) -> None:
|
|
37
|
+
project_dir = _src_project(tmp_path)
|
|
38
|
+
|
|
39
|
+
generated = add_usecase_cmd(project_dir=project_dir, usecase_name="PlanShoppingList")
|
|
40
|
+
|
|
41
|
+
assert generated == project_dir / "src" / "demo_service" / "application" / "use_cases" / "plan_shopping_list.py"
|
|
42
|
+
assert generated.read_text(encoding="utf-8") == (
|
|
43
|
+
"class PlanShoppingListUseCase:\n"
|
|
44
|
+
" async def execute(self) -> None:\n"
|
|
45
|
+
' raise NotImplementedError("Implement PlanShoppingListUseCase.execute")\n'
|
|
46
|
+
)
|
|
47
|
+
assert (project_dir / "src" / "demo_service" / "application" / "__init__.py").exists()
|
|
48
|
+
assert (project_dir / "src" / "demo_service" / "application" / "use_cases" / "__init__.py").exists()
|
|
49
|
+
assert not (project_dir / "src" / "demo_service" / "adapters").exists()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_add_usecase_strips_repeated_usecase_suffix(tmp_path: Path) -> None:
|
|
53
|
+
project_dir = _src_project(tmp_path)
|
|
54
|
+
|
|
55
|
+
generated = add_usecase_cmd(project_dir=project_dir, usecase_name="plan-shopping-list-use-case-usecase")
|
|
56
|
+
|
|
57
|
+
assert generated.name == "plan_shopping_list.py"
|
|
58
|
+
assert "class PlanShoppingListUseCase:" in generated.read_text(encoding="utf-8")
|
|
59
|
+
assert "UseCaseUseCase" not in generated.read_text(encoding="utf-8")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_add_entity_supports_legacy_root_layout(tmp_path: Path) -> None:
|
|
63
|
+
project_dir = tmp_path / "legacy-service"
|
|
64
|
+
project_dir.mkdir(parents=True)
|
|
65
|
+
(project_dir / "pyproject.toml").write_text('[project]\nname = "legacy-service"\n', encoding="utf-8")
|
|
66
|
+
|
|
67
|
+
generated = add_entity_cmd(project_dir=project_dir, entity_name="Recipe")
|
|
68
|
+
|
|
69
|
+
assert generated == project_dir / "domain" / "models" / "recipe.py"
|
|
70
|
+
assert (project_dir / "domain" / "__init__.py").exists()
|
|
71
|
+
assert (project_dir / "domain" / "models" / "__init__.py").exists()
|
|
72
|
+
assert not (project_dir / "__init__.py").exists()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_add_entity_refuses_to_overwrite_existing_file(tmp_path: Path) -> None:
|
|
76
|
+
project_dir = _src_project(tmp_path)
|
|
77
|
+
existing = project_dir / "src" / "demo_service" / "domain" / "models" / "recipe.py"
|
|
78
|
+
existing.parent.mkdir(parents=True)
|
|
79
|
+
existing.write_text("class Recipe:\n pass\n", encoding="utf-8")
|
|
80
|
+
|
|
81
|
+
with pytest.raises(typer.Exit):
|
|
82
|
+
add_entity_cmd(project_dir=project_dir, entity_name="Recipe")
|
|
83
|
+
|
|
84
|
+
assert existing.read_text(encoding="utf-8") == "class Recipe:\n pass\n"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_add_usecase_rejects_invalid_name(tmp_path: Path) -> None:
|
|
88
|
+
project_dir = _src_project(tmp_path)
|
|
89
|
+
|
|
90
|
+
with pytest.raises(typer.Exit):
|
|
91
|
+
add_usecase_cmd(project_dir=project_dir, usecase_name="123-import")
|
|
@@ -136,7 +136,63 @@ def test_scaffold_and_run(temp_workspace: Path):
|
|
|
136
136
|
for fname in expected_files:
|
|
137
137
|
assert (project_dir / fname).exists(), f"Missing expected file: {fname}"
|
|
138
138
|
|
|
139
|
-
# Step 6 — validate
|
|
139
|
+
# Step 6 — validate core scaffolding through the CLI entry point
|
|
140
|
+
result = subprocess.run(
|
|
141
|
+
[
|
|
142
|
+
"arclith-cli",
|
|
143
|
+
"add-entity",
|
|
144
|
+
"ShoppingItem",
|
|
145
|
+
],
|
|
146
|
+
cwd=project_dir,
|
|
147
|
+
capture_output=True,
|
|
148
|
+
text=True,
|
|
149
|
+
timeout=30,
|
|
150
|
+
)
|
|
151
|
+
assert result.returncode == 0, f"add-entity failed:\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
|
|
152
|
+
assert (
|
|
153
|
+
project_dir / "src" / "test_plan_service" / "domain" / "models" / "shopping_item.py"
|
|
154
|
+
).exists()
|
|
155
|
+
|
|
156
|
+
result = subprocess.run(
|
|
157
|
+
[
|
|
158
|
+
"arclith-cli",
|
|
159
|
+
"add-usecase",
|
|
160
|
+
"PlanShoppingList",
|
|
161
|
+
],
|
|
162
|
+
cwd=project_dir,
|
|
163
|
+
capture_output=True,
|
|
164
|
+
text=True,
|
|
165
|
+
timeout=30,
|
|
166
|
+
)
|
|
167
|
+
assert result.returncode == 0, f"add-usecase failed:\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
|
|
168
|
+
assert (
|
|
169
|
+
project_dir / "src" / "test_plan_service" / "application" / "use_cases" / "plan_shopping_list.py"
|
|
170
|
+
).exists()
|
|
171
|
+
|
|
172
|
+
import_script = "\n".join(
|
|
173
|
+
[
|
|
174
|
+
"from test_plan_service.domain.models.shopping_item import ShoppingItem",
|
|
175
|
+
"from test_plan_service.application.use_cases.plan_shopping_list import PlanShoppingListUseCase",
|
|
176
|
+
"print(ShoppingItem.__name__, PlanShoppingListUseCase.__name__)",
|
|
177
|
+
]
|
|
178
|
+
)
|
|
179
|
+
result = subprocess.run(
|
|
180
|
+
[
|
|
181
|
+
"uv",
|
|
182
|
+
"run",
|
|
183
|
+
"python",
|
|
184
|
+
"-c",
|
|
185
|
+
import_script,
|
|
186
|
+
],
|
|
187
|
+
cwd=project_dir,
|
|
188
|
+
capture_output=True,
|
|
189
|
+
text=True,
|
|
190
|
+
timeout=30,
|
|
191
|
+
)
|
|
192
|
+
assert result.returncode == 0, f"Core scaffold import failed:\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
|
|
193
|
+
assert "ShoppingItem PlanShoppingListUseCase" in result.stdout
|
|
194
|
+
|
|
195
|
+
# Step 7 — validate non-interactive adapter generation through the CLI entry point
|
|
140
196
|
result = subprocess.run(
|
|
141
197
|
[
|
|
142
198
|
"arclith-cli",
|
|
@@ -12,11 +12,25 @@ def test_detect_project_paths_uses_src_package(tmp_path: Path):
|
|
|
12
12
|
assert paths.package_root == package_root
|
|
13
13
|
assert paths.package_name == "my_service"
|
|
14
14
|
assert paths.domain_models == package_root / "domain" / "models"
|
|
15
|
+
assert paths.application_use_cases == package_root / "application" / "use_cases"
|
|
15
16
|
assert paths.adapters_outbound == package_root / "adapters" / "outbound"
|
|
16
17
|
assert paths.containers == package_root / "infrastructure" / "containers"
|
|
17
18
|
assert paths.import_path("domain", "models", "recipe") == "my_service.domain.models.recipe"
|
|
18
19
|
|
|
19
20
|
|
|
21
|
+
def test_detect_project_paths_uses_src_package_before_domain_exists(tmp_path: Path):
|
|
22
|
+
package_root = tmp_path / "src" / "my_service"
|
|
23
|
+
package_root.mkdir(parents=True)
|
|
24
|
+
(package_root / "__init__.py").write_text("", encoding="utf-8")
|
|
25
|
+
|
|
26
|
+
paths = detect_project_paths(tmp_path)
|
|
27
|
+
|
|
28
|
+
assert paths.package_root == package_root
|
|
29
|
+
assert paths.package_name == "my_service"
|
|
30
|
+
assert paths.domain_models == package_root / "domain" / "models"
|
|
31
|
+
assert paths.application_use_cases == package_root / "application" / "use_cases"
|
|
32
|
+
|
|
33
|
+
|
|
20
34
|
def test_detect_project_paths_supports_root_layout(tmp_path: Path):
|
|
21
35
|
(tmp_path / "domain" / "models").mkdir(parents=True)
|
|
22
36
|
|
|
@@ -24,4 +38,6 @@ def test_detect_project_paths_supports_root_layout(tmp_path: Path):
|
|
|
24
38
|
|
|
25
39
|
assert paths.package_root == tmp_path
|
|
26
40
|
assert paths.package_name is None
|
|
41
|
+
assert paths.domain_models == tmp_path / "domain" / "models"
|
|
42
|
+
assert paths.application_use_cases == tmp_path / "application" / "use_cases"
|
|
27
43
|
assert paths.import_path("domain", "models", "recipe") == "domain.models.recipe"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
from importlib.metadata import version
|
|
1
2
|
from pathlib import Path
|
|
2
3
|
|
|
3
|
-
from arclith_cli.rename import EntityNames, apply_rename
|
|
4
|
+
from arclith_cli.rename import EntityNames, _patch_arclith_dependency, apply_rename
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
def test_apply_rename_updates_src_package_and_imports(tmp_path: Path):
|
|
@@ -16,6 +17,7 @@ def test_apply_rename_updates_src_package_and_imports(tmp_path: Path):
|
|
|
16
17
|
pyproject.write_text(
|
|
17
18
|
'name = "arclith-sample"\n'
|
|
18
19
|
'packages = ["src/arclith_sample"]\n'
|
|
20
|
+
'dependencies = ["arclith[all]>=0.12.0"]\n'
|
|
19
21
|
"[tool.uv.sources]\n"
|
|
20
22
|
'arclith = { path = "../arclith", editable = true }\n',
|
|
21
23
|
encoding="utf-8",
|
|
@@ -29,4 +31,13 @@ def test_apply_rename_updates_src_package_and_imports(tmp_path: Path):
|
|
|
29
31
|
patched_pyproject = pyproject.read_text(encoding="utf-8")
|
|
30
32
|
assert 'name = "my-recipe-service"' in patched_pyproject
|
|
31
33
|
assert 'packages = ["src/my_recipe_service"]' in patched_pyproject
|
|
34
|
+
assert f'"arclith[all]>={version("arclith")}"' in patched_pyproject
|
|
32
35
|
assert "[tool.uv.sources]" not in patched_pyproject
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_patch_arclith_dependency_keeps_template_requirement_when_version_is_unknown(monkeypatch):
|
|
39
|
+
monkeypatch.setattr("arclith_cli.rename._installed_arclith_version", lambda: None)
|
|
40
|
+
|
|
41
|
+
text = 'dependencies = ["arclith[all]>=0.12.0"]\n'
|
|
42
|
+
|
|
43
|
+
assert _patch_arclith_dependency(text) == text
|
|
@@ -46,7 +46,7 @@ wheels = [
|
|
|
46
46
|
|
|
47
47
|
[[package]]
|
|
48
48
|
name = "arclith"
|
|
49
|
-
version = "0.
|
|
49
|
+
version = "0.13.0"
|
|
50
50
|
source = { editable = "../" }
|
|
51
51
|
dependencies = [
|
|
52
52
|
{ name = "fastapi" },
|
|
@@ -84,6 +84,16 @@ requires-dist = [
|
|
|
84
84
|
{ name = "loguru", specifier = "==0.7.3" },
|
|
85
85
|
{ name = "motor", marker = "extra == 'all'", specifier = "==3.7.1" },
|
|
86
86
|
{ name = "motor", marker = "extra == 'mongodb'", specifier = "==3.7.1" },
|
|
87
|
+
{ name = "opentelemetry-api", marker = "extra == 'all'", specifier = ">=1.30.0" },
|
|
88
|
+
{ name = "opentelemetry-api", marker = "extra == 'opentelemetry'", specifier = ">=1.30.0" },
|
|
89
|
+
{ name = "opentelemetry-exporter-otlp", marker = "extra == 'all'", specifier = ">=1.30.0" },
|
|
90
|
+
{ name = "opentelemetry-exporter-otlp", marker = "extra == 'opentelemetry'", specifier = ">=1.30.0" },
|
|
91
|
+
{ name = "opentelemetry-instrumentation-fastapi", marker = "extra == 'all'", specifier = ">=0.51b0" },
|
|
92
|
+
{ name = "opentelemetry-instrumentation-fastapi", marker = "extra == 'opentelemetry'", specifier = ">=0.51b0" },
|
|
93
|
+
{ name = "opentelemetry-instrumentation-logging", marker = "extra == 'all'", specifier = ">=0.51b0" },
|
|
94
|
+
{ name = "opentelemetry-instrumentation-logging", marker = "extra == 'opentelemetry'", specifier = ">=0.51b0" },
|
|
95
|
+
{ name = "opentelemetry-sdk", marker = "extra == 'all'", specifier = ">=1.30.0" },
|
|
96
|
+
{ name = "opentelemetry-sdk", marker = "extra == 'opentelemetry'", specifier = ">=1.30.0" },
|
|
87
97
|
{ name = "pydantic", specifier = "==2.12.5" },
|
|
88
98
|
{ name = "pydantic-ai-slim", extras = ["anthropic", "openai"], marker = "extra == 'all'", specifier = ">=2.24.0,<3.0.0" },
|
|
89
99
|
{ name = "pydantic-ai-slim", extras = ["anthropic", "openai"], marker = "extra == 'langgraph'", specifier = ">=2.24.0,<3.0.0" },
|
|
@@ -100,7 +110,7 @@ requires-dist = [
|
|
|
100
110
|
{ name = "uvicorn", marker = "extra == 'all'", specifier = "==0.41.0" },
|
|
101
111
|
{ name = "uvicorn", marker = "extra == 'fastapi'", specifier = "==0.41.0" },
|
|
102
112
|
]
|
|
103
|
-
provides-extras = ["mongodb", "duckdb", "mariadb", "fastapi", "mcp", "vault", "cache", "auth", "langgraph", "all"]
|
|
113
|
+
provides-extras = ["mongodb", "duckdb", "mariadb", "fastapi", "mcp", "vault", "cache", "auth", "langgraph", "opentelemetry", "all"]
|
|
104
114
|
|
|
105
115
|
[package.metadata.requires-dev]
|
|
106
116
|
dev = [
|
|
@@ -118,7 +128,7 @@ dev = [
|
|
|
118
128
|
|
|
119
129
|
[[package]]
|
|
120
130
|
name = "arclith-cli"
|
|
121
|
-
version = "0.
|
|
131
|
+
version = "0.10.0"
|
|
122
132
|
source = { editable = "." }
|
|
123
133
|
dependencies = [
|
|
124
134
|
{ name = "arclith" },
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.9.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
|