csrd-utils 0.3.2__tar.gz → 0.3.4__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.4/PKG-INFO +113 -0
- csrd_utils-0.3.4/README.md +100 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/pyproject.toml +2 -1
- csrd_utils-0.3.4/src/csrd_utils/__init__.py +7 -0
- csrd_utils-0.3.4/src/csrd_utils/__main__.py +330 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/augmentor.py +13 -0
- csrd_utils-0.3.4/src/csrd_utils/doctor.py +46 -0
- csrd_utils-0.3.4/src/csrd_utils/features/workers/settings.fragment.py +3 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/fragments.py +2 -0
- csrd_utils-0.3.4/src/csrd_utils/generator.py +107 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/settings.py +2 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/conftest.py +3 -0
- csrd_utils-0.3.2/PKG-INFO +0 -46
- csrd_utils-0.3.2/README.md +0 -34
- csrd_utils-0.3.2/src/csrd_utils/__init__.py +0 -5
- csrd_utils-0.3.2/src/csrd_utils/__main__.py +0 -55
- csrd_utils-0.3.2/src/csrd_utils/features/workers/settings.fragment.py +0 -9
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/.gitignore +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/features/workers/conftest.fragment.py +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/features/workers/docker-compose.fragment.yaml +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/features/workers/manifest.yaml +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/features/workers/requirements.fragment +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/features/workers/src/app/workers/__init__.py +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/features/workers/src/app/workers/celery_app.py +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/features/workers/src/app/workers/tasks.py +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/features/workers/tests/test_workers.py +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/resources.py +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/templates/cookiecutter-service/README.md +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/templates/cookiecutter-service/cookiecutter.json +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/templates/cookiecutter-service/hooks/post_gen_project.py +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/.dockerignore +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/.env.example +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/Dockerfile +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/README.md +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/docker-compose.yml +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/requirements.txt +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/__init__.py +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/cache.py +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/delegates/__init__.py +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/delegates/upstreams.py +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/dependencies/__init__.py +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/dependencies/delegates.py +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/dependencies/item_repository.py +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/dependencies/worker_broker.py +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/middleware/__init__.py +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/middleware/logging.py +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/repositories/__init__.py +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/repositories/item_repository.py +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/views/__init__.py +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/views/unversioned/__init__.py +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/views/unversioned/health_view.py +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/views/unversioned/items_view.py +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/workers/__init__.py +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/workers/tasks.py +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/__init__.py +0 -0
- {csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/test_items.py +0 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: csrd-utils
|
|
3
|
+
Version: 0.3.4
|
|
4
|
+
Summary: CLI utilities for csrd service generation and feature augmentation
|
|
5
|
+
Project-URL: Repository, https://github.com/csrd-api/fastapi-common
|
|
6
|
+
Project-URL: Documentation, https://github.com/csrd-api/fastapi-common/tree/main/packages/utils
|
|
7
|
+
Project-URL: Changelog, https://github.com/csrd-api/fastapi-common/blob/main/CHANGELOG.md
|
|
8
|
+
License: MIT
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Requires-Dist: cookiecutter<3,>=2.6
|
|
11
|
+
Requires-Dist: pyyaml<7,>=6
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# csrd-utils
|
|
15
|
+
|
|
16
|
+
CLI and runtime helpers for generating csrd services and augmenting existing services with optional features.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install "csrd-utils @ git+https://github.com/csrd-api/fastapi-common.git#subdirectory=packages/utils"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## CLI
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
csrd --help
|
|
28
|
+
csrd new service # interactive prompts
|
|
29
|
+
csrd new service --interactive --name items # pre-fills name prompt with 'items'
|
|
30
|
+
csrd new service --name inventory --no-interactive --output .
|
|
31
|
+
csrd feature list
|
|
32
|
+
csrd feature plan workers --service .
|
|
33
|
+
csrd feature plan workers --service . --json
|
|
34
|
+
csrd feature add workers --service .
|
|
35
|
+
csrd doctor --service .
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`feature plan --json` is useful for CI or tooling wrappers.
|
|
39
|
+
|
|
40
|
+
### Interactive generation behavior
|
|
41
|
+
|
|
42
|
+
- `csrd new service` runs interactive prompts by default.
|
|
43
|
+
- Passing `--name <value>` in interactive mode uses that value as the name prompt default.
|
|
44
|
+
- You can still overwrite the default by entering a different value at the prompt.
|
|
45
|
+
- Use `--no-interactive` for fully flag-driven generation.
|
|
46
|
+
|
|
47
|
+
## Typical workflow
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# 0) Generate a new service from bundled template
|
|
51
|
+
csrd new service --name inventory --output . --database postgres --workers
|
|
52
|
+
|
|
53
|
+
# 1) Verify service compatibility
|
|
54
|
+
csrd doctor --service ./inventory
|
|
55
|
+
|
|
56
|
+
# 2) Inspect available bundled features
|
|
57
|
+
csrd feature list
|
|
58
|
+
|
|
59
|
+
# 3) Dry-run a feature
|
|
60
|
+
csrd feature plan workers --service ./inventory
|
|
61
|
+
|
|
62
|
+
# 4) Apply feature files/merges
|
|
63
|
+
csrd feature add workers --service ./inventory
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Bundled assets
|
|
67
|
+
|
|
68
|
+
- `features/workers/` feature manifests and fragments
|
|
69
|
+
- `templates/cookiecutter-service/` template starter
|
|
70
|
+
|
|
71
|
+
## Python API
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from pathlib import Path
|
|
75
|
+
from csrd_utils import ServiceAugmentor
|
|
76
|
+
from csrd_utils.resources import features_path
|
|
77
|
+
|
|
78
|
+
with features_path() as feature_lib:
|
|
79
|
+
augmentor = ServiceAugmentor(Path("."), feature_lib)
|
|
80
|
+
ok, changes = augmentor.add_feature("workers", plan=True)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Documentation
|
|
84
|
+
|
|
85
|
+
- `docs/CLI_REFERENCE.md` - full command/flag reference
|
|
86
|
+
- `docs/FEATURES.md` - bundled feature catalog and behavior
|
|
87
|
+
- `docs/AGENT_SMOKE_TESTS.md` - copy/paste validation flow for clean workspaces
|
|
88
|
+
- `docs/COMMAND_MATRIX.yaml` - machine-readable command matrix for automation
|
|
89
|
+
- `AGENTS.md` - package-local rules for autonomous agents
|
|
90
|
+
|
|
91
|
+
## CLI-only agent mode
|
|
92
|
+
|
|
93
|
+
If an agent can only see the installed CLI (not source files), use this exploration sequence:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
csrd --help
|
|
97
|
+
csrd new --help
|
|
98
|
+
csrd feature --help
|
|
99
|
+
csrd doctor --help
|
|
100
|
+
csrd feature list
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Then run a clean smoke flow:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
mkdir -p /tmp/csrd-smoke
|
|
107
|
+
cd /tmp/csrd-smoke
|
|
108
|
+
csrd new service --name demo-svc --output . --no-interactive
|
|
109
|
+
cd demo-svc
|
|
110
|
+
csrd doctor
|
|
111
|
+
csrd feature plan workers
|
|
112
|
+
csrd feature add workers
|
|
113
|
+
```
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# csrd-utils
|
|
2
|
+
|
|
3
|
+
CLI and runtime helpers for generating csrd services and augmenting existing services with optional features.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install "csrd-utils @ git+https://github.com/csrd-api/fastapi-common.git#subdirectory=packages/utils"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## CLI
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
csrd --help
|
|
15
|
+
csrd new service # interactive prompts
|
|
16
|
+
csrd new service --interactive --name items # pre-fills name prompt with 'items'
|
|
17
|
+
csrd new service --name inventory --no-interactive --output .
|
|
18
|
+
csrd feature list
|
|
19
|
+
csrd feature plan workers --service .
|
|
20
|
+
csrd feature plan workers --service . --json
|
|
21
|
+
csrd feature add workers --service .
|
|
22
|
+
csrd doctor --service .
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`feature plan --json` is useful for CI or tooling wrappers.
|
|
26
|
+
|
|
27
|
+
### Interactive generation behavior
|
|
28
|
+
|
|
29
|
+
- `csrd new service` runs interactive prompts by default.
|
|
30
|
+
- Passing `--name <value>` in interactive mode uses that value as the name prompt default.
|
|
31
|
+
- You can still overwrite the default by entering a different value at the prompt.
|
|
32
|
+
- Use `--no-interactive` for fully flag-driven generation.
|
|
33
|
+
|
|
34
|
+
## Typical workflow
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# 0) Generate a new service from bundled template
|
|
38
|
+
csrd new service --name inventory --output . --database postgres --workers
|
|
39
|
+
|
|
40
|
+
# 1) Verify service compatibility
|
|
41
|
+
csrd doctor --service ./inventory
|
|
42
|
+
|
|
43
|
+
# 2) Inspect available bundled features
|
|
44
|
+
csrd feature list
|
|
45
|
+
|
|
46
|
+
# 3) Dry-run a feature
|
|
47
|
+
csrd feature plan workers --service ./inventory
|
|
48
|
+
|
|
49
|
+
# 4) Apply feature files/merges
|
|
50
|
+
csrd feature add workers --service ./inventory
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Bundled assets
|
|
54
|
+
|
|
55
|
+
- `features/workers/` feature manifests and fragments
|
|
56
|
+
- `templates/cookiecutter-service/` template starter
|
|
57
|
+
|
|
58
|
+
## Python API
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from pathlib import Path
|
|
62
|
+
from csrd_utils import ServiceAugmentor
|
|
63
|
+
from csrd_utils.resources import features_path
|
|
64
|
+
|
|
65
|
+
with features_path() as feature_lib:
|
|
66
|
+
augmentor = ServiceAugmentor(Path("."), feature_lib)
|
|
67
|
+
ok, changes = augmentor.add_feature("workers", plan=True)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Documentation
|
|
71
|
+
|
|
72
|
+
- `docs/CLI_REFERENCE.md` - full command/flag reference
|
|
73
|
+
- `docs/FEATURES.md` - bundled feature catalog and behavior
|
|
74
|
+
- `docs/AGENT_SMOKE_TESTS.md` - copy/paste validation flow for clean workspaces
|
|
75
|
+
- `docs/COMMAND_MATRIX.yaml` - machine-readable command matrix for automation
|
|
76
|
+
- `AGENTS.md` - package-local rules for autonomous agents
|
|
77
|
+
|
|
78
|
+
## CLI-only agent mode
|
|
79
|
+
|
|
80
|
+
If an agent can only see the installed CLI (not source files), use this exploration sequence:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
csrd --help
|
|
84
|
+
csrd new --help
|
|
85
|
+
csrd feature --help
|
|
86
|
+
csrd doctor --help
|
|
87
|
+
csrd feature list
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Then run a clean smoke flow:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
mkdir -p /tmp/csrd-smoke
|
|
94
|
+
cd /tmp/csrd-smoke
|
|
95
|
+
csrd new service --name demo-svc --output . --no-interactive
|
|
96
|
+
cd demo-svc
|
|
97
|
+
csrd doctor
|
|
98
|
+
csrd feature plan workers
|
|
99
|
+
csrd feature add workers
|
|
100
|
+
```
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "csrd-utils"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.4"
|
|
4
4
|
description = "CLI utilities for csrd service generation and feature augmentation"
|
|
5
5
|
license = { text = "MIT" }
|
|
6
6
|
requires-python = ">=3.12"
|
|
7
7
|
dependencies = [
|
|
8
|
+
"cookiecutter>=2.6,<3",
|
|
8
9
|
"PyYAML>=6,<7",
|
|
9
10
|
]
|
|
10
11
|
readme = "README.md"
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""Command-line interface for csrd-utils."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TypedDict
|
|
9
|
+
|
|
10
|
+
from .augmentor import ServiceAugmentor
|
|
11
|
+
from .doctor import run_doctor
|
|
12
|
+
from .generator import generate_service
|
|
13
|
+
from .resources import features_path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GenerateServiceArgs(TypedDict):
|
|
17
|
+
name: str
|
|
18
|
+
output_dir: Path
|
|
19
|
+
description: str
|
|
20
|
+
port: int
|
|
21
|
+
model_name: str
|
|
22
|
+
model_name_plural: str
|
|
23
|
+
has_database: bool
|
|
24
|
+
database: str
|
|
25
|
+
has_delegates: bool
|
|
26
|
+
upstreams: str
|
|
27
|
+
has_workers: bool
|
|
28
|
+
broker: str
|
|
29
|
+
has_caching: bool
|
|
30
|
+
cache_backend: str
|
|
31
|
+
has_structured_logging: bool
|
|
32
|
+
overwrite_if_exists: bool
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _prompt_text(label: str, default: str) -> str:
|
|
36
|
+
value = input(f"{label} [{default}]: ").strip()
|
|
37
|
+
return value or default
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _prompt_bool(label: str, default: bool) -> bool:
|
|
41
|
+
suffix = "Y/n" if default else "y/N"
|
|
42
|
+
value = input(f"{label} ({suffix}): ").strip().lower()
|
|
43
|
+
if not value:
|
|
44
|
+
return default
|
|
45
|
+
return value in {"y", "yes", "true", "1"}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _prompt_choice(label: str, choices: list[str], default: str) -> str:
|
|
49
|
+
rendered = ", ".join(choices)
|
|
50
|
+
while True:
|
|
51
|
+
value = input(f"{label} [{default}] ({rendered}): ").strip().lower()
|
|
52
|
+
if not value:
|
|
53
|
+
return default
|
|
54
|
+
if value in choices:
|
|
55
|
+
return value
|
|
56
|
+
print(f"ERROR: Invalid choice '{value}'. Expected one of: {rendered}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _prompt_int(label: str, default: int) -> int:
|
|
60
|
+
while True:
|
|
61
|
+
value = input(f"{label} [{default}]: ").strip()
|
|
62
|
+
if not value:
|
|
63
|
+
return default
|
|
64
|
+
try:
|
|
65
|
+
return int(value)
|
|
66
|
+
except ValueError:
|
|
67
|
+
print("ERROR: Enter a valid integer")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _collect_new_service_config(args: argparse.Namespace) -> GenerateServiceArgs:
|
|
71
|
+
explicit_interactive = getattr(args, "interactive", None)
|
|
72
|
+
interactive = (
|
|
73
|
+
bool(explicit_interactive) if explicit_interactive is not None else args.name is None
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Non-interactive mode keeps current flag-driven behavior.
|
|
77
|
+
if not interactive:
|
|
78
|
+
has_database = args.database != "none"
|
|
79
|
+
selected_database = "sqlite" if not has_database else args.database
|
|
80
|
+
return GenerateServiceArgs(
|
|
81
|
+
{
|
|
82
|
+
"name": args.name,
|
|
83
|
+
"output_dir": args.output,
|
|
84
|
+
"description": args.description,
|
|
85
|
+
"port": args.port,
|
|
86
|
+
"model_name": args.model_name,
|
|
87
|
+
"model_name_plural": args.model_name_plural,
|
|
88
|
+
"has_database": has_database,
|
|
89
|
+
"database": selected_database,
|
|
90
|
+
"has_delegates": args.delegates,
|
|
91
|
+
"upstreams": args.upstreams,
|
|
92
|
+
"has_workers": args.workers,
|
|
93
|
+
"broker": args.broker,
|
|
94
|
+
"has_caching": args.caching,
|
|
95
|
+
"cache_backend": args.cache_backend,
|
|
96
|
+
"has_structured_logging": args.structured_logging,
|
|
97
|
+
"overwrite_if_exists": args.force,
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
print("Interactive mode: press Enter to accept defaults.")
|
|
102
|
+
|
|
103
|
+
name = _prompt_text("Service name", args.name or "my-service")
|
|
104
|
+
output_dir = Path(_prompt_text("Output directory", str(args.output)))
|
|
105
|
+
description = _prompt_text("Service description", args.description)
|
|
106
|
+
port = _prompt_int("HTTP port", args.port)
|
|
107
|
+
model_name = _prompt_text("Model name", args.model_name)
|
|
108
|
+
|
|
109
|
+
plural_default = args.model_name_plural
|
|
110
|
+
if plural_default == "items" and model_name != "Item":
|
|
111
|
+
plural_default = f"{model_name.lower()}s"
|
|
112
|
+
model_name_plural = _prompt_text("Model name plural", plural_default)
|
|
113
|
+
|
|
114
|
+
database = _prompt_choice("Database", ["sqlite", "maria", "postgres", "none"], args.database)
|
|
115
|
+
has_database = database != "none"
|
|
116
|
+
selected_database = "sqlite" if not has_database else database
|
|
117
|
+
|
|
118
|
+
has_delegates = _prompt_bool("Enable delegates", args.delegates)
|
|
119
|
+
upstreams_default = args.upstreams
|
|
120
|
+
if has_delegates and not upstreams_default:
|
|
121
|
+
upstreams_default = "http://service-a:8080"
|
|
122
|
+
upstreams = (
|
|
123
|
+
_prompt_text("Delegates upstreams (comma-separated)", upstreams_default)
|
|
124
|
+
if has_delegates
|
|
125
|
+
else ""
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
has_workers = _prompt_bool("Enable workers", args.workers)
|
|
129
|
+
broker = (
|
|
130
|
+
_prompt_choice("Worker broker", ["redis", "rabbitmq"], args.broker)
|
|
131
|
+
if has_workers
|
|
132
|
+
else args.broker
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
has_caching = _prompt_bool("Enable caching", args.caching)
|
|
136
|
+
cache_backend = (
|
|
137
|
+
_prompt_choice("Cache backend", ["redis"], args.cache_backend)
|
|
138
|
+
if has_caching
|
|
139
|
+
else args.cache_backend
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
has_structured_logging = _prompt_bool("Enable structured logging", args.structured_logging)
|
|
143
|
+
overwrite_if_exists = _prompt_bool("Overwrite if target exists", args.force)
|
|
144
|
+
|
|
145
|
+
return GenerateServiceArgs(
|
|
146
|
+
{
|
|
147
|
+
"name": name,
|
|
148
|
+
"output_dir": output_dir,
|
|
149
|
+
"description": description,
|
|
150
|
+
"port": port,
|
|
151
|
+
"model_name": model_name,
|
|
152
|
+
"model_name_plural": model_name_plural,
|
|
153
|
+
"has_database": has_database,
|
|
154
|
+
"database": selected_database,
|
|
155
|
+
"has_delegates": has_delegates,
|
|
156
|
+
"upstreams": upstreams,
|
|
157
|
+
"has_workers": has_workers,
|
|
158
|
+
"broker": broker,
|
|
159
|
+
"has_caching": has_caching,
|
|
160
|
+
"cache_backend": cache_backend,
|
|
161
|
+
"has_structured_logging": has_structured_logging,
|
|
162
|
+
"overwrite_if_exists": overwrite_if_exists,
|
|
163
|
+
}
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
168
|
+
parser = argparse.ArgumentParser(prog="csrd", description="csrd utilities")
|
|
169
|
+
sub = parser.add_subparsers(dest="command")
|
|
170
|
+
|
|
171
|
+
feature = sub.add_parser("feature", help="Feature operations")
|
|
172
|
+
feature_sub = feature.add_subparsers(dest="feature_command")
|
|
173
|
+
|
|
174
|
+
list_cmd = feature_sub.add_parser("list", help="List bundled features")
|
|
175
|
+
list_cmd.set_defaults(plan=False)
|
|
176
|
+
|
|
177
|
+
for name, plan in (("add", False), ("plan", True)):
|
|
178
|
+
cmd = feature_sub.add_parser(name, help=f"{name} feature")
|
|
179
|
+
cmd.add_argument("feature", help="Feature name, e.g. workers")
|
|
180
|
+
cmd.add_argument("--service", type=Path, default=Path.cwd(), help="Service root path")
|
|
181
|
+
if name == "plan":
|
|
182
|
+
cmd.add_argument("--json", action="store_true", help="Print plan as JSON")
|
|
183
|
+
cmd.set_defaults(plan=plan)
|
|
184
|
+
|
|
185
|
+
doctor = sub.add_parser("doctor", help="Validate service compatibility")
|
|
186
|
+
doctor.add_argument("--service", type=Path, default=Path.cwd(), help="Service root path")
|
|
187
|
+
doctor.add_argument("--json", action="store_true", help="Print doctor report as JSON")
|
|
188
|
+
|
|
189
|
+
new = sub.add_parser("new", help="Generate new assets")
|
|
190
|
+
new_sub = new.add_subparsers(dest="new_command")
|
|
191
|
+
service = new_sub.add_parser("service", help="Generate a service from bundled template")
|
|
192
|
+
service.add_argument("--name", help="Service name")
|
|
193
|
+
service.add_argument("--output", type=Path, default=Path.cwd(), help="Output directory")
|
|
194
|
+
service.add_argument(
|
|
195
|
+
"--description",
|
|
196
|
+
default="A microservice using the csrd library stack",
|
|
197
|
+
help="Service description",
|
|
198
|
+
)
|
|
199
|
+
service.add_argument("--port", type=int, default=8080, help="HTTP service port")
|
|
200
|
+
service.add_argument("--model-name", default="Item", help="Primary model name")
|
|
201
|
+
service.add_argument("--model-name-plural", default="items", help="Plural model name")
|
|
202
|
+
service.add_argument(
|
|
203
|
+
"--database", choices=["sqlite", "maria", "postgres", "none"], default="sqlite"
|
|
204
|
+
)
|
|
205
|
+
service.add_argument("--delegates", action="store_true", help="Enable delegates")
|
|
206
|
+
service.add_argument("--upstreams", default="", help="Comma-separated upstream URLs")
|
|
207
|
+
service.add_argument("--workers", action="store_true", help="Enable workers")
|
|
208
|
+
service.add_argument("--broker", choices=["redis", "rabbitmq"], default="redis")
|
|
209
|
+
service.add_argument("--caching", action="store_true", help="Enable caching")
|
|
210
|
+
service.add_argument("--cache-backend", choices=["redis"], default="redis")
|
|
211
|
+
service.add_argument(
|
|
212
|
+
"--structured-logging",
|
|
213
|
+
dest="structured_logging",
|
|
214
|
+
action="store_true",
|
|
215
|
+
default=True,
|
|
216
|
+
help="Enable structured logging",
|
|
217
|
+
)
|
|
218
|
+
service.add_argument(
|
|
219
|
+
"--no-structured-logging",
|
|
220
|
+
dest="structured_logging",
|
|
221
|
+
action="store_false",
|
|
222
|
+
help="Disable structured logging",
|
|
223
|
+
)
|
|
224
|
+
service.add_argument("--interactive", dest="interactive", action="store_true", default=None)
|
|
225
|
+
service.add_argument("--no-interactive", dest="interactive", action="store_false")
|
|
226
|
+
service.add_argument("--force", action="store_true", help="Overwrite existing generated path")
|
|
227
|
+
|
|
228
|
+
return parser
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _list_features() -> int:
|
|
232
|
+
with features_path() as feature_lib:
|
|
233
|
+
names = sorted(
|
|
234
|
+
path.name
|
|
235
|
+
for path in feature_lib.iterdir()
|
|
236
|
+
if path.is_dir() and (path / "manifest.yaml").exists()
|
|
237
|
+
)
|
|
238
|
+
if not names:
|
|
239
|
+
print("No bundled features found.")
|
|
240
|
+
return 1
|
|
241
|
+
for name in names:
|
|
242
|
+
print(name)
|
|
243
|
+
return 0
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _run_doctor(service: Path, as_json: bool) -> int:
|
|
247
|
+
report = run_doctor(service)
|
|
248
|
+
payload = {
|
|
249
|
+
"ok": report.ok,
|
|
250
|
+
"errors": report.errors,
|
|
251
|
+
"warnings": report.warnings,
|
|
252
|
+
}
|
|
253
|
+
if as_json:
|
|
254
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
255
|
+
else:
|
|
256
|
+
if report.ok:
|
|
257
|
+
print("Doctor check passed.")
|
|
258
|
+
else:
|
|
259
|
+
print("Doctor check failed.")
|
|
260
|
+
for err in report.errors:
|
|
261
|
+
print(f"ERROR: {err}")
|
|
262
|
+
for warning in report.warnings:
|
|
263
|
+
print(f"WARN: {warning}")
|
|
264
|
+
return 0 if report.ok else 1
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def main() -> int:
|
|
268
|
+
parser = _build_parser()
|
|
269
|
+
args = parser.parse_args()
|
|
270
|
+
|
|
271
|
+
if args.command == "new" and args.new_command == "service":
|
|
272
|
+
try:
|
|
273
|
+
config = _collect_new_service_config(args)
|
|
274
|
+
generated = generate_service(**config)
|
|
275
|
+
except (FileExistsError, ValueError) as exc:
|
|
276
|
+
print(f"ERROR: {exc}")
|
|
277
|
+
return 1
|
|
278
|
+
except KeyboardInterrupt:
|
|
279
|
+
print("\nCancelled.")
|
|
280
|
+
return 1
|
|
281
|
+
print(f"Generated service at: {generated}")
|
|
282
|
+
return 0
|
|
283
|
+
|
|
284
|
+
if args.command == "doctor":
|
|
285
|
+
return _run_doctor(args.service, args.json)
|
|
286
|
+
|
|
287
|
+
if args.command != "feature" or args.feature_command not in {"add", "plan", "list"}:
|
|
288
|
+
parser.print_help()
|
|
289
|
+
return 0
|
|
290
|
+
|
|
291
|
+
if args.feature_command == "list":
|
|
292
|
+
return _list_features()
|
|
293
|
+
|
|
294
|
+
with features_path() as feature_lib:
|
|
295
|
+
augmentor = ServiceAugmentor(args.service, feature_lib)
|
|
296
|
+
if args.feature_command == "plan" and args.json:
|
|
297
|
+
plan = augmentor.build_plan(args.feature)
|
|
298
|
+
if plan is None:
|
|
299
|
+
return 1
|
|
300
|
+
print(
|
|
301
|
+
json.dumps(
|
|
302
|
+
{
|
|
303
|
+
"feature": args.feature,
|
|
304
|
+
"service": str(args.service.resolve()),
|
|
305
|
+
"modifies": plan["modifies"],
|
|
306
|
+
"creates": plan["creates"],
|
|
307
|
+
},
|
|
308
|
+
indent=2,
|
|
309
|
+
sort_keys=True,
|
|
310
|
+
)
|
|
311
|
+
)
|
|
312
|
+
return 0
|
|
313
|
+
success, changes = augmentor.add_feature(args.feature, plan=args.plan)
|
|
314
|
+
|
|
315
|
+
if not success:
|
|
316
|
+
return 1
|
|
317
|
+
|
|
318
|
+
if args.plan:
|
|
319
|
+
print("Plan validated.")
|
|
320
|
+
else:
|
|
321
|
+
print(f"Feature '{args.feature}' added.")
|
|
322
|
+
if changes:
|
|
323
|
+
for change in changes:
|
|
324
|
+
print(f"- {change}")
|
|
325
|
+
|
|
326
|
+
return 0
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
if __name__ == "__main__":
|
|
330
|
+
raise SystemExit(main())
|
|
@@ -387,6 +387,7 @@ class ServiceAugmentor:
|
|
|
387
387
|
tuple[bool, list[str]]
|
|
388
388
|
(success, list of changes applied)
|
|
389
389
|
"""
|
|
390
|
+
self.applied_changes = []
|
|
390
391
|
print(f"\n📦 Adding feature: {feature_name}")
|
|
391
392
|
print()
|
|
392
393
|
|
|
@@ -430,8 +431,19 @@ class ServiceAugmentor:
|
|
|
430
431
|
all_success &= self.create_feature_files(feature_name)
|
|
431
432
|
all_success &= self.create_test_files(feature_name)
|
|
432
433
|
|
|
434
|
+
if not all_success:
|
|
435
|
+
self.rollback()
|
|
433
436
|
return all_success, self.applied_changes
|
|
434
437
|
|
|
438
|
+
def build_plan(self, feature_name: str) -> dict[str, list[str]] | None:
|
|
439
|
+
"""Return a structured dry-run plan for feature application."""
|
|
440
|
+
if not self.load_feature(feature_name):
|
|
441
|
+
return None
|
|
442
|
+
return {
|
|
443
|
+
"modifies": list(self.manifest.get("modifies", [])),
|
|
444
|
+
"creates": list(self.manifest.get("creates", [])),
|
|
445
|
+
}
|
|
446
|
+
|
|
435
447
|
def rollback(self) -> None:
|
|
436
448
|
"""Restore all backups."""
|
|
437
449
|
print("\n🔄 Rolling back changes...")
|
|
@@ -439,3 +451,4 @@ class ServiceAugmentor:
|
|
|
439
451
|
shutil.copy2(backup_path, original_path)
|
|
440
452
|
Path(backup_path).unlink()
|
|
441
453
|
print(f" Restored {original_path}")
|
|
454
|
+
self.backups.clear()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Preflight checks for services targeted by csrd-utils feature augmentation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class DoctorReport:
|
|
11
|
+
"""Structured doctor output."""
|
|
12
|
+
|
|
13
|
+
ok: bool
|
|
14
|
+
errors: list[str] = field(default_factory=list)
|
|
15
|
+
warnings: list[str] = field(default_factory=list)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def run_doctor(service_root: Path) -> DoctorReport:
|
|
19
|
+
"""Validate that a service is compatible with feature augmentation."""
|
|
20
|
+
root = service_root.resolve()
|
|
21
|
+
errors: list[str] = []
|
|
22
|
+
warnings: list[str] = []
|
|
23
|
+
|
|
24
|
+
required_dirs = ["src/app", "tests"]
|
|
25
|
+
required_files = ["docker-compose.yml", "requirements.txt"]
|
|
26
|
+
|
|
27
|
+
for rel in required_dirs:
|
|
28
|
+
if not (root / rel).is_dir():
|
|
29
|
+
errors.append(f"Missing required directory: {rel}")
|
|
30
|
+
|
|
31
|
+
for rel in required_files:
|
|
32
|
+
if not (root / rel).is_file():
|
|
33
|
+
errors.append(f"Missing required file: {rel}")
|
|
34
|
+
|
|
35
|
+
marker_files = {
|
|
36
|
+
"src/app/settings.py": "# [INSERT: fields]",
|
|
37
|
+
"tests/conftest.py": "# [INSERT: fixtures]",
|
|
38
|
+
}
|
|
39
|
+
for rel, marker in marker_files.items():
|
|
40
|
+
path = root / rel
|
|
41
|
+
if path.exists():
|
|
42
|
+
text = path.read_text(encoding="utf-8")
|
|
43
|
+
if marker not in text:
|
|
44
|
+
warnings.append(f"Optional insertion marker missing in {rel}: {marker}")
|
|
45
|
+
|
|
46
|
+
return DoctorReport(ok=not errors, errors=errors, warnings=warnings)
|
|
@@ -170,6 +170,8 @@ class PythonFileMerger:
|
|
|
170
170
|
|
|
171
171
|
# Insert fragment after the marker line
|
|
172
172
|
fragment_lines = fragment_content.strip().split("\n")
|
|
173
|
+
if fragment_lines and re.match(r"^# \[INSERT:\s*\w+\]", fragment_lines[0]):
|
|
174
|
+
fragment_lines = fragment_lines[1:]
|
|
173
175
|
for line in reversed(fragment_lines):
|
|
174
176
|
lines.insert(insert_idx + 1, line)
|
|
175
177
|
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Service generation helpers backed by bundled cookiecutter templates."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from cookiecutter.main import cookiecutter
|
|
9
|
+
|
|
10
|
+
from .resources import templates_path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _build_context(
|
|
14
|
+
*,
|
|
15
|
+
name: str,
|
|
16
|
+
description: str,
|
|
17
|
+
port: int,
|
|
18
|
+
model_name: str,
|
|
19
|
+
model_name_plural: str,
|
|
20
|
+
has_database: bool,
|
|
21
|
+
database: str,
|
|
22
|
+
has_delegates: bool,
|
|
23
|
+
upstreams: str,
|
|
24
|
+
has_workers: bool,
|
|
25
|
+
broker: str,
|
|
26
|
+
has_caching: bool,
|
|
27
|
+
cache_backend: str,
|
|
28
|
+
has_structured_logging: bool,
|
|
29
|
+
) -> dict[str, Any]:
|
|
30
|
+
return {
|
|
31
|
+
"service_name": name,
|
|
32
|
+
"service_description": description,
|
|
33
|
+
"port": str(port),
|
|
34
|
+
"model_name": model_name,
|
|
35
|
+
"model_name_plural": model_name_plural,
|
|
36
|
+
"has_database": has_database,
|
|
37
|
+
"database": database,
|
|
38
|
+
"has_delegates": has_delegates,
|
|
39
|
+
"upstreams": upstreams,
|
|
40
|
+
"has_workers": has_workers,
|
|
41
|
+
"broker": broker,
|
|
42
|
+
"has_caching": has_caching,
|
|
43
|
+
"cache_backend": cache_backend,
|
|
44
|
+
"has_structured_logging": has_structured_logging,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def generate_service(
|
|
49
|
+
*,
|
|
50
|
+
name: str,
|
|
51
|
+
output_dir: Path,
|
|
52
|
+
description: str,
|
|
53
|
+
port: int,
|
|
54
|
+
model_name: str,
|
|
55
|
+
model_name_plural: str,
|
|
56
|
+
has_database: bool,
|
|
57
|
+
database: str,
|
|
58
|
+
has_delegates: bool,
|
|
59
|
+
upstreams: str,
|
|
60
|
+
has_workers: bool,
|
|
61
|
+
broker: str,
|
|
62
|
+
has_caching: bool,
|
|
63
|
+
cache_backend: str,
|
|
64
|
+
has_structured_logging: bool,
|
|
65
|
+
overwrite_if_exists: bool,
|
|
66
|
+
) -> Path:
|
|
67
|
+
"""Generate a service from the bundled cookiecutter-service template."""
|
|
68
|
+
output_root = output_dir.resolve()
|
|
69
|
+
output_root.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
target = output_root / name
|
|
71
|
+
|
|
72
|
+
if target.exists() and any(target.iterdir()) and not overwrite_if_exists:
|
|
73
|
+
raise FileExistsError(
|
|
74
|
+
f"Target directory already exists and is not empty: {target}. Use --force to overwrite."
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
if has_delegates and not upstreams.strip():
|
|
78
|
+
raise ValueError("--upstreams is required when --delegates is enabled")
|
|
79
|
+
|
|
80
|
+
context = _build_context(
|
|
81
|
+
name=name,
|
|
82
|
+
description=description,
|
|
83
|
+
port=port,
|
|
84
|
+
model_name=model_name,
|
|
85
|
+
model_name_plural=model_name_plural,
|
|
86
|
+
has_database=has_database,
|
|
87
|
+
database=database,
|
|
88
|
+
has_delegates=has_delegates,
|
|
89
|
+
upstreams=upstreams,
|
|
90
|
+
has_workers=has_workers,
|
|
91
|
+
broker=broker,
|
|
92
|
+
has_caching=has_caching,
|
|
93
|
+
cache_backend=cache_backend,
|
|
94
|
+
has_structured_logging=has_structured_logging,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
with templates_path() as template_root:
|
|
98
|
+
template = template_root / "cookiecutter-service"
|
|
99
|
+
generated = cookiecutter(
|
|
100
|
+
str(template),
|
|
101
|
+
no_input=True,
|
|
102
|
+
extra_context=context,
|
|
103
|
+
output_dir=str(output_root),
|
|
104
|
+
overwrite_if_exists=overwrite_if_exists,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return Path(generated).resolve()
|
|
@@ -4,6 +4,8 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
4
4
|
class Settings(BaseSettings):
|
|
5
5
|
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
|
|
6
6
|
|
|
7
|
+
# [INSERT: fields]
|
|
8
|
+
|
|
7
9
|
app_name: str = "{{ cookiecutter.service_name }}"
|
|
8
10
|
port: int = {{ cookiecutter.port }}
|
|
9
11
|
has_structured_logging: bool = {{ "True" if cookiecutter.has_structured_logging else "False" }}
|
csrd_utils-0.3.2/PKG-INFO
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: csrd-utils
|
|
3
|
-
Version: 0.3.2
|
|
4
|
-
Summary: CLI utilities for csrd service generation and feature augmentation
|
|
5
|
-
Project-URL: Repository, https://github.com/csrd-api/fastapi-common
|
|
6
|
-
Project-URL: Documentation, https://github.com/csrd-api/fastapi-common/tree/main/packages/utils
|
|
7
|
-
Project-URL: Changelog, https://github.com/csrd-api/fastapi-common/blob/main/CHANGELOG.md
|
|
8
|
-
License: MIT
|
|
9
|
-
Requires-Python: >=3.12
|
|
10
|
-
Requires-Dist: pyyaml<7,>=6
|
|
11
|
-
Description-Content-Type: text/markdown
|
|
12
|
-
|
|
13
|
-
# csrd-utils
|
|
14
|
-
|
|
15
|
-
CLI and runtime helpers for generating csrd services and augmenting existing services with optional features.
|
|
16
|
-
|
|
17
|
-
## Install
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
pip install "csrd-utils @ git+https://github.com/csrd-api/fastapi-common.git#subdirectory=packages/utils"
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
## CLI
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
csrd --help
|
|
27
|
-
csrd feature plan workers --service .
|
|
28
|
-
csrd feature add workers --service .
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## Bundled assets
|
|
32
|
-
|
|
33
|
-
- `features/workers/` feature manifests and fragments
|
|
34
|
-
- `templates/cookiecutter-service/` template starter
|
|
35
|
-
|
|
36
|
-
## Python API
|
|
37
|
-
|
|
38
|
-
```python
|
|
39
|
-
from pathlib import Path
|
|
40
|
-
from csrd_utils import ServiceAugmentor
|
|
41
|
-
from csrd_utils.resources import features_path
|
|
42
|
-
|
|
43
|
-
with features_path() as feature_lib:
|
|
44
|
-
augmentor = ServiceAugmentor(Path("."), feature_lib)
|
|
45
|
-
ok, changes = augmentor.add_feature("workers", plan=True)
|
|
46
|
-
```
|
csrd_utils-0.3.2/README.md
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# csrd-utils
|
|
2
|
-
|
|
3
|
-
CLI and runtime helpers for generating csrd services and augmenting existing services with optional features.
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
pip install "csrd-utils @ git+https://github.com/csrd-api/fastapi-common.git#subdirectory=packages/utils"
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## CLI
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
csrd --help
|
|
15
|
-
csrd feature plan workers --service .
|
|
16
|
-
csrd feature add workers --service .
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## Bundled assets
|
|
20
|
-
|
|
21
|
-
- `features/workers/` feature manifests and fragments
|
|
22
|
-
- `templates/cookiecutter-service/` template starter
|
|
23
|
-
|
|
24
|
-
## Python API
|
|
25
|
-
|
|
26
|
-
```python
|
|
27
|
-
from pathlib import Path
|
|
28
|
-
from csrd_utils import ServiceAugmentor
|
|
29
|
-
from csrd_utils.resources import features_path
|
|
30
|
-
|
|
31
|
-
with features_path() as feature_lib:
|
|
32
|
-
augmentor = ServiceAugmentor(Path("."), feature_lib)
|
|
33
|
-
ok, changes = augmentor.add_feature("workers", plan=True)
|
|
34
|
-
```
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
"""Command-line interface for csrd-utils."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import argparse
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
from .augmentor import ServiceAugmentor
|
|
9
|
-
from .resources import features_path
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def _build_parser() -> argparse.ArgumentParser:
|
|
13
|
-
parser = argparse.ArgumentParser(prog="csrd", description="csrd utilities")
|
|
14
|
-
sub = parser.add_subparsers(dest="command")
|
|
15
|
-
|
|
16
|
-
feature = sub.add_parser("feature", help="Feature operations")
|
|
17
|
-
feature_sub = feature.add_subparsers(dest="feature_command")
|
|
18
|
-
|
|
19
|
-
for name, plan in (("add", False), ("plan", True)):
|
|
20
|
-
cmd = feature_sub.add_parser(name, help=f"{name} feature")
|
|
21
|
-
cmd.add_argument("feature", help="Feature name, e.g. workers")
|
|
22
|
-
cmd.add_argument("--service", type=Path, default=Path.cwd(), help="Service root path")
|
|
23
|
-
cmd.set_defaults(plan=plan)
|
|
24
|
-
|
|
25
|
-
return parser
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def main() -> int:
|
|
29
|
-
parser = _build_parser()
|
|
30
|
-
args = parser.parse_args()
|
|
31
|
-
|
|
32
|
-
if args.command != "feature" or args.feature_command not in {"add", "plan"}:
|
|
33
|
-
parser.print_help()
|
|
34
|
-
return 0
|
|
35
|
-
|
|
36
|
-
with features_path() as feature_lib:
|
|
37
|
-
augmentor = ServiceAugmentor(args.service, feature_lib)
|
|
38
|
-
success, changes = augmentor.add_feature(args.feature, plan=args.plan)
|
|
39
|
-
|
|
40
|
-
if not success:
|
|
41
|
-
return 1
|
|
42
|
-
|
|
43
|
-
if args.plan:
|
|
44
|
-
print("Plan validated.")
|
|
45
|
-
else:
|
|
46
|
-
print(f"Feature '{args.feature}' added.")
|
|
47
|
-
if changes:
|
|
48
|
-
for change in changes:
|
|
49
|
-
print(f"- {change}")
|
|
50
|
-
|
|
51
|
-
return 0
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if __name__ == "__main__":
|
|
55
|
-
raise SystemExit(main())
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# [INSERT: fields]
|
|
2
|
-
celery_broker_url: str = Field( # noqa: F821
|
|
3
|
-
default="redis://localhost:6379/0",
|
|
4
|
-
description="Celery broker URL",
|
|
5
|
-
)
|
|
6
|
-
celery_result_backend: str = Field( # noqa: F821
|
|
7
|
-
default="redis://localhost:6379/0",
|
|
8
|
-
description="Celery result backend URL",
|
|
9
|
-
)
|
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/features/workers/docker-compose.fragment.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/features/workers/src/app/workers/__init__.py
RENAMED
|
File without changes
|
{csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/features/workers/src/app/workers/celery_app.py
RENAMED
|
File without changes
|
{csrd_utils-0.3.2 → csrd_utils-0.3.4}/src/csrd_utils/features/workers/src/app/workers/tasks.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.2 → csrd_utils-0.3.4}/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
|