csrd-utils 0.3.55__tar.gz → 0.3.57__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.55 → csrd_utils-0.3.57}/PKG-INFO +1 -1
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/pyproject.toml +1 -1
- csrd_utils-0.3.57/src/csrd_utils/__init__.py +14 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/__main__.py +101 -4
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/cluster.py +27 -27
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/generator.py +100 -0
- csrd_utils-0.3.57/src/csrd_utils/templates/platform-auth-service/cookiecutter.json +6 -0
- csrd_utils-0.3.57/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/.dockerignore +7 -0
- csrd_utils-0.3.57/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/Dockerfile +10 -0
- csrd_utils-0.3.57/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/README.md +28 -0
- csrd_utils-0.3.57/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/__init__.py +23 -0
- csrd_utils-0.3.57/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/dependencies/__init__.py +25 -0
- csrd_utils-0.3.57/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/repositories/__init__.py +3 -0
- csrd_utils-0.3.57/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/repositories/token_repository.py +32 -0
- csrd_utils-0.3.57/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/requirements.txt +9 -0
- csrd_utils-0.3.57/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/settings.py +20 -0
- csrd_utils-0.3.57/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/views/__init__.py +3 -0
- csrd_utils-0.3.57/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/views/unversioned/__init__.py +10 -0
- csrd_utils-0.3.57/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/views/unversioned/auth_view.py +93 -0
- csrd_utils-0.3.57/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/views/unversioned/health_view.py +8 -0
- csrd_utils-0.3.57/src/csrd_utils/templates/platform-identity-service/cookiecutter.json +5 -0
- csrd_utils-0.3.57/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/.dockerignore +7 -0
- csrd_utils-0.3.57/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/Dockerfile +10 -0
- csrd_utils-0.3.57/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/README.md +25 -0
- csrd_utils-0.3.57/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/__init__.py +23 -0
- csrd_utils-0.3.57/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/dependencies/__init__.py +25 -0
- csrd_utils-0.3.57/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/repositories/__init__.py +3 -0
- csrd_utils-0.3.57/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/repositories/user_repository.py +123 -0
- csrd_utils-0.3.57/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/requirements.txt +6 -0
- csrd_utils-0.3.57/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/settings.py +12 -0
- csrd_utils-0.3.57/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/views/__init__.py +3 -0
- csrd_utils-0.3.57/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/views/unversioned/__init__.py +10 -0
- csrd_utils-0.3.57/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/views/unversioned/health_view.py +8 -0
- csrd_utils-0.3.57/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/views/unversioned/users_view.py +63 -0
- csrd_utils-0.3.55/src/csrd_utils/__init__.py +0 -8
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/.gitignore +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/README.md +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/audit.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/augmentor.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/doctor.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/features/caching/conftest.fragment.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/features/caching/docker-compose.fragment.yaml +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/features/caching/manifest.yaml +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/features/caching/requirements.fragment +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/features/caching/settings.fragment.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/features/caching/src/app/cache.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/features/caching/tests/test_cache.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/features/logging/conftest.fragment.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/features/logging/manifest.yaml +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/features/logging/requirements.fragment +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/features/logging/settings.fragment.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/features/logging/src/app/middleware/logging.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/features/logging/tests/test_logging_middleware.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/features/metrics/conftest.fragment.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/features/metrics/docker-compose.fragment.yaml +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/features/metrics/manifest.yaml +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/features/metrics/requirements.fragment +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/features/metrics/settings.fragment.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/features/metrics/src/app/middleware/metrics.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/features/metrics/tests/test_metrics_middleware.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/features/tracing/conftest.fragment.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/features/tracing/manifest.yaml +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/features/tracing/requirements.fragment +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/features/tracing/settings.fragment.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/features/tracing/src/app/middleware/tracing.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/features/tracing/tests/test_tracing_middleware.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/features/workers/conftest.fragment.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/features/workers/docker-compose.fragment.yaml +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/features/workers/manifest.yaml +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/features/workers/requirements.fragment +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/features/workers/settings.fragment.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/features/workers/src/app/workers/__init__.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/features/workers/src/app/workers/celery_app.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/features/workers/src/app/workers/tasks.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/features/workers/tests/test_workers.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/fragments.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/resources.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/README.md +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/cookiecutter.json +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/hooks/post_gen_project.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/.cookiecutterignore +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/.dockerignore +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/Dockerfile +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/README.md +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/__init__.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/cache.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/delegates/__init__.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/delegates/upstreams.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/dependencies/__init__.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/dependencies/delegates.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/dependencies/item_repository.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/dependencies/worker_broker.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/docker-compose.yml +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/middleware/__init__.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/middleware/logging.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/repositories/__init__.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/repositories/item_repository.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/requirements.txt +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/settings.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/__init__.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/acceptance/__init__.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/acceptance/test_logging_middleware.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/conftest.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/test_items.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/unit/__init__.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/unit/test_cache.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/unit/test_database_settings.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/unit/test_delegates.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/unit/test_settings_contracts.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/unit/test_worker_broker.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/views/__init__.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/views/unversioned/__init__.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/views/unversioned/health_view.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/views/unversioned/items_view.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/workers/__init__.py +0 -0
- {csrd_utils-0.3.55 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/workers/tasks.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.57
|
|
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
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""csrd-utils: template and feature tooling for csrd services."""
|
|
2
|
+
|
|
3
|
+
from .audit import run_audit
|
|
4
|
+
from .augmentor import ServiceAugmentor
|
|
5
|
+
from .doctor import run_doctor
|
|
6
|
+
from .generator import generate_platform_service, generate_service
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"ServiceAugmentor",
|
|
10
|
+
"generate_platform_service",
|
|
11
|
+
"generate_service",
|
|
12
|
+
"run_audit",
|
|
13
|
+
"run_doctor",
|
|
14
|
+
]
|
|
@@ -22,7 +22,7 @@ from .cluster import (
|
|
|
22
22
|
normalize_cluster_spec,
|
|
23
23
|
)
|
|
24
24
|
from .doctor import run_doctor
|
|
25
|
-
from .generator import generate_service
|
|
25
|
+
from .generator import generate_platform_service, generate_service
|
|
26
26
|
from .resources import features_path
|
|
27
27
|
|
|
28
28
|
|
|
@@ -346,7 +346,9 @@ def _collect_new_cluster_config(args: argparse.Namespace) -> GenerateClusterArgs
|
|
|
346
346
|
)
|
|
347
347
|
|
|
348
348
|
print("Interactive cluster mode: press Enter to accept defaults.")
|
|
349
|
-
cluster_name =
|
|
349
|
+
cluster_name = (
|
|
350
|
+
_prompt_text("Cluster name", args.name or "my-cluster") if not args.name else args.name
|
|
351
|
+
)
|
|
350
352
|
preset = _prompt_choice("Preset", list(PRESETS.keys()), args.preset)
|
|
351
353
|
services: list[ServiceSpec] = []
|
|
352
354
|
|
|
@@ -436,6 +438,94 @@ def _create_workspace(config: GenerateWorkspaceArgs) -> Path:
|
|
|
436
438
|
return workspace_root
|
|
437
439
|
|
|
438
440
|
|
|
441
|
+
def _generate_services_from_cluster_spec(
|
|
442
|
+
spec: ClusterSpec, workspace_root: Path, force: bool = False
|
|
443
|
+
) -> list[Path]:
|
|
444
|
+
"""Scaffold source trees for every service in a cluster spec that lacks one.
|
|
445
|
+
|
|
446
|
+
Derives generate_service arguments from each ServiceSpec so the resulting
|
|
447
|
+
workspace has src/<service>/ code alongside the compose wiring.
|
|
448
|
+
"""
|
|
449
|
+
|
|
450
|
+
normalized = normalize_cluster_spec(spec)
|
|
451
|
+
services = normalized["services"]
|
|
452
|
+
port_map = {str(svc["name"]): int(svc["port"]) for svc in services}
|
|
453
|
+
|
|
454
|
+
generated: list[Path] = []
|
|
455
|
+
for service in services:
|
|
456
|
+
name = str(service["name"])
|
|
457
|
+
service_type = str(service["type"])
|
|
458
|
+
port = int(service["port"])
|
|
459
|
+
features = list(service.get("features", []))
|
|
460
|
+
delegates_list = list(service.get("delegates", []))
|
|
461
|
+
broker = str(service.get("broker", "redis"))
|
|
462
|
+
|
|
463
|
+
# Derive upstreams from delegate targets + their ports
|
|
464
|
+
upstreams = ",".join(f"http://{t}:{port_map[t]}" for t in delegates_list if t in port_map)
|
|
465
|
+
|
|
466
|
+
has_database = service_type == "db"
|
|
467
|
+
has_delegates = bool(delegates_list) or service_type == "delegate"
|
|
468
|
+
has_workers = service_type == "worker" or "workers" in features
|
|
469
|
+
has_caching = "caching" in features
|
|
470
|
+
has_structured_logging = "logging" in features
|
|
471
|
+
database = str(service.get("database", "sqlite")) if has_database else "sqlite"
|
|
472
|
+
|
|
473
|
+
src_dir = workspace_root / "src" / name.replace("-", "_")
|
|
474
|
+
if src_dir.exists() and any(src_dir.iterdir()) and not force:
|
|
475
|
+
print(f"Skipped existing service: {name}")
|
|
476
|
+
continue
|
|
477
|
+
|
|
478
|
+
try:
|
|
479
|
+
if service_type == "platform":
|
|
480
|
+
role = str(service.get("role", "custom")).strip()
|
|
481
|
+
# Build extra_context for platform templates that have additional variables
|
|
482
|
+
extra: dict[str, str] = {}
|
|
483
|
+
if role == "auth":
|
|
484
|
+
# Resolve the identity-service URL from the port map if present
|
|
485
|
+
identity_port = next(
|
|
486
|
+
(port_map[n] for n in port_map if "identity" in n),
|
|
487
|
+
8082,
|
|
488
|
+
)
|
|
489
|
+
identity_name = next(
|
|
490
|
+
(n for n in port_map if "identity" in n),
|
|
491
|
+
"identity-service",
|
|
492
|
+
)
|
|
493
|
+
extra["identity_service_url"] = f"http://{identity_name}:{identity_port}"
|
|
494
|
+
result = generate_platform_service(
|
|
495
|
+
name=name,
|
|
496
|
+
output_dir=workspace_root,
|
|
497
|
+
port=port,
|
|
498
|
+
role=role,
|
|
499
|
+
extra_context=extra if extra else None,
|
|
500
|
+
overwrite_if_exists=force,
|
|
501
|
+
)
|
|
502
|
+
else:
|
|
503
|
+
result = generate_service(
|
|
504
|
+
name=name,
|
|
505
|
+
output_dir=workspace_root,
|
|
506
|
+
description=f"{name} service",
|
|
507
|
+
port=port,
|
|
508
|
+
model_name="Item",
|
|
509
|
+
model_name_plural="items",
|
|
510
|
+
has_database=has_database,
|
|
511
|
+
database=database,
|
|
512
|
+
has_delegates=has_delegates,
|
|
513
|
+
upstreams=upstreams,
|
|
514
|
+
has_workers=has_workers,
|
|
515
|
+
broker=broker,
|
|
516
|
+
has_caching=has_caching,
|
|
517
|
+
cache_backend="redis",
|
|
518
|
+
has_structured_logging=has_structured_logging,
|
|
519
|
+
overwrite_if_exists=force,
|
|
520
|
+
)
|
|
521
|
+
generated.append(result)
|
|
522
|
+
print(f"Generated service: {name}")
|
|
523
|
+
except Exception as exc:
|
|
524
|
+
print(f"WARN: could not scaffold service '{name}': {exc}")
|
|
525
|
+
|
|
526
|
+
return generated
|
|
527
|
+
|
|
528
|
+
|
|
439
529
|
def _default_service_namespace(output_dir: Path) -> argparse.Namespace:
|
|
440
530
|
return argparse.Namespace(
|
|
441
531
|
interactive=True,
|
|
@@ -461,7 +551,7 @@ def _default_service_namespace(output_dir: Path) -> argparse.Namespace:
|
|
|
461
551
|
def _default_cluster_namespace(output_dir: Path) -> argparse.Namespace:
|
|
462
552
|
return argparse.Namespace(
|
|
463
553
|
spec=None,
|
|
464
|
-
name=
|
|
554
|
+
name=output_dir.name, # Default cluster name to workspace folder name
|
|
465
555
|
output=output_dir,
|
|
466
556
|
preset="none",
|
|
467
557
|
force=False,
|
|
@@ -1293,7 +1383,14 @@ def main() -> int:
|
|
|
1293
1383
|
cluster_config["output_dir"],
|
|
1294
1384
|
force=cluster_config["overwrite_if_exists"],
|
|
1295
1385
|
)
|
|
1296
|
-
|
|
1386
|
+
_save_workspace_cluster_spec(workspace_root, cluster_config["spec"])
|
|
1387
|
+
print(f"Generated cluster scaffold at: {generated_cluster}")
|
|
1388
|
+
scaffolded = _generate_services_from_cluster_spec(
|
|
1389
|
+
cluster_config["spec"],
|
|
1390
|
+
workspace_root,
|
|
1391
|
+
force=cluster_config["overwrite_if_exists"],
|
|
1392
|
+
)
|
|
1393
|
+
print(f"Scaffolded {len(scaffolded)} service(s) under src/.")
|
|
1297
1394
|
except (FileExistsError, FileNotFoundError, ValueError) as exc:
|
|
1298
1395
|
print(f"ERROR: {exc}")
|
|
1299
1396
|
return 1
|
|
@@ -39,33 +39,33 @@ PRESETS: dict[str, list[ServiceSpec]] = {
|
|
|
39
39
|
"requires": ["auth-service"],
|
|
40
40
|
},
|
|
41
41
|
],
|
|
42
|
-
"minimal-cluster": [
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
],
|
|
52
|
-
"event-driven-cluster": [
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
],
|
|
42
|
+
# "minimal-cluster": [
|
|
43
|
+
# {"name": "auth-service", "type": "platform", "role": "auth", "port": 8081, "requires": []},
|
|
44
|
+
# {
|
|
45
|
+
# "name": "core-db",
|
|
46
|
+
# "type": "db",
|
|
47
|
+
# "database": "sqlite",
|
|
48
|
+
# "port": 8091,
|
|
49
|
+
# "requires": ["auth-service"],
|
|
50
|
+
# },
|
|
51
|
+
# ],
|
|
52
|
+
# "event-driven-cluster": [
|
|
53
|
+
# {"name": "auth-service", "type": "platform", "role": "auth", "port": 8081, "requires": []},
|
|
54
|
+
# {
|
|
55
|
+
# "name": "events",
|
|
56
|
+
# "type": "db",
|
|
57
|
+
# "database": "postgres",
|
|
58
|
+
# "port": 8091,
|
|
59
|
+
# "requires": ["auth-service"],
|
|
60
|
+
# },
|
|
61
|
+
# {
|
|
62
|
+
# "name": "worker-consume",
|
|
63
|
+
# "type": "worker",
|
|
64
|
+
# "broker": "redis",
|
|
65
|
+
# "port": 8093,
|
|
66
|
+
# "requires": ["events", "redis"],
|
|
67
|
+
# },
|
|
68
|
+
# ],
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
|
|
@@ -244,6 +244,106 @@ def generate_service(
|
|
|
244
244
|
return generated_path
|
|
245
245
|
|
|
246
246
|
|
|
247
|
+
# ---------------------------------------------------------------------------
|
|
248
|
+
# Platform-specific service generation
|
|
249
|
+
# ---------------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
#: Maps role names to the cookiecutter template directory names bundled under templates/.
|
|
252
|
+
_PLATFORM_ROLE_TEMPLATES: dict[str, str] = {
|
|
253
|
+
"auth": "platform-auth-service",
|
|
254
|
+
"identity": "platform-identity-service",
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def generate_platform_service(
|
|
259
|
+
*,
|
|
260
|
+
name: str,
|
|
261
|
+
output_dir: Path,
|
|
262
|
+
port: int,
|
|
263
|
+
role: str,
|
|
264
|
+
extra_context: dict[str, Any] | None = None,
|
|
265
|
+
overwrite_if_exists: bool = False,
|
|
266
|
+
) -> Path:
|
|
267
|
+
"""Generate a platform service (auth/identity) from a dedicated cookiecutter template.
|
|
268
|
+
|
|
269
|
+
Unlike :func:`generate_service` this function uses a role-specific template that
|
|
270
|
+
ships purpose-built source code rather than the generic item-CRUD stub.
|
|
271
|
+
|
|
272
|
+
Parameters
|
|
273
|
+
----------
|
|
274
|
+
name:
|
|
275
|
+
Kebab-case service name (e.g. ``"auth-service"``).
|
|
276
|
+
output_dir:
|
|
277
|
+
Workspace root. The service source lands at ``output_dir/src/<name_snake>/``.
|
|
278
|
+
port:
|
|
279
|
+
Port the service will listen on.
|
|
280
|
+
role:
|
|
281
|
+
Platform role — currently ``"auth"`` or ``"identity"``.
|
|
282
|
+
extra_context:
|
|
283
|
+
Additional cookiecutter variables merged on top of the defaults.
|
|
284
|
+
overwrite_if_exists:
|
|
285
|
+
When *True*, an existing service directory is overwritten.
|
|
286
|
+
|
|
287
|
+
Returns
|
|
288
|
+
-------
|
|
289
|
+
Path
|
|
290
|
+
Absolute path to the generated service package (``output_dir/src/<name_snake>``).
|
|
291
|
+
"""
|
|
292
|
+
if role not in _PLATFORM_ROLE_TEMPLATES:
|
|
293
|
+
raise ValueError(
|
|
294
|
+
f"Unknown platform role '{role}'. Supported roles: {sorted(_PLATFORM_ROLE_TEMPLATES)}"
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
template_name = _PLATFORM_ROLE_TEMPLATES[role]
|
|
298
|
+
output_root = output_dir.resolve()
|
|
299
|
+
src_dir = output_root / "src"
|
|
300
|
+
src_dir.mkdir(parents=True, exist_ok=True)
|
|
301
|
+
|
|
302
|
+
service_name_snake = name.replace("-", "_")
|
|
303
|
+
target = src_dir / service_name_snake
|
|
304
|
+
|
|
305
|
+
if target.exists() and any(target.iterdir()) and not overwrite_if_exists:
|
|
306
|
+
raise FileExistsError(
|
|
307
|
+
f"Target directory already exists and is not empty: {target}. Use --force to overwrite."
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
context: dict[str, Any] = {
|
|
311
|
+
"service_name": name,
|
|
312
|
+
"port": str(port),
|
|
313
|
+
"__service_name_snake": service_name_snake,
|
|
314
|
+
}
|
|
315
|
+
if extra_context:
|
|
316
|
+
context.update(extra_context)
|
|
317
|
+
|
|
318
|
+
old_dontwritebytecode = os.environ.get("PYTHONDONTWRITEBYTECODE")
|
|
319
|
+
try:
|
|
320
|
+
os.environ["PYTHONDONTWRITEBYTECODE"] = "1"
|
|
321
|
+
with templates_path() as template_root:
|
|
322
|
+
template = template_root / template_name
|
|
323
|
+
generated = cookiecutter(
|
|
324
|
+
str(template),
|
|
325
|
+
no_input=True,
|
|
326
|
+
extra_context=context,
|
|
327
|
+
output_dir=str(src_dir),
|
|
328
|
+
overwrite_if_exists=overwrite_if_exists,
|
|
329
|
+
)
|
|
330
|
+
finally:
|
|
331
|
+
if old_dontwritebytecode is None:
|
|
332
|
+
os.environ.pop("PYTHONDONTWRITEBYTECODE", None)
|
|
333
|
+
else:
|
|
334
|
+
os.environ["PYTHONDONTWRITEBYTECODE"] = old_dontwritebytecode
|
|
335
|
+
|
|
336
|
+
generated_path = Path(generated).resolve()
|
|
337
|
+
for pycache_dir in generated_path.rglob("__pycache__"):
|
|
338
|
+
shutil.rmtree(pycache_dir, ignore_errors=True)
|
|
339
|
+
|
|
340
|
+
_move_docker_files_to_workspace_root(generated_path, output_root, name)
|
|
341
|
+
_write_workspace_pyproject(output_root)
|
|
342
|
+
_write_workspace_readme(output_root)
|
|
343
|
+
|
|
344
|
+
return generated_path
|
|
345
|
+
|
|
346
|
+
|
|
247
347
|
def _move_docker_files_to_workspace_root(
|
|
248
348
|
service_path: Path, workspace_root: Path, service_name: str
|
|
249
349
|
) -> None:
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# {{ cookiecutter.service_name }}
|
|
2
|
+
|
|
3
|
+
Platform auth service — issues and verifies JWT tokens.
|
|
4
|
+
|
|
5
|
+
## Endpoints
|
|
6
|
+
|
|
7
|
+
- `POST /api/token` — issue a JWT (validates credentials via identity-service)
|
|
8
|
+
- `POST /api/verify` — verify a JWT and return claims
|
|
9
|
+
- `GET /health` — liveness probe
|
|
10
|
+
|
|
11
|
+
## Configuration
|
|
12
|
+
|
|
13
|
+
| Variable | Default | Description |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| `JWT_SECRET` | `dev-secret-change-me-please-32-bytes` | Signing key |
|
|
16
|
+
| `JWT_ALGORITHM` | `HS256` | JWT algorithm |
|
|
17
|
+
| `JWT_ISSUER` | `{{ cookiecutter.service_name }}` | Token issuer |
|
|
18
|
+
| `JWT_AUDIENCE` | `cluster` | Token audience |
|
|
19
|
+
| `JWT_TTL_SECONDS` | `3600` | Token TTL |
|
|
20
|
+
| `DB_PATH` | `./{{ cookiecutter.__service_name_snake }}.db` | SQLite database path |
|
|
21
|
+
| `IDENTITY_SERVICE_URL` | `{{ cookiecutter.identity_service_url }}` | Identity service URL |
|
|
22
|
+
|
|
23
|
+
## Run
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install -r requirements.txt
|
|
27
|
+
uvicorn {{ cookiecutter.__service_name_snake }}:build_app --factory --port {{ cookiecutter.port }}
|
|
28
|
+
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from fastapi import FastAPI
|
|
2
|
+
|
|
3
|
+
from csrd.versioning import (
|
|
4
|
+
UNVERSIONED,
|
|
5
|
+
VersionedApiConfig,
|
|
6
|
+
VersionedAppComposeConfig,
|
|
7
|
+
compose_versioned_apps,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
from .views import unversioned_app
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def build_app() -> FastAPI:
|
|
14
|
+
return compose_versioned_apps(
|
|
15
|
+
version_mapping={UNVERSIONED: unversioned_app},
|
|
16
|
+
config=VersionedAppComposeConfig(
|
|
17
|
+
title="{{ cookiecutter.service_name }}",
|
|
18
|
+
api=VersionedApiConfig(prefix="/", include_actuator_endpoints=False),
|
|
19
|
+
),
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
__all__ = ("build_app",)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from collections.abc import AsyncIterator
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
from fastapi import Depends
|
|
5
|
+
|
|
6
|
+
from csrd.repository import SQLiteAdapter
|
|
7
|
+
|
|
8
|
+
from ..repositories.token_repository import TokenRepository
|
|
9
|
+
from ..settings import settings
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def token_repository_factory() -> AsyncIterator[TokenRepository]:
|
|
13
|
+
adapter = SQLiteAdapter(settings.db_path)
|
|
14
|
+
await adapter.connect()
|
|
15
|
+
repo = TokenRepository(adapter)
|
|
16
|
+
await repo._init_schema()
|
|
17
|
+
try:
|
|
18
|
+
yield repo
|
|
19
|
+
finally:
|
|
20
|
+
await adapter.close()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
TokenRepositoryDep = Annotated[TokenRepository, Depends(token_repository_factory)]
|
|
24
|
+
|
|
25
|
+
__all__ = ("TokenRepositoryDep", "token_repository_factory")
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
|
|
5
|
+
from csrd.repository import BaseRepository, DBProtocol
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TokenRepository(BaseRepository):
|
|
9
|
+
"""Repository backed by a pluggable DBProtocol adapter."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, adapter: DBProtocol) -> None:
|
|
12
|
+
super().__init__(adapter)
|
|
13
|
+
|
|
14
|
+
async def _init_schema(self) -> None:
|
|
15
|
+
await self.execute(
|
|
16
|
+
"""
|
|
17
|
+
CREATE TABLE IF NOT EXISTS issued_tokens (
|
|
18
|
+
token TEXT PRIMARY KEY,
|
|
19
|
+
subject TEXT NOT NULL,
|
|
20
|
+
issued_at TEXT NOT NULL
|
|
21
|
+
)
|
|
22
|
+
"""
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
async def record_issued_token(self, token: str, subject: str) -> None:
|
|
26
|
+
issued_at = datetime.now(UTC).isoformat()
|
|
27
|
+
await self._init_schema()
|
|
28
|
+
await self.execute(
|
|
29
|
+
"INSERT OR REPLACE INTO issued_tokens (token, subject, issued_at) "
|
|
30
|
+
"VALUES (:token, :subject, :issued_at)",
|
|
31
|
+
{"token": token, "subject": subject, "issued_at": issued_at},
|
|
32
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Settings(BaseSettings):
|
|
5
|
+
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
|
|
6
|
+
|
|
7
|
+
app_name: str = "{{ cookiecutter.service_name }}"
|
|
8
|
+
port: int = {{ cookiecutter.port }}
|
|
9
|
+
|
|
10
|
+
jwt_secret: str = "dev-secret-change-me-please-32-bytes"
|
|
11
|
+
jwt_algorithm: str = "HS256"
|
|
12
|
+
jwt_issuer: str = "{{ cookiecutter.service_name }}"
|
|
13
|
+
jwt_audience: str = "cluster"
|
|
14
|
+
jwt_ttl_seconds: int = 3600
|
|
15
|
+
|
|
16
|
+
db_path: str = "./{{ cookiecutter.__service_name_snake }}.db"
|
|
17
|
+
identity_service_url: str = "{{ cookiecutter.identity_service_url }}"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
settings = Settings()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from fastapi import FastAPI
|
|
2
|
+
|
|
3
|
+
from . import auth_view, health_view
|
|
4
|
+
|
|
5
|
+
unversioned_app = FastAPI(title="{{ cookiecutter.service_name }}")
|
|
6
|
+
|
|
7
|
+
unversioned_app.include_router(health_view.router)
|
|
8
|
+
unversioned_app.include_router(auth_view.router)
|
|
9
|
+
|
|
10
|
+
__all__ = ("unversioned_app",)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from datetime import UTC, datetime, timedelta
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
import jwt
|
|
5
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from csrd.auth import JWTAuthenticator, StaticKeyProvider
|
|
9
|
+
|
|
10
|
+
from ...dependencies import TokenRepositoryDep
|
|
11
|
+
from ...settings import settings
|
|
12
|
+
|
|
13
|
+
router = APIRouter(prefix="/api", tags=["Auth Endpoints"])
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TokenRequest(BaseModel):
|
|
17
|
+
username: str
|
|
18
|
+
password: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class VerifyRequest(BaseModel):
|
|
22
|
+
token: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _make_verifier() -> JWTAuthenticator:
|
|
26
|
+
return JWTAuthenticator(
|
|
27
|
+
key=StaticKeyProvider(settings.jwt_secret),
|
|
28
|
+
algorithms=[settings.jwt_algorithm],
|
|
29
|
+
audience=settings.jwt_audience,
|
|
30
|
+
issuer=settings.jwt_issuer,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def _validate_credentials(username: str, password: str) -> list[str]:
|
|
35
|
+
"""Validate username/password against identity-service; raises 401 on failure.
|
|
36
|
+
Returns the list of roles assigned to the user."""
|
|
37
|
+
if not settings.identity_service_url:
|
|
38
|
+
return [] # identity not configured — allow any username (fallback)
|
|
39
|
+
|
|
40
|
+
async with httpx.AsyncClient(timeout=5.0) as client:
|
|
41
|
+
try:
|
|
42
|
+
response = await client.post(
|
|
43
|
+
f"{settings.identity_service_url}/api/verify-credentials",
|
|
44
|
+
json={"username": username, "password": password},
|
|
45
|
+
)
|
|
46
|
+
except httpx.ConnectError as exc:
|
|
47
|
+
raise HTTPException(status_code=503, detail="identity service unavailable") from exc
|
|
48
|
+
|
|
49
|
+
if response.status_code == 401:
|
|
50
|
+
raise HTTPException(status_code=401, detail="invalid credentials")
|
|
51
|
+
|
|
52
|
+
if response.status_code != 200:
|
|
53
|
+
raise HTTPException(status_code=502, detail="identity service error")
|
|
54
|
+
|
|
55
|
+
data = response.json()
|
|
56
|
+
user = data.get("user", {})
|
|
57
|
+
return list(user.get("roles", []))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@router.post("/token")
|
|
61
|
+
async def issue_token(
|
|
62
|
+
payload: TokenRequest, token_repo: TokenRepositoryDep
|
|
63
|
+
) -> dict[str, str | int | list[str]]:
|
|
64
|
+
roles = await _validate_credentials(payload.username, payload.password)
|
|
65
|
+
|
|
66
|
+
now = datetime.now(UTC)
|
|
67
|
+
claims = {
|
|
68
|
+
"sub": payload.username,
|
|
69
|
+
"iss": settings.jwt_issuer,
|
|
70
|
+
"aud": settings.jwt_audience,
|
|
71
|
+
"iat": int(now.timestamp()),
|
|
72
|
+
"exp": int((now + timedelta(seconds=settings.jwt_ttl_seconds)).timestamp()),
|
|
73
|
+
"authorities": roles,
|
|
74
|
+
"roles": roles,
|
|
75
|
+
}
|
|
76
|
+
token = jwt.encode(claims, settings.jwt_secret, algorithm=settings.jwt_algorithm)
|
|
77
|
+
await token_repo.record_issued_token(token=token, subject=payload.username)
|
|
78
|
+
return {"access_token": token, "token_type": "bearer", "expires_in": settings.jwt_ttl_seconds}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@router.post("/verify")
|
|
82
|
+
async def verify_token(request: Request, payload: VerifyRequest) -> dict[str, object]:
|
|
83
|
+
verifier = _make_verifier()
|
|
84
|
+
claims = await verifier(request, payload.token)
|
|
85
|
+
return {
|
|
86
|
+
"valid": True,
|
|
87
|
+
"claims": {
|
|
88
|
+
"sub": claims.sub,
|
|
89
|
+
"user_name": claims.user_name,
|
|
90
|
+
"authorities": claims.authorities,
|
|
91
|
+
"roles": claims.authorities,
|
|
92
|
+
},
|
|
93
|
+
}
|