csrd-utils 0.3.56__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.56 → csrd_utils-0.3.57}/PKG-INFO +1 -1
- {csrd_utils-0.3.56 → 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.56 → csrd_utils-0.3.57}/src/csrd_utils/__main__.py +43 -19
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/cluster.py +27 -27
- {csrd_utils-0.3.56 → 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.56/src/csrd_utils/__init__.py +0 -8
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/.gitignore +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/README.md +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/audit.py +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/augmentor.py +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/doctor.py +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/caching/conftest.fragment.py +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/caching/docker-compose.fragment.yaml +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/caching/manifest.yaml +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/caching/requirements.fragment +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/caching/settings.fragment.py +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/caching/src/app/cache.py +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/caching/tests/test_cache.py +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/logging/conftest.fragment.py +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/logging/manifest.yaml +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/logging/requirements.fragment +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/logging/settings.fragment.py +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/logging/src/app/middleware/logging.py +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/logging/tests/test_logging_middleware.py +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/metrics/conftest.fragment.py +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/metrics/docker-compose.fragment.yaml +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/metrics/manifest.yaml +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/metrics/requirements.fragment +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/metrics/settings.fragment.py +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/metrics/src/app/middleware/metrics.py +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/metrics/tests/test_metrics_middleware.py +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/tracing/conftest.fragment.py +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/tracing/manifest.yaml +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/tracing/requirements.fragment +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/tracing/settings.fragment.py +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/tracing/src/app/middleware/tracing.py +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/tracing/tests/test_tracing_middleware.py +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/workers/conftest.fragment.py +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/workers/docker-compose.fragment.yaml +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/workers/manifest.yaml +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/workers/requirements.fragment +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/workers/settings.fragment.py +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/workers/src/app/workers/__init__.py +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/workers/src/app/workers/celery_app.py +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/workers/src/app/workers/tasks.py +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/workers/tests/test_workers.py +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/fragments.py +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/resources.py +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/README.md +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/cookiecutter.json +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/hooks/post_gen_project.py +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/.cookiecutterignore +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/.dockerignore +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/Dockerfile +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/README.md +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/__init__.py +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/cache.py +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/delegates/__init__.py +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/delegates/upstreams.py +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/dependencies/__init__.py +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/dependencies/delegates.py +0 -0
- {csrd_utils-0.3.56 → 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.56 → 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.56 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/docker-compose.yml +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/middleware/__init__.py +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/middleware/logging.py +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/repositories/__init__.py +0 -0
- {csrd_utils-0.3.56 → 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.56 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/requirements.txt +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/settings.py +0 -0
- {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/__init__.py +0 -0
- {csrd_utils-0.3.56 → 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.56 → 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.56 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/conftest.py +0 -0
- {csrd_utils-0.3.56 → 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.56 → 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.56 → 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.56 → 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.56 → 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.56 → 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.56 → 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.56 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/views/__init__.py +0 -0
- {csrd_utils-0.3.56 → 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.56 → 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.56 → 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.56 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/workers/__init__.py +0 -0
- {csrd_utils-0.3.56 → 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
|
|
|
@@ -476,24 +476,48 @@ def _generate_services_from_cluster_spec(
|
|
|
476
476
|
continue
|
|
477
477
|
|
|
478
478
|
try:
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
+
)
|
|
497
521
|
generated.append(result)
|
|
498
522
|
print(f"Generated service: {name}")
|
|
499
523
|
except Exception as exc:
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# {{ cookiecutter.service_name }}
|
|
2
|
+
|
|
3
|
+
Platform identity service — manages users and credentials.
|
|
4
|
+
|
|
5
|
+
## Endpoints
|
|
6
|
+
|
|
7
|
+
- `POST /api/signup` — create a new user
|
|
8
|
+
- `GET /api/users/{user_id}` — get user by ID
|
|
9
|
+
- `GET /api/users/{user_id}/roles` — get user roles
|
|
10
|
+
- `POST /api/users/{user_id}/roles` — assign a role to user
|
|
11
|
+
- `POST /api/verify-credentials` — verify username/password (used by auth-service)
|
|
12
|
+
- `GET /health` — liveness probe
|
|
13
|
+
|
|
14
|
+
## Configuration
|
|
15
|
+
|
|
16
|
+
| Variable | Default | Description |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| `DB_PATH` | `./{{ cookiecutter.__service_name_snake }}.db` | SQLite database path |
|
|
19
|
+
|
|
20
|
+
## Run
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install -r requirements.txt
|
|
24
|
+
uvicorn {{ cookiecutter.__service_name_snake }}:build_app --factory --port {{ cookiecutter.port }}
|
|
25
|
+
```
|
|
@@ -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",)
|