csrd-utils 0.3.17__tar.gz → 0.3.19__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.
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/PKG-INFO +10 -10
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/README.md +9 -9
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/pyproject.toml +1 -1
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/__main__.py +86 -25
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/augmentor.py +18 -2
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/doctor.py +8 -1
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/views/unversioned/items_view.py +44 -12
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/.gitignore +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/__init__.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/audit.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/cluster.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/caching/conftest.fragment.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/caching/docker-compose.fragment.yaml +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/caching/manifest.yaml +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/caching/requirements.fragment +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/caching/settings.fragment.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/caching/src/app/cache.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/caching/tests/test_cache.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/logging/conftest.fragment.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/logging/manifest.yaml +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/logging/requirements.fragment +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/logging/settings.fragment.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/logging/src/app/middleware/logging.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/logging/tests/test_logging_middleware.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/metrics/conftest.fragment.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/metrics/docker-compose.fragment.yaml +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/metrics/manifest.yaml +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/metrics/requirements.fragment +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/metrics/settings.fragment.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/metrics/src/app/middleware/metrics.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/metrics/tests/test_metrics_middleware.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/tracing/conftest.fragment.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/tracing/manifest.yaml +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/tracing/requirements.fragment +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/tracing/settings.fragment.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/tracing/src/app/middleware/tracing.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/tracing/tests/test_tracing_middleware.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/workers/conftest.fragment.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/workers/docker-compose.fragment.yaml +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/workers/manifest.yaml +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/workers/requirements.fragment +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/workers/settings.fragment.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/workers/src/app/workers/__init__.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/workers/src/app/workers/celery_app.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/workers/src/app/workers/tasks.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/workers/tests/test_workers.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/fragments.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/generator.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/resources.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/README.md +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/cookiecutter.json +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/hooks/post_gen_project.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/.dockerignore +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/.env.example +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/.gitignore +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/Dockerfile +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/README.md +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/docker-compose.yml +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/pyproject.toml +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/requirements.txt +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/__init__.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/cache.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/delegates/__init__.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/delegates/upstreams.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/dependencies/__init__.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/dependencies/delegates.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/dependencies/item_repository.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/dependencies/worker_broker.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/middleware/__init__.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/middleware/logging.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/py.typed +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/repositories/__init__.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/repositories/item_repository.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/settings.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/views/__init__.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/views/unversioned/__init__.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/views/unversioned/health_view.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/workers/__init__.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/workers/tasks.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/__init__.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/acceptance/test_logging_middleware.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/conftest.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/test_items.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/unit/test_cache.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/unit/test_database_settings.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/unit/test_delegates.py +0 -0
- {csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/unit/test_worker_broker.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: csrd-utils
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.19
|
|
4
4
|
Summary: CLI utilities for csrd service generation and feature augmentation
|
|
5
5
|
Project-URL: Repository, https://github.com/csrd-api/fastapi-common
|
|
6
6
|
Project-URL: Documentation, https://github.com/csrd-api/fastapi-common/tree/main/packages/utils
|
|
@@ -51,7 +51,7 @@ csrd audit --service . # explicitly audit one servi
|
|
|
51
51
|
- Non-interactive generation errors when no workspace marker is present.
|
|
52
52
|
- Workspace cluster source of truth is persisted at `.csrd/cluster.yaml`.
|
|
53
53
|
- If `.csrd/cluster.yaml` already exists, `csrd new service` appends the generated service and refreshes scaffold files (`docker-compose.yml`, `.env.example`, `README.md`, `smoke.sh`). Service source directories and running containers are never touched.
|
|
54
|
-
- If `.csrd/cluster.yaml` is missing
|
|
54
|
+
- If `.csrd/cluster.yaml` is missing, `csrd new service` bootstraps `.csrd/cluster.yaml` and writes initial cluster scaffold files.
|
|
55
55
|
- `csrd new service` runs a compact interactive flow by default (core prompts 1-6), then asks whether to show advanced options.
|
|
56
56
|
- `csrd new workspace` creates a workspace directory and prompts whether to generate `none`, `service`, or `cluster` immediately.
|
|
57
57
|
- In compact mode, advanced options are hidden by default.
|
|
@@ -70,19 +70,19 @@ csrd new workspace --name local-dev --output . --generate none
|
|
|
70
70
|
csrd new service --name inventory --output ./local-dev --database postgres --workers
|
|
71
71
|
|
|
72
72
|
# 1) Verify service compatibility
|
|
73
|
-
csrd doctor --service ./local-dev/inventory
|
|
73
|
+
csrd doctor --service ./local-dev/src/inventory
|
|
74
74
|
|
|
75
75
|
# 1b) Audit for weak/insecure defaults
|
|
76
|
-
csrd audit --service ./local-dev/inventory
|
|
76
|
+
csrd audit --service ./local-dev/src/inventory
|
|
77
77
|
|
|
78
78
|
# 2) Inspect available bundled features
|
|
79
79
|
csrd feature list
|
|
80
80
|
|
|
81
81
|
# 3) Dry-run a feature
|
|
82
|
-
csrd feature plan workers --service ./local-dev/inventory
|
|
82
|
+
csrd feature plan workers --service ./local-dev/src/inventory
|
|
83
83
|
|
|
84
84
|
# 4) Apply feature files/merges
|
|
85
|
-
csrd feature add workers --service ./local-dev/inventory
|
|
85
|
+
csrd feature add workers --service ./local-dev/src/inventory
|
|
86
86
|
|
|
87
87
|
# 5) Add a second service — workspace auto-upgrades to cluster
|
|
88
88
|
csrd new service --name pricing --output ./local-dev --database postgres
|
|
@@ -135,8 +135,8 @@ Then run a clean smoke flow:
|
|
|
135
135
|
mkdir -p /tmp/csrd-smoke
|
|
136
136
|
csrd new workspace --name ws --output /tmp/csrd-smoke --generate none
|
|
137
137
|
csrd new service --name demo-svc --output /tmp/csrd-smoke/ws --no-interactive
|
|
138
|
-
csrd doctor --service /tmp/csrd-smoke/ws/demo-svc
|
|
139
|
-
csrd audit --service /tmp/csrd-smoke/ws/demo-svc
|
|
140
|
-
csrd feature plan workers --service /tmp/csrd-smoke/ws/demo-svc
|
|
141
|
-
csrd feature add workers --service /tmp/csrd-smoke/ws/demo-svc
|
|
138
|
+
csrd doctor --service /tmp/csrd-smoke/ws/src/demo-svc
|
|
139
|
+
csrd audit --service /tmp/csrd-smoke/ws/src/demo-svc
|
|
140
|
+
csrd feature plan workers --service /tmp/csrd-smoke/ws/src/demo-svc
|
|
141
|
+
csrd feature add workers --service /tmp/csrd-smoke/ws/src/demo-svc
|
|
142
142
|
```
|
|
@@ -38,7 +38,7 @@ csrd audit --service . # explicitly audit one servi
|
|
|
38
38
|
- Non-interactive generation errors when no workspace marker is present.
|
|
39
39
|
- Workspace cluster source of truth is persisted at `.csrd/cluster.yaml`.
|
|
40
40
|
- If `.csrd/cluster.yaml` already exists, `csrd new service` appends the generated service and refreshes scaffold files (`docker-compose.yml`, `.env.example`, `README.md`, `smoke.sh`). Service source directories and running containers are never touched.
|
|
41
|
-
- If `.csrd/cluster.yaml` is missing
|
|
41
|
+
- If `.csrd/cluster.yaml` is missing, `csrd new service` bootstraps `.csrd/cluster.yaml` and writes initial cluster scaffold files.
|
|
42
42
|
- `csrd new service` runs a compact interactive flow by default (core prompts 1-6), then asks whether to show advanced options.
|
|
43
43
|
- `csrd new workspace` creates a workspace directory and prompts whether to generate `none`, `service`, or `cluster` immediately.
|
|
44
44
|
- In compact mode, advanced options are hidden by default.
|
|
@@ -57,19 +57,19 @@ csrd new workspace --name local-dev --output . --generate none
|
|
|
57
57
|
csrd new service --name inventory --output ./local-dev --database postgres --workers
|
|
58
58
|
|
|
59
59
|
# 1) Verify service compatibility
|
|
60
|
-
csrd doctor --service ./local-dev/inventory
|
|
60
|
+
csrd doctor --service ./local-dev/src/inventory
|
|
61
61
|
|
|
62
62
|
# 1b) Audit for weak/insecure defaults
|
|
63
|
-
csrd audit --service ./local-dev/inventory
|
|
63
|
+
csrd audit --service ./local-dev/src/inventory
|
|
64
64
|
|
|
65
65
|
# 2) Inspect available bundled features
|
|
66
66
|
csrd feature list
|
|
67
67
|
|
|
68
68
|
# 3) Dry-run a feature
|
|
69
|
-
csrd feature plan workers --service ./local-dev/inventory
|
|
69
|
+
csrd feature plan workers --service ./local-dev/src/inventory
|
|
70
70
|
|
|
71
71
|
# 4) Apply feature files/merges
|
|
72
|
-
csrd feature add workers --service ./local-dev/inventory
|
|
72
|
+
csrd feature add workers --service ./local-dev/src/inventory
|
|
73
73
|
|
|
74
74
|
# 5) Add a second service — workspace auto-upgrades to cluster
|
|
75
75
|
csrd new service --name pricing --output ./local-dev --database postgres
|
|
@@ -122,8 +122,8 @@ Then run a clean smoke flow:
|
|
|
122
122
|
mkdir -p /tmp/csrd-smoke
|
|
123
123
|
csrd new workspace --name ws --output /tmp/csrd-smoke --generate none
|
|
124
124
|
csrd new service --name demo-svc --output /tmp/csrd-smoke/ws --no-interactive
|
|
125
|
-
csrd doctor --service /tmp/csrd-smoke/ws/demo-svc
|
|
126
|
-
csrd audit --service /tmp/csrd-smoke/ws/demo-svc
|
|
127
|
-
csrd feature plan workers --service /tmp/csrd-smoke/ws/demo-svc
|
|
128
|
-
csrd feature add workers --service /tmp/csrd-smoke/ws/demo-svc
|
|
125
|
+
csrd doctor --service /tmp/csrd-smoke/ws/src/demo-svc
|
|
126
|
+
csrd audit --service /tmp/csrd-smoke/ws/src/demo-svc
|
|
127
|
+
csrd feature plan workers --service /tmp/csrd-smoke/ws/src/demo-svc
|
|
128
|
+
csrd feature add workers --service /tmp/csrd-smoke/ws/src/demo-svc
|
|
129
129
|
```
|
|
@@ -61,6 +61,10 @@ WORKSPACE_MARKER = ".csrd-workspace"
|
|
|
61
61
|
WORKSPACE_CLUSTER_SPEC = Path(".csrd") / "cluster.yaml"
|
|
62
62
|
|
|
63
63
|
|
|
64
|
+
def _workspace_services_dir(workspace_root: Path) -> Path:
|
|
65
|
+
return workspace_root / "src"
|
|
66
|
+
|
|
67
|
+
|
|
64
68
|
def _prompt_text(label: str, default: str) -> str:
|
|
65
69
|
value = input(f"{label} [{default}]: ").strip()
|
|
66
70
|
return value or default
|
|
@@ -485,23 +489,55 @@ def _append_service_to_workspace_cluster(
|
|
|
485
489
|
|
|
486
490
|
|
|
487
491
|
def _workspace_service_roots(workspace_root: Path) -> list[Path]:
|
|
488
|
-
roots:
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
and
|
|
497
|
-
|
|
498
|
-
|
|
492
|
+
roots: set[Path] = set()
|
|
493
|
+
|
|
494
|
+
def _collect_from(base_dir: Path, skip_names: set[str] | None = None) -> None:
|
|
495
|
+
if not base_dir.is_dir():
|
|
496
|
+
return
|
|
497
|
+
for child in base_dir.iterdir():
|
|
498
|
+
if not child.is_dir() or child.name.startswith("."):
|
|
499
|
+
continue
|
|
500
|
+
if skip_names and child.name in skip_names:
|
|
501
|
+
continue
|
|
502
|
+
if (
|
|
503
|
+
(child / "src" / "app").is_dir()
|
|
504
|
+
and (child / "tests").is_dir()
|
|
505
|
+
and (child / "requirements.txt").is_file()
|
|
506
|
+
):
|
|
507
|
+
roots.add(child.resolve())
|
|
508
|
+
|
|
509
|
+
services_root = _workspace_services_dir(workspace_root)
|
|
510
|
+
_collect_from(services_root)
|
|
511
|
+
# Legacy fallback: older workspaces stored services at workspace root.
|
|
512
|
+
_collect_from(workspace_root, skip_names={"src"})
|
|
499
513
|
return sorted(roots)
|
|
500
514
|
|
|
501
515
|
|
|
502
|
-
def
|
|
516
|
+
def _resolve_feature_service_root(service_arg: Path) -> Path:
|
|
517
|
+
candidate = service_arg.resolve()
|
|
518
|
+
if not _is_workspace(candidate):
|
|
519
|
+
return candidate
|
|
520
|
+
|
|
521
|
+
service_roots = _workspace_service_roots(candidate)
|
|
522
|
+
if len(service_roots) == 1:
|
|
523
|
+
return service_roots[0]
|
|
524
|
+
if not service_roots:
|
|
525
|
+
raise ValueError("No services found in workspace. Pass --service with a service root path.")
|
|
526
|
+
raise ValueError(
|
|
527
|
+
"Multiple services found in workspace. Pass --service with a specific service root path."
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def _workspace_root_for_service(service_root: Path) -> Path | None:
|
|
532
|
+
parent = service_root.parent
|
|
533
|
+
if parent.name == "src" and _is_workspace(parent.parent):
|
|
534
|
+
return parent.parent
|
|
535
|
+
return None
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _infer_port_from_compose(compose_path: Path, service_name: str | None = None) -> int:
|
|
503
539
|
try:
|
|
504
|
-
payload = yaml.safe_load(
|
|
540
|
+
payload = yaml.safe_load(compose_path.read_text(encoding="utf-8"))
|
|
505
541
|
except (OSError, yaml.YAMLError):
|
|
506
542
|
return 8080
|
|
507
543
|
|
|
@@ -511,10 +547,15 @@ def _infer_port_from_service_root(service_root: Path) -> int:
|
|
|
511
547
|
if not isinstance(services, dict) or not services:
|
|
512
548
|
return 8080
|
|
513
549
|
|
|
514
|
-
|
|
515
|
-
if
|
|
550
|
+
selected: object
|
|
551
|
+
if service_name and service_name in services:
|
|
552
|
+
selected = services[service_name]
|
|
553
|
+
else:
|
|
554
|
+
selected = next(iter(services.values()))
|
|
555
|
+
if not isinstance(selected, dict):
|
|
516
556
|
return 8080
|
|
517
|
-
|
|
557
|
+
|
|
558
|
+
ports = selected.get("ports")
|
|
518
559
|
if not isinstance(ports, list) or not ports:
|
|
519
560
|
return 8080
|
|
520
561
|
|
|
@@ -528,6 +569,17 @@ def _infer_port_from_service_root(service_root: Path) -> int:
|
|
|
528
569
|
return 8080
|
|
529
570
|
|
|
530
571
|
|
|
572
|
+
def _infer_port_from_service_root(service_root: Path) -> int:
|
|
573
|
+
service_compose = service_root / "docker-compose.yml"
|
|
574
|
+
if service_compose.is_file():
|
|
575
|
+
return _infer_port_from_compose(service_compose)
|
|
576
|
+
|
|
577
|
+
workspace_root = _workspace_root_for_service(service_root)
|
|
578
|
+
if workspace_root is not None:
|
|
579
|
+
return _infer_port_from_compose(workspace_root / "docker-compose.yml", service_root.name)
|
|
580
|
+
return 8080
|
|
581
|
+
|
|
582
|
+
|
|
531
583
|
def _infer_existing_workspace_service(service_root: Path) -> ServiceSpec:
|
|
532
584
|
return {
|
|
533
585
|
"name": service_root.name,
|
|
@@ -566,9 +618,6 @@ def _maybe_update_workspace_cluster(
|
|
|
566
618
|
return generate_cluster(updated_spec, workspace_root, force=True, workspace_mode=True)
|
|
567
619
|
|
|
568
620
|
service_roots = _workspace_service_roots(workspace_root)
|
|
569
|
-
if len(service_roots) < 2:
|
|
570
|
-
return None
|
|
571
|
-
|
|
572
621
|
generated_name = generated_service.name
|
|
573
622
|
services: list[ServiceSpec] = [_service_config_to_cluster_service(service_config)]
|
|
574
623
|
for service_root in service_roots:
|
|
@@ -816,12 +865,13 @@ def main() -> int:
|
|
|
816
865
|
if args.command == "new" and args.new_command == "service":
|
|
817
866
|
try:
|
|
818
867
|
service_config = _collect_new_service_config(args)
|
|
819
|
-
|
|
868
|
+
workspace_root = _ensure_workspace_for_output(
|
|
820
869
|
service_config["output_dir"], allow_prompt=_service_is_interactive(args)
|
|
821
870
|
)
|
|
871
|
+
service_config["output_dir"] = _workspace_services_dir(workspace_root)
|
|
822
872
|
generated = generate_service(**service_config)
|
|
823
873
|
generated_cluster = _maybe_update_workspace_cluster(
|
|
824
|
-
|
|
874
|
+
workspace_root, service_config, generated
|
|
825
875
|
)
|
|
826
876
|
if generated_cluster is not None:
|
|
827
877
|
print(f"Refreshed cluster scaffold at: {generated_cluster}")
|
|
@@ -864,10 +914,15 @@ def main() -> int:
|
|
|
864
914
|
generate_mode = workspace_config["generate"]
|
|
865
915
|
if generate_mode == "service":
|
|
866
916
|
service_config = _collect_new_service_config(
|
|
867
|
-
_default_service_namespace(workspace_root)
|
|
917
|
+
_default_service_namespace(_workspace_services_dir(workspace_root))
|
|
868
918
|
)
|
|
869
|
-
service_config["output_dir"] = workspace_root
|
|
919
|
+
service_config["output_dir"] = _workspace_services_dir(workspace_root)
|
|
870
920
|
generated_service = generate_service(**service_config)
|
|
921
|
+
generated_cluster = _maybe_update_workspace_cluster(
|
|
922
|
+
workspace_root, service_config, generated_service
|
|
923
|
+
)
|
|
924
|
+
if generated_cluster is not None:
|
|
925
|
+
print(f"Refreshed cluster scaffold at: {generated_cluster}")
|
|
871
926
|
print(f"Generated service at: {generated_service}")
|
|
872
927
|
elif generate_mode == "cluster":
|
|
873
928
|
cluster_config = _collect_new_cluster_config(
|
|
@@ -901,8 +956,14 @@ def main() -> int:
|
|
|
901
956
|
if args.feature_command == "list":
|
|
902
957
|
return _list_features()
|
|
903
958
|
|
|
959
|
+
try:
|
|
960
|
+
resolved_service = _resolve_feature_service_root(args.service)
|
|
961
|
+
except ValueError as exc:
|
|
962
|
+
print(f"ERROR: {exc}")
|
|
963
|
+
return 1
|
|
964
|
+
|
|
904
965
|
with features_path() as feature_lib:
|
|
905
|
-
augmentor = ServiceAugmentor(
|
|
966
|
+
augmentor = ServiceAugmentor(resolved_service, feature_lib)
|
|
906
967
|
if args.feature_command == "plan" and args.json:
|
|
907
968
|
plan = augmentor.build_plan(args.feature)
|
|
908
969
|
if plan is None:
|
|
@@ -911,7 +972,7 @@ def main() -> int:
|
|
|
911
972
|
json.dumps(
|
|
912
973
|
{
|
|
913
974
|
"feature": args.feature,
|
|
914
|
-
"service": str(
|
|
975
|
+
"service": str(resolved_service),
|
|
915
976
|
"modifies": plan["modifies"],
|
|
916
977
|
"creates": plan["creates"],
|
|
917
978
|
},
|
|
@@ -80,7 +80,7 @@ class ServiceAugmentor:
|
|
|
80
80
|
True if valid
|
|
81
81
|
"""
|
|
82
82
|
required_dirs = ["src/app", "tests"]
|
|
83
|
-
required_files = ["
|
|
83
|
+
required_files = ["requirements.txt"]
|
|
84
84
|
|
|
85
85
|
for d in required_dirs:
|
|
86
86
|
if not (self.service_root / d).is_dir():
|
|
@@ -92,8 +92,24 @@ class ServiceAugmentor:
|
|
|
92
92
|
print(f"❌ Missing required file: {f}")
|
|
93
93
|
return False
|
|
94
94
|
|
|
95
|
+
if not self._docker_compose_path().is_file():
|
|
96
|
+
print("❌ Missing required file: docker-compose.yml")
|
|
97
|
+
return False
|
|
98
|
+
|
|
95
99
|
return True
|
|
96
100
|
|
|
101
|
+
def _docker_compose_path(self) -> Path:
|
|
102
|
+
local = self.service_root / "docker-compose.yml"
|
|
103
|
+
if local.is_file():
|
|
104
|
+
return local
|
|
105
|
+
|
|
106
|
+
parent = self.service_root.parent
|
|
107
|
+
if parent.name == "src" and (parent.parent / ".csrd-workspace").is_file():
|
|
108
|
+
workspace_compose = parent.parent / "docker-compose.yml"
|
|
109
|
+
if workspace_compose.is_file():
|
|
110
|
+
return workspace_compose
|
|
111
|
+
return local
|
|
112
|
+
|
|
97
113
|
def validate_requirements(self) -> list[str]:
|
|
98
114
|
"""
|
|
99
115
|
Validate that feature requirements are met.
|
|
@@ -189,7 +205,7 @@ class ServiceAugmentor:
|
|
|
189
205
|
return True # Not required
|
|
190
206
|
|
|
191
207
|
try:
|
|
192
|
-
dc_path = self.
|
|
208
|
+
dc_path = self._docker_compose_path()
|
|
193
209
|
self.backup_file(dc_path)
|
|
194
210
|
|
|
195
211
|
# Load YAMLs
|
|
@@ -20,7 +20,11 @@ def run_doctor(service_root: Path) -> DoctorReport:
|
|
|
20
20
|
warnings: list[str] = []
|
|
21
21
|
|
|
22
22
|
required_dirs = ["src/app", "tests"]
|
|
23
|
-
required_files = ["
|
|
23
|
+
required_files = ["requirements.txt"]
|
|
24
|
+
|
|
25
|
+
compose_candidates = [root / "docker-compose.yml"]
|
|
26
|
+
if root.parent.name == "src" and (root.parent.parent / ".csrd-workspace").is_file():
|
|
27
|
+
compose_candidates.append(root.parent.parent / "docker-compose.yml")
|
|
24
28
|
|
|
25
29
|
for rel in required_dirs:
|
|
26
30
|
if not (root / rel).is_dir():
|
|
@@ -30,6 +34,9 @@ def run_doctor(service_root: Path) -> DoctorReport:
|
|
|
30
34
|
if not (root / rel).is_file():
|
|
31
35
|
errors.append(f"Missing required file: {rel}")
|
|
32
36
|
|
|
37
|
+
if not any(path.is_file() for path in compose_candidates):
|
|
38
|
+
errors.append("Missing required file: docker-compose.yml")
|
|
39
|
+
|
|
33
40
|
marker_files = {
|
|
34
41
|
"src/app/settings.py": "# [INSERT: fields]",
|
|
35
42
|
"tests/conftest.py": "# [INSERT: fixtures]",
|
|
@@ -31,6 +31,33 @@ class Update{{ cookiecutter.model_name }}Request(BaseModel):
|
|
|
31
31
|
name: str
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
class {{ cookiecutter.model_name }}Response(BaseModel):
|
|
35
|
+
id: str
|
|
36
|
+
name: str
|
|
37
|
+
created_at: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class {{ cookiecutter.model_name }}ListResponse(BaseModel):
|
|
41
|
+
{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}: list[{{ cookiecutter.model_name }}Response]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class {{ cookiecutter.model_name }}UpdateResponse(BaseModel):
|
|
45
|
+
id: str
|
|
46
|
+
name: str
|
|
47
|
+
updated: bool
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class {{ cookiecutter.model_name }}UpsertResponse(BaseModel):
|
|
51
|
+
id: str
|
|
52
|
+
name: str
|
|
53
|
+
rows_affected: int
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class {{ cookiecutter.model_name }}DeleteResponse(BaseModel):
|
|
57
|
+
id: str
|
|
58
|
+
deleted: bool
|
|
59
|
+
|
|
60
|
+
|
|
34
61
|
{%- if cookiecutter.has_delegates and not cookiecutter.has_database %}
|
|
35
62
|
async def _delegate_request(
|
|
36
63
|
method: str,
|
|
@@ -59,13 +86,13 @@ def _dummy_item(record_id: str, name: str, created_at: str) -> dict[str, str]:
|
|
|
59
86
|
{%- endif %}
|
|
60
87
|
|
|
61
88
|
|
|
62
|
-
@router.post(ITEMS_ROUTE)
|
|
89
|
+
@router.post(ITEMS_ROUTE, response_model={{ cookiecutter.model_name }}Response)
|
|
63
90
|
async def create_{{ cookiecutter.model_name | lower }}(
|
|
64
91
|
payload: Create{{ cookiecutter.model_name }}Request,
|
|
65
92
|
{%- if cookiecutter.has_database %}
|
|
66
93
|
repo: RepositoryDep,
|
|
67
94
|
{%- endif %}
|
|
68
|
-
) ->
|
|
95
|
+
) -> {{ cookiecutter.model_name }}Response:
|
|
69
96
|
{%- if cookiecutter.has_database %}
|
|
70
97
|
return await repo.create(payload.name)
|
|
71
98
|
{%- elif cookiecutter.has_delegates %}
|
|
@@ -84,12 +111,12 @@ async def create_{{ cookiecutter.model_name | lower }}(
|
|
|
84
111
|
{%- endif %}
|
|
85
112
|
|
|
86
113
|
|
|
87
|
-
@router.get(ITEMS_ROUTE)
|
|
114
|
+
@router.get(ITEMS_ROUTE, response_model={{ cookiecutter.model_name }}ListResponse)
|
|
88
115
|
async def list_{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}(
|
|
89
116
|
{%- if cookiecutter.has_database %}
|
|
90
117
|
repo: RepositoryDep,
|
|
91
118
|
{%- endif %}
|
|
92
|
-
) ->
|
|
119
|
+
) -> {{ cookiecutter.model_name }}ListResponse:
|
|
93
120
|
{%- if cookiecutter.has_database %}
|
|
94
121
|
return {"{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}": await repo.list()}
|
|
95
122
|
{%- elif cookiecutter.has_delegates %}
|
|
@@ -104,13 +131,13 @@ async def list_{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}(
|
|
|
104
131
|
{%- endif %}
|
|
105
132
|
|
|
106
133
|
|
|
107
|
-
@router.get(f"{ITEMS_ROUTE}" + "/{entity_id}")
|
|
134
|
+
@router.get(f"{ITEMS_ROUTE}" + "/{entity_id}", response_model={{ cookiecutter.model_name }}Response)
|
|
108
135
|
async def get_{{ cookiecutter.model_name | lower }}(
|
|
109
136
|
entity_id: str,
|
|
110
137
|
{%- if cookiecutter.has_database %}
|
|
111
138
|
repo: RepositoryDep,
|
|
112
139
|
{%- endif %}
|
|
113
|
-
) ->
|
|
140
|
+
) -> {{ cookiecutter.model_name }}Response:
|
|
114
141
|
{%- if cookiecutter.has_database %}
|
|
115
142
|
item = await repo.get(entity_id)
|
|
116
143
|
{%- elif cookiecutter.has_delegates %}
|
|
@@ -128,14 +155,16 @@ async def get_{{ cookiecutter.model_name | lower }}(
|
|
|
128
155
|
return item
|
|
129
156
|
|
|
130
157
|
|
|
131
|
-
@router.put(
|
|
158
|
+
@router.put(
|
|
159
|
+
f"{ITEMS_ROUTE}" + "/{entity_id}", response_model={{ cookiecutter.model_name }}UpdateResponse
|
|
160
|
+
)
|
|
132
161
|
async def update_{{ cookiecutter.model_name | lower }}(
|
|
133
162
|
entity_id: str,
|
|
134
163
|
payload: Update{{ cookiecutter.model_name }}Request,
|
|
135
164
|
{%- if cookiecutter.has_database %}
|
|
136
165
|
repo: RepositoryDep,
|
|
137
166
|
{%- endif %}
|
|
138
|
-
) ->
|
|
167
|
+
) -> {{ cookiecutter.model_name }}UpdateResponse:
|
|
139
168
|
{%- if cookiecutter.has_database %}
|
|
140
169
|
rows = await repo.update(entity_id, payload.name)
|
|
141
170
|
{%- elif cookiecutter.has_delegates %}
|
|
@@ -161,14 +190,17 @@ async def update_{{ cookiecutter.model_name | lower }}(
|
|
|
161
190
|
return {"id": entity_id, "name": payload.name, "updated": True}
|
|
162
191
|
|
|
163
192
|
|
|
164
|
-
@router.put(
|
|
193
|
+
@router.put(
|
|
194
|
+
f"{ITEMS_ROUTE}" + "/{entity_id}/upsert",
|
|
195
|
+
response_model={{ cookiecutter.model_name }}UpsertResponse,
|
|
196
|
+
)
|
|
165
197
|
async def upsert_{{ cookiecutter.model_name | lower }}(
|
|
166
198
|
entity_id: str,
|
|
167
199
|
payload: Update{{ cookiecutter.model_name }}Request,
|
|
168
200
|
{%- if cookiecutter.has_database %}
|
|
169
201
|
repo: RepositoryDep,
|
|
170
202
|
{%- endif %}
|
|
171
|
-
) ->
|
|
203
|
+
) -> {{ cookiecutter.model_name }}UpsertResponse:
|
|
172
204
|
{%- if cookiecutter.has_database %}
|
|
173
205
|
rows = await repo.upsert(entity_id, payload.name)
|
|
174
206
|
{%- elif cookiecutter.has_delegates %}
|
|
@@ -194,13 +226,13 @@ async def upsert_{{ cookiecutter.model_name | lower }}(
|
|
|
194
226
|
return {"id": entity_id, "name": payload.name, "rows_affected": rows}
|
|
195
227
|
|
|
196
228
|
|
|
197
|
-
@router.delete(f"{ITEMS_ROUTE}" + "/{entity_id}")
|
|
229
|
+
@router.delete(f"{ITEMS_ROUTE}" + "/{entity_id}", response_model={{ cookiecutter.model_name }}DeleteResponse)
|
|
198
230
|
async def delete_{{ cookiecutter.model_name | lower }}(
|
|
199
231
|
entity_id: str,
|
|
200
232
|
{%- if cookiecutter.has_database %}
|
|
201
233
|
repo: RepositoryDep,
|
|
202
234
|
{%- endif %}
|
|
203
|
-
) ->
|
|
235
|
+
) -> {{ cookiecutter.model_name }}DeleteResponse:
|
|
204
236
|
{%- if cookiecutter.has_database %}
|
|
205
237
|
rows = await repo.delete(entity_id)
|
|
206
238
|
{%- elif cookiecutter.has_delegates %}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/caching/conftest.fragment.py
RENAMED
|
File without changes
|
{csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/caching/docker-compose.fragment.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/caching/requirements.fragment
RENAMED
|
File without changes
|
{csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/caching/settings.fragment.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/logging/conftest.fragment.py
RENAMED
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/logging/requirements.fragment
RENAMED
|
File without changes
|
{csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/logging/settings.fragment.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/metrics/conftest.fragment.py
RENAMED
|
File without changes
|
{csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/metrics/docker-compose.fragment.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/metrics/requirements.fragment
RENAMED
|
File without changes
|
{csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/metrics/settings.fragment.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/tracing/conftest.fragment.py
RENAMED
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/tracing/requirements.fragment
RENAMED
|
File without changes
|
{csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/tracing/settings.fragment.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/workers/conftest.fragment.py
RENAMED
|
File without changes
|
{csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/workers/docker-compose.fragment.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/workers/requirements.fragment
RENAMED
|
File without changes
|
{csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/workers/settings.fragment.py
RENAMED
|
File without changes
|
{csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/workers/src/app/workers/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/workers/src/app/workers/tasks.py
RENAMED
|
File without changes
|
{csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/features/workers/tests/test_workers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.17 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/README.md
RENAMED
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|