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.
Files changed (116) hide show
  1. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/PKG-INFO +1 -1
  2. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/pyproject.toml +1 -1
  3. csrd_utils-0.3.57/src/csrd_utils/__init__.py +14 -0
  4. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/__main__.py +43 -19
  5. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/cluster.py +27 -27
  6. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/generator.py +100 -0
  7. csrd_utils-0.3.57/src/csrd_utils/templates/platform-auth-service/cookiecutter.json +6 -0
  8. csrd_utils-0.3.57/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/.dockerignore +7 -0
  9. csrd_utils-0.3.57/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/Dockerfile +10 -0
  10. csrd_utils-0.3.57/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/README.md +28 -0
  11. csrd_utils-0.3.57/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/__init__.py +23 -0
  12. csrd_utils-0.3.57/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/dependencies/__init__.py +25 -0
  13. csrd_utils-0.3.57/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/repositories/__init__.py +3 -0
  14. csrd_utils-0.3.57/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/repositories/token_repository.py +32 -0
  15. csrd_utils-0.3.57/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/requirements.txt +9 -0
  16. csrd_utils-0.3.57/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/settings.py +20 -0
  17. csrd_utils-0.3.57/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/views/__init__.py +3 -0
  18. csrd_utils-0.3.57/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/views/unversioned/__init__.py +10 -0
  19. csrd_utils-0.3.57/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/views/unversioned/auth_view.py +93 -0
  20. csrd_utils-0.3.57/src/csrd_utils/templates/platform-auth-service/{{cookiecutter.__service_name_snake}}/views/unversioned/health_view.py +8 -0
  21. csrd_utils-0.3.57/src/csrd_utils/templates/platform-identity-service/cookiecutter.json +5 -0
  22. csrd_utils-0.3.57/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/.dockerignore +7 -0
  23. csrd_utils-0.3.57/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/Dockerfile +10 -0
  24. csrd_utils-0.3.57/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/README.md +25 -0
  25. csrd_utils-0.3.57/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/__init__.py +23 -0
  26. csrd_utils-0.3.57/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/dependencies/__init__.py +25 -0
  27. csrd_utils-0.3.57/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/repositories/__init__.py +3 -0
  28. csrd_utils-0.3.57/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/repositories/user_repository.py +123 -0
  29. csrd_utils-0.3.57/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/requirements.txt +6 -0
  30. csrd_utils-0.3.57/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/settings.py +12 -0
  31. csrd_utils-0.3.57/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/views/__init__.py +3 -0
  32. csrd_utils-0.3.57/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/views/unversioned/__init__.py +10 -0
  33. csrd_utils-0.3.57/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/views/unversioned/health_view.py +8 -0
  34. csrd_utils-0.3.57/src/csrd_utils/templates/platform-identity-service/{{cookiecutter.__service_name_snake}}/views/unversioned/users_view.py +63 -0
  35. csrd_utils-0.3.56/src/csrd_utils/__init__.py +0 -8
  36. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/.gitignore +0 -0
  37. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/README.md +0 -0
  38. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/audit.py +0 -0
  39. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/augmentor.py +0 -0
  40. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/doctor.py +0 -0
  41. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/caching/conftest.fragment.py +0 -0
  42. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/caching/docker-compose.fragment.yaml +0 -0
  43. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/caching/manifest.yaml +0 -0
  44. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/caching/requirements.fragment +0 -0
  45. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/caching/settings.fragment.py +0 -0
  46. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/caching/src/app/cache.py +0 -0
  47. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/caching/tests/test_cache.py +0 -0
  48. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/logging/conftest.fragment.py +0 -0
  49. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/logging/manifest.yaml +0 -0
  50. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/logging/requirements.fragment +0 -0
  51. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/logging/settings.fragment.py +0 -0
  52. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/logging/src/app/middleware/logging.py +0 -0
  53. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/logging/tests/test_logging_middleware.py +0 -0
  54. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/metrics/conftest.fragment.py +0 -0
  55. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/metrics/docker-compose.fragment.yaml +0 -0
  56. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/metrics/manifest.yaml +0 -0
  57. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/metrics/requirements.fragment +0 -0
  58. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/metrics/settings.fragment.py +0 -0
  59. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/metrics/src/app/middleware/metrics.py +0 -0
  60. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/metrics/tests/test_metrics_middleware.py +0 -0
  61. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/tracing/conftest.fragment.py +0 -0
  62. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/tracing/manifest.yaml +0 -0
  63. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/tracing/requirements.fragment +0 -0
  64. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/tracing/settings.fragment.py +0 -0
  65. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/tracing/src/app/middleware/tracing.py +0 -0
  66. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/tracing/tests/test_tracing_middleware.py +0 -0
  67. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/workers/conftest.fragment.py +0 -0
  68. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/workers/docker-compose.fragment.yaml +0 -0
  69. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/workers/manifest.yaml +0 -0
  70. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/workers/requirements.fragment +0 -0
  71. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/workers/settings.fragment.py +0 -0
  72. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/workers/src/app/workers/__init__.py +0 -0
  73. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/workers/src/app/workers/celery_app.py +0 -0
  74. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/workers/src/app/workers/tasks.py +0 -0
  75. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/features/workers/tests/test_workers.py +0 -0
  76. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/fragments.py +0 -0
  77. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/resources.py +0 -0
  78. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/README.md +0 -0
  79. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/cookiecutter.json +0 -0
  80. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/hooks/post_gen_project.py +0 -0
  81. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/.cookiecutterignore +0 -0
  82. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/.dockerignore +0 -0
  83. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/Dockerfile +0 -0
  84. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/README.md +0 -0
  85. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/__init__.py +0 -0
  86. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/cache.py +0 -0
  87. {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
  88. {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
  89. {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
  90. {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
  91. {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
  92. {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
  93. {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
  94. {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
  95. {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
  96. {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
  97. {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
  98. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/requirements.txt +0 -0
  99. {csrd_utils-0.3.56 → csrd_utils-0.3.57}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/settings.py +0 -0
  100. {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
  101. {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
  102. {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
  103. {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
  104. {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
  105. {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
  106. {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
  107. {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
  108. {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
  109. {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
  110. {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
  111. {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
  112. {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
  113. {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
  114. {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
  115. {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
  116. {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.56
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "csrd-utils"
3
- version = "0.3.56"
3
+ version = "0.3.57"
4
4
  description = "CLI utilities for csrd service generation and feature augmentation"
5
5
  license = { text = "MIT" }
6
6
  requires-python = ">=3.12"
@@ -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
- result = generate_service(
480
- name=name,
481
- output_dir=workspace_root,
482
- description=f"{name} service",
483
- port=port,
484
- model_name="Item",
485
- model_name_plural="items",
486
- has_database=has_database,
487
- database=database,
488
- has_delegates=has_delegates,
489
- upstreams=upstreams,
490
- has_workers=has_workers,
491
- broker=broker,
492
- has_caching=has_caching,
493
- cache_backend="redis",
494
- has_structured_logging=has_structured_logging,
495
- overwrite_if_exists=force,
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
- {"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
- ],
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,6 @@
1
+ {
2
+ "service_name": "auth-service",
3
+ "port": "8081",
4
+ "identity_service_url": "http://identity-service:8082",
5
+ "__service_name_snake": "{{ cookiecutter.service_name | replace('-', '_') }}"
6
+ }
@@ -0,0 +1,10 @@
1
+ FROM python:3.12-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY . /app
6
+ RUN pip install --no-cache-dir -r /app/requirements.txt
7
+
8
+ ENV PORT={{ cookiecutter.port }}
9
+
10
+ CMD uvicorn {{cookiecutter.__service_name_snake}}:build_app --factory --host 0.0.0.0 --port ${PORT}
@@ -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,3 @@
1
+ from .token_repository import TokenRepository
2
+
3
+ __all__ = ("TokenRepository",)
@@ -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,9 @@
1
+ fastapi>=0.115,<1
2
+ uvicorn[standard]>=0.30,<1
3
+ pyjwt>=2.8,<3
4
+ pydantic-settings>=2.6,<3
5
+ httpx>=0.27,<1
6
+ aiosqlite>=0.21,<1
7
+ csrd-versioning>=0.1.0,<1
8
+ csrd-repository>=0.1.0,<1
9
+ csrd-auth>=0.1.0,<1
@@ -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,3 @@
1
+ from .unversioned import unversioned_app
2
+
3
+ __all__ = ("unversioned_app",)
@@ -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,8 @@
1
+ from fastapi import APIRouter
2
+
3
+ router = APIRouter(tags=["Health Endpoints"])
4
+
5
+
6
+ @router.get("/health")
7
+ def health() -> dict[str, str]:
8
+ return {"status": "ok", "service": "{{ cookiecutter.service_name }}"}
@@ -0,0 +1,5 @@
1
+ {
2
+ "service_name": "identity-service",
3
+ "port": "8082",
4
+ "__service_name_snake": "{{ cookiecutter.service_name | replace('-', '_') }}"
5
+ }
@@ -0,0 +1,10 @@
1
+ FROM python:3.12-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY . /app
6
+ RUN pip install --no-cache-dir -r /app/requirements.txt
7
+
8
+ ENV PORT={{ cookiecutter.port }}
9
+
10
+ CMD uvicorn {{cookiecutter.__service_name_snake}}:build_app --factory --host 0.0.0.0 --port ${PORT}
@@ -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",)