csrd-utils 0.3.22__tar.gz → 0.3.24__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.22 → csrd_utils-0.3.24}/PKG-INFO +12 -1
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/README.md +11 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/pyproject.toml +1 -1
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/__main__.py +221 -1
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/README.md +4 -4
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/.gitignore +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/__init__.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/audit.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/augmentor.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/cluster.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/doctor.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/caching/conftest.fragment.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/caching/docker-compose.fragment.yaml +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/caching/manifest.yaml +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/caching/requirements.fragment +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/caching/settings.fragment.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/caching/src/app/cache.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/caching/tests/test_cache.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/logging/conftest.fragment.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/logging/manifest.yaml +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/logging/requirements.fragment +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/logging/settings.fragment.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/logging/src/app/middleware/logging.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/logging/tests/test_logging_middleware.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/metrics/conftest.fragment.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/metrics/docker-compose.fragment.yaml +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/metrics/manifest.yaml +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/metrics/requirements.fragment +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/metrics/settings.fragment.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/metrics/src/app/middleware/metrics.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/metrics/tests/test_metrics_middleware.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/tracing/conftest.fragment.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/tracing/manifest.yaml +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/tracing/requirements.fragment +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/tracing/settings.fragment.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/tracing/src/app/middleware/tracing.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/tracing/tests/test_tracing_middleware.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/workers/conftest.fragment.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/workers/docker-compose.fragment.yaml +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/workers/manifest.yaml +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/workers/requirements.fragment +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/workers/settings.fragment.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/workers/src/app/workers/__init__.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/workers/src/app/workers/celery_app.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/workers/src/app/workers/tasks.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/workers/tests/test_workers.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/fragments.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/generator.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/resources.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/cookiecutter.json +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/hooks/post_gen_project.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/.dockerignore +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/.env.example +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/.gitignore +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/Dockerfile +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/README.md +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/docker-compose.yml +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/pyproject.toml +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/requirements.txt +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/__init__.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/cache.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/delegates/__init__.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/delegates/upstreams.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/dependencies/__init__.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/dependencies/delegates.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/dependencies/item_repository.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/dependencies/worker_broker.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/middleware/__init__.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/middleware/logging.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/py.typed +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/repositories/__init__.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/repositories/item_repository.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/settings.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/views/__init__.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/views/unversioned/__init__.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/views/unversioned/health_view.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/views/unversioned/items_view.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/workers/__init__.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/workers/tasks.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/__init__.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/acceptance/test_logging_middleware.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/conftest.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/test_items.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/unit/test_cache.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/unit/test_database_settings.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/unit/test_delegates.py +0 -0
- {csrd_utils-0.3.22 → csrd_utils-0.3.24}/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.24
|
|
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
|
|
@@ -44,6 +44,16 @@ csrd audit --service . # explicitly audit one servi
|
|
|
44
44
|
|
|
45
45
|
`feature plan --json` is useful for CI or tooling wrappers.
|
|
46
46
|
|
|
47
|
+
### Workspace extraction command
|
|
48
|
+
|
|
49
|
+
Service extraction into a sibling workspace is supported:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
csrd workspace extract-service --service ./source-ws/src/pricing --target-workspace ../pricing-ws --mode copy
|
|
53
|
+
csrd workspace extract-service --service ./source-ws/src/pricing --target-workspace ../pricing-ws --mode move
|
|
54
|
+
csrd workspace extract-service --service ./source-ws/src/pricing --target-workspace ../pricing-ws --dry-run --json
|
|
55
|
+
```
|
|
56
|
+
|
|
47
57
|
### Interactive generation behavior
|
|
48
58
|
|
|
49
59
|
- `csrd new service` and `csrd new cluster` are workspace-first commands.
|
|
@@ -112,6 +122,7 @@ with features_path() as feature_lib:
|
|
|
112
122
|
- `docs/AGENT_SMOKE_TESTS.md` - copy/paste validation flow for clean workspaces
|
|
113
123
|
- `docs/COMMAND_MATRIX.yaml` - machine-readable command matrix for automation
|
|
114
124
|
- `AGENTS.md` - package-local rules for autonomous agents
|
|
125
|
+
- `../../docs/SERVICE_EXTRACTION_PLAYBOOK.md` - workspace service extraction workflow and status
|
|
115
126
|
|
|
116
127
|
## CLI-only agent mode
|
|
117
128
|
|
|
@@ -31,6 +31,16 @@ csrd audit --service . # explicitly audit one servi
|
|
|
31
31
|
|
|
32
32
|
`feature plan --json` is useful for CI or tooling wrappers.
|
|
33
33
|
|
|
34
|
+
### Workspace extraction command
|
|
35
|
+
|
|
36
|
+
Service extraction into a sibling workspace is supported:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
csrd workspace extract-service --service ./source-ws/src/pricing --target-workspace ../pricing-ws --mode copy
|
|
40
|
+
csrd workspace extract-service --service ./source-ws/src/pricing --target-workspace ../pricing-ws --mode move
|
|
41
|
+
csrd workspace extract-service --service ./source-ws/src/pricing --target-workspace ../pricing-ws --dry-run --json
|
|
42
|
+
```
|
|
43
|
+
|
|
34
44
|
### Interactive generation behavior
|
|
35
45
|
|
|
36
46
|
- `csrd new service` and `csrd new cluster` are workspace-first commands.
|
|
@@ -99,6 +109,7 @@ with features_path() as feature_lib:
|
|
|
99
109
|
- `docs/AGENT_SMOKE_TESTS.md` - copy/paste validation flow for clean workspaces
|
|
100
110
|
- `docs/COMMAND_MATRIX.yaml` - machine-readable command matrix for automation
|
|
101
111
|
- `AGENTS.md` - package-local rules for autonomous agents
|
|
112
|
+
- `../../docs/SERVICE_EXTRACTION_PLAYBOOK.md` - workspace service extraction workflow and status
|
|
102
113
|
|
|
103
114
|
## CLI-only agent mode
|
|
104
115
|
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
import json
|
|
5
|
+
import shutil
|
|
5
6
|
from contextlib import suppress
|
|
6
7
|
from importlib.metadata import version
|
|
7
8
|
from pathlib import Path
|
|
8
|
-
from typing import TypedDict
|
|
9
|
+
from typing import TypedDict, cast
|
|
9
10
|
from urllib.parse import urlparse
|
|
10
11
|
|
|
11
12
|
import yaml # type: ignore[import-untyped]
|
|
@@ -57,6 +58,15 @@ class GenerateWorkspaceArgs(TypedDict):
|
|
|
57
58
|
generate: str
|
|
58
59
|
|
|
59
60
|
|
|
61
|
+
class ExtractWorkspaceServiceArgs(TypedDict):
|
|
62
|
+
service_root: Path
|
|
63
|
+
target_workspace: Path
|
|
64
|
+
mode: str
|
|
65
|
+
force: bool
|
|
66
|
+
dry_run: bool
|
|
67
|
+
as_json: bool
|
|
68
|
+
|
|
69
|
+
|
|
60
70
|
WORKSPACE_MARKER = ".csrd-workspace"
|
|
61
71
|
WORKSPACE_CLUSTER_SPEC = Path(".csrd") / "cluster.yaml"
|
|
62
72
|
|
|
@@ -545,9 +555,179 @@ def _workspace_root_for_service(service_root: Path) -> Path | None:
|
|
|
545
555
|
parent = service_root.parent
|
|
546
556
|
if parent.name == "src" and _is_workspace(parent.parent):
|
|
547
557
|
return parent.parent
|
|
558
|
+
if _is_workspace(parent):
|
|
559
|
+
return parent
|
|
548
560
|
return None
|
|
549
561
|
|
|
550
562
|
|
|
563
|
+
def _validate_service_root(service_root: Path) -> None:
|
|
564
|
+
if not service_root.is_dir():
|
|
565
|
+
raise ValueError(f"Service path is not a directory: {service_root}")
|
|
566
|
+
if not (service_root / "src" / "app").is_dir():
|
|
567
|
+
raise ValueError(f"Missing required directory: {service_root / 'src' / 'app'}")
|
|
568
|
+
if not (service_root / "tests").is_dir():
|
|
569
|
+
raise ValueError(f"Missing required directory: {service_root / 'tests'}")
|
|
570
|
+
if not (service_root / "requirements.txt").is_file():
|
|
571
|
+
raise ValueError(f"Missing required file: {service_root / 'requirements.txt'}")
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def _collect_workspace_extract_config(args: argparse.Namespace) -> ExtractWorkspaceServiceArgs:
|
|
575
|
+
return ExtractWorkspaceServiceArgs(
|
|
576
|
+
{
|
|
577
|
+
"service_root": args.service.resolve(),
|
|
578
|
+
"target_workspace": args.target_workspace.resolve(),
|
|
579
|
+
"mode": args.mode,
|
|
580
|
+
"force": args.force,
|
|
581
|
+
"dry_run": args.dry_run,
|
|
582
|
+
"as_json": args.json,
|
|
583
|
+
}
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def _service_spec_from_workspace(service_root: Path, workspace_root: Path) -> ServiceSpec:
|
|
588
|
+
spec = _load_workspace_cluster_spec(workspace_root)
|
|
589
|
+
services = spec.get("services", [])
|
|
590
|
+
for service in services:
|
|
591
|
+
if str(service.get("name", "")).strip() == service_root.name:
|
|
592
|
+
return cast(ServiceSpec, dict(service))
|
|
593
|
+
return _infer_existing_workspace_service(service_root)
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def _upsert_workspace_service(
|
|
597
|
+
spec: ClusterSpec, service: ServiceSpec, workspace_name: str
|
|
598
|
+
) -> ClusterSpec:
|
|
599
|
+
services: list[ServiceSpec] = [
|
|
600
|
+
cast(ServiceSpec, dict(entry))
|
|
601
|
+
for entry in spec.get("services", [])
|
|
602
|
+
if str(entry.get("name", "")).strip() != str(service["name"])
|
|
603
|
+
]
|
|
604
|
+
services.append(cast(ServiceSpec, dict(service)))
|
|
605
|
+
_ensure_unique_ports(services, preserve_name=str(service["name"]))
|
|
606
|
+
|
|
607
|
+
cluster = spec.get("cluster", {})
|
|
608
|
+
candidate: ClusterSpec = {
|
|
609
|
+
"cluster": {
|
|
610
|
+
"name": str(cluster.get("name", "")).strip() or workspace_name,
|
|
611
|
+
"preset": str(cluster.get("preset", "none")).strip() or "none",
|
|
612
|
+
},
|
|
613
|
+
"services": services,
|
|
614
|
+
}
|
|
615
|
+
return normalize_cluster_spec(candidate)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def _remove_workspace_service(
|
|
619
|
+
spec: ClusterSpec, service_name: str, workspace_name: str
|
|
620
|
+
) -> ClusterSpec:
|
|
621
|
+
services: list[ServiceSpec] = [
|
|
622
|
+
cast(ServiceSpec, dict(entry))
|
|
623
|
+
for entry in spec.get("services", [])
|
|
624
|
+
if str(entry.get("name", "")).strip() != service_name
|
|
625
|
+
]
|
|
626
|
+
cluster = spec.get("cluster", {})
|
|
627
|
+
candidate: ClusterSpec = {
|
|
628
|
+
"cluster": {
|
|
629
|
+
"name": str(cluster.get("name", "")).strip() or workspace_name,
|
|
630
|
+
"preset": str(cluster.get("preset", "none")).strip() or "none",
|
|
631
|
+
},
|
|
632
|
+
"services": services,
|
|
633
|
+
}
|
|
634
|
+
return normalize_cluster_spec(candidate)
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def _extract_service(config: ExtractWorkspaceServiceArgs) -> dict[str, object]:
|
|
638
|
+
service_root = config["service_root"]
|
|
639
|
+
target_workspace = config["target_workspace"]
|
|
640
|
+
mode = config["mode"]
|
|
641
|
+
|
|
642
|
+
_validate_service_root(service_root)
|
|
643
|
+
source_workspace = _workspace_root_for_service(service_root)
|
|
644
|
+
if source_workspace is None:
|
|
645
|
+
raise ValueError("Service is not inside a csrd workspace")
|
|
646
|
+
if source_workspace == target_workspace:
|
|
647
|
+
raise ValueError("Source and target workspace must be different paths")
|
|
648
|
+
|
|
649
|
+
source_service_spec = _service_spec_from_workspace(service_root, source_workspace)
|
|
650
|
+
target_services_dir = _workspace_services_dir(target_workspace)
|
|
651
|
+
target_service_root = target_services_dir / service_root.name
|
|
652
|
+
|
|
653
|
+
actions = [
|
|
654
|
+
f"source_workspace={source_workspace}",
|
|
655
|
+
f"target_workspace={target_workspace}",
|
|
656
|
+
f"service={service_root.name}",
|
|
657
|
+
f"mode={mode}",
|
|
658
|
+
]
|
|
659
|
+
|
|
660
|
+
if not _is_workspace(target_workspace):
|
|
661
|
+
if target_workspace.exists() and any(target_workspace.iterdir()):
|
|
662
|
+
raise ValueError(
|
|
663
|
+
f"Target exists but is not a workspace: {target_workspace}. "
|
|
664
|
+
"Create a workspace marker first or use an empty directory."
|
|
665
|
+
)
|
|
666
|
+
actions.append("create_target_workspace_marker")
|
|
667
|
+
|
|
668
|
+
if target_service_root.exists() and not config["force"]:
|
|
669
|
+
raise FileExistsError(
|
|
670
|
+
f"Target service already exists: {target_service_root}. Use --force to overwrite."
|
|
671
|
+
)
|
|
672
|
+
if target_service_root.exists():
|
|
673
|
+
actions.append("overwrite_target_service")
|
|
674
|
+
|
|
675
|
+
if mode == "move":
|
|
676
|
+
actions.append("remove_source_service")
|
|
677
|
+
|
|
678
|
+
if config["dry_run"]:
|
|
679
|
+
return {
|
|
680
|
+
"ok": True,
|
|
681
|
+
"dry_run": True,
|
|
682
|
+
"source_workspace": str(source_workspace),
|
|
683
|
+
"target_workspace": str(target_workspace),
|
|
684
|
+
"service": service_root.name,
|
|
685
|
+
"mode": mode,
|
|
686
|
+
"actions": actions,
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
target_workspace.mkdir(parents=True, exist_ok=True)
|
|
690
|
+
if not _is_workspace(target_workspace):
|
|
691
|
+
_write_workspace_marker(target_workspace)
|
|
692
|
+
|
|
693
|
+
target_services_dir.mkdir(parents=True, exist_ok=True)
|
|
694
|
+
if target_service_root.exists():
|
|
695
|
+
shutil.rmtree(target_service_root)
|
|
696
|
+
shutil.copytree(service_root, target_service_root)
|
|
697
|
+
|
|
698
|
+
target_spec = _upsert_workspace_service(
|
|
699
|
+
_load_workspace_cluster_spec(target_workspace), source_service_spec, target_workspace.name
|
|
700
|
+
)
|
|
701
|
+
_save_workspace_cluster_spec(target_workspace, target_spec)
|
|
702
|
+
target_cluster_root = generate_cluster(
|
|
703
|
+
target_spec, target_workspace, force=True, workspace_mode=True
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
source_cluster_root = None
|
|
707
|
+
if mode == "move":
|
|
708
|
+
shutil.rmtree(service_root)
|
|
709
|
+
source_spec = _remove_workspace_service(
|
|
710
|
+
_load_workspace_cluster_spec(source_workspace), service_root.name, source_workspace.name
|
|
711
|
+
)
|
|
712
|
+
_save_workspace_cluster_spec(source_workspace, source_spec)
|
|
713
|
+
source_cluster_root = generate_cluster(
|
|
714
|
+
source_spec, source_workspace, force=True, workspace_mode=True
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
return {
|
|
718
|
+
"ok": True,
|
|
719
|
+
"dry_run": False,
|
|
720
|
+
"source_workspace": str(source_workspace),
|
|
721
|
+
"target_workspace": str(target_workspace),
|
|
722
|
+
"service": service_root.name,
|
|
723
|
+
"mode": mode,
|
|
724
|
+
"target_service": str(target_service_root),
|
|
725
|
+
"target_cluster": str(target_cluster_root),
|
|
726
|
+
"source_cluster": str(source_cluster_root) if source_cluster_root else None,
|
|
727
|
+
"actions": actions,
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
|
|
551
731
|
def _infer_port_from_compose(compose_path: Path, service_name: str | None = None) -> int:
|
|
552
732
|
try:
|
|
553
733
|
payload = yaml.safe_load(compose_path.read_text(encoding="utf-8"))
|
|
@@ -752,6 +932,21 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
752
932
|
)
|
|
753
933
|
workspace.add_argument("--force", action="store_true", help="Overwrite existing workspace path")
|
|
754
934
|
|
|
935
|
+
workspace_cmd = sub.add_parser("workspace", help="Workspace operations")
|
|
936
|
+
workspace_sub = workspace_cmd.add_subparsers(dest="workspace_command", required=True)
|
|
937
|
+
|
|
938
|
+
extract = workspace_sub.add_parser(
|
|
939
|
+
"extract-service", help="Copy or move a service into a sibling workspace"
|
|
940
|
+
)
|
|
941
|
+
extract.add_argument("--service", type=Path, required=True, help="Source service root path")
|
|
942
|
+
extract.add_argument(
|
|
943
|
+
"--target-workspace", type=Path, required=True, help="Target workspace root path"
|
|
944
|
+
)
|
|
945
|
+
extract.add_argument("--mode", choices=["copy", "move"], default="copy")
|
|
946
|
+
extract.add_argument("--dry-run", action="store_true", help="Show extraction plan only")
|
|
947
|
+
extract.add_argument("--json", action="store_true", help="Print extraction result as JSON")
|
|
948
|
+
extract.add_argument("--force", action="store_true", help="Overwrite target service path")
|
|
949
|
+
|
|
755
950
|
return parser
|
|
756
951
|
|
|
757
952
|
|
|
@@ -960,6 +1155,31 @@ def main() -> int:
|
|
|
960
1155
|
return 1
|
|
961
1156
|
return 0
|
|
962
1157
|
|
|
1158
|
+
if args.command == "workspace" and args.workspace_command == "extract-service":
|
|
1159
|
+
try:
|
|
1160
|
+
result = _extract_service(_collect_workspace_extract_config(args))
|
|
1161
|
+
except (FileExistsError, ValueError) as exc:
|
|
1162
|
+
print(f"ERROR: {exc}")
|
|
1163
|
+
return 1
|
|
1164
|
+
|
|
1165
|
+
if args.json:
|
|
1166
|
+
print(json.dumps(result, indent=2, sort_keys=True))
|
|
1167
|
+
return 0
|
|
1168
|
+
|
|
1169
|
+
if result.get("dry_run"):
|
|
1170
|
+
print("Extraction plan (dry-run):")
|
|
1171
|
+
actions = result.get("actions")
|
|
1172
|
+
if isinstance(actions, list):
|
|
1173
|
+
for action in actions:
|
|
1174
|
+
print(f"- {action}")
|
|
1175
|
+
return 0
|
|
1176
|
+
|
|
1177
|
+
print(f"Extracted service to: {result['target_service']}")
|
|
1178
|
+
print(f"Refreshed target cluster scaffold at: {result['target_cluster']}")
|
|
1179
|
+
if result.get("source_cluster"):
|
|
1180
|
+
print(f"Refreshed source cluster scaffold at: {result['source_cluster']}")
|
|
1181
|
+
return 0
|
|
1182
|
+
|
|
963
1183
|
if args.command == "doctor":
|
|
964
1184
|
return _run_doctor(args.service, args.json)
|
|
965
1185
|
|
{csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/templates/cookiecutter-service/README.md
RENAMED
|
@@ -5,7 +5,7 @@ Generate a FastAPI service with composable feature toggles (database, delegates,
|
|
|
5
5
|
## Generate a service
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
uvx cookiecutter . --directory
|
|
8
|
+
uvx cookiecutter . --directory packages/utils/src/csrd_utils/templates/cookiecutter-service
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
Key prompts:
|
|
@@ -31,18 +31,18 @@ Key prompts:
|
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
33
|
# DB-only SQLite service
|
|
34
|
-
uvx cookiecutter . --directory
|
|
34
|
+
uvx cookiecutter . --directory packages/utils/src/csrd_utils/templates/cookiecutter-service \
|
|
35
35
|
--no-input service_name=inventory has_database=true database=sqlite \
|
|
36
36
|
has_delegates=false has_workers=false has_caching=false
|
|
37
37
|
|
|
38
38
|
# Postgres + delegates + caching
|
|
39
|
-
uvx cookiecutter . --directory
|
|
39
|
+
uvx cookiecutter . --directory packages/utils/src/csrd_utils/templates/cookiecutter-service \
|
|
40
40
|
--no-input service_name=pricing has_database=true database=postgres \
|
|
41
41
|
has_delegates=true upstreams=http://inventory:8081 \
|
|
42
42
|
has_workers=false has_caching=true
|
|
43
43
|
|
|
44
44
|
# Worker-focused service with RabbitMQ and no DB
|
|
45
|
-
uvx cookiecutter . --directory
|
|
45
|
+
uvx cookiecutter . --directory packages/utils/src/csrd_utils/templates/cookiecutter-service \
|
|
46
46
|
--no-input service_name=jobs has_database=false has_workers=true broker=rabbitmq \
|
|
47
47
|
has_delegates=false has_caching=false
|
|
48
48
|
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/caching/conftest.fragment.py
RENAMED
|
File without changes
|
{csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/caching/docker-compose.fragment.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/caching/requirements.fragment
RENAMED
|
File without changes
|
{csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/caching/settings.fragment.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/logging/conftest.fragment.py
RENAMED
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/logging/requirements.fragment
RENAMED
|
File without changes
|
{csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/logging/settings.fragment.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/metrics/conftest.fragment.py
RENAMED
|
File without changes
|
{csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/metrics/docker-compose.fragment.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/metrics/requirements.fragment
RENAMED
|
File without changes
|
{csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/metrics/settings.fragment.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/tracing/conftest.fragment.py
RENAMED
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/tracing/requirements.fragment
RENAMED
|
File without changes
|
{csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/tracing/settings.fragment.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/workers/conftest.fragment.py
RENAMED
|
File without changes
|
{csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/workers/docker-compose.fragment.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/workers/requirements.fragment
RENAMED
|
File without changes
|
{csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/workers/settings.fragment.py
RENAMED
|
File without changes
|
{csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/workers/src/app/workers/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/workers/src/app/workers/tasks.py
RENAMED
|
File without changes
|
{csrd_utils-0.3.22 → csrd_utils-0.3.24}/src/csrd_utils/features/workers/tests/test_workers.py
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|