csrd-utils 0.3.18__tar.gz → 0.3.19__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 (87) hide show
  1. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/PKG-INFO +2 -2
  2. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/README.md +1 -1
  3. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/pyproject.toml +1 -1
  4. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/__main__.py +50 -24
  5. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/augmentor.py +18 -2
  6. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/doctor.py +8 -1
  7. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/views/unversioned/items_view.py +44 -12
  8. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/.gitignore +0 -0
  9. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/__init__.py +0 -0
  10. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/audit.py +0 -0
  11. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/cluster.py +0 -0
  12. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/features/caching/conftest.fragment.py +0 -0
  13. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/features/caching/docker-compose.fragment.yaml +0 -0
  14. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/features/caching/manifest.yaml +0 -0
  15. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/features/caching/requirements.fragment +0 -0
  16. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/features/caching/settings.fragment.py +0 -0
  17. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/features/caching/src/app/cache.py +0 -0
  18. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/features/caching/tests/test_cache.py +0 -0
  19. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/features/logging/conftest.fragment.py +0 -0
  20. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/features/logging/manifest.yaml +0 -0
  21. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/features/logging/requirements.fragment +0 -0
  22. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/features/logging/settings.fragment.py +0 -0
  23. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/features/logging/src/app/middleware/logging.py +0 -0
  24. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/features/logging/tests/test_logging_middleware.py +0 -0
  25. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/features/metrics/conftest.fragment.py +0 -0
  26. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/features/metrics/docker-compose.fragment.yaml +0 -0
  27. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/features/metrics/manifest.yaml +0 -0
  28. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/features/metrics/requirements.fragment +0 -0
  29. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/features/metrics/settings.fragment.py +0 -0
  30. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/features/metrics/src/app/middleware/metrics.py +0 -0
  31. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/features/metrics/tests/test_metrics_middleware.py +0 -0
  32. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/features/tracing/conftest.fragment.py +0 -0
  33. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/features/tracing/manifest.yaml +0 -0
  34. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/features/tracing/requirements.fragment +0 -0
  35. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/features/tracing/settings.fragment.py +0 -0
  36. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/features/tracing/src/app/middleware/tracing.py +0 -0
  37. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/features/tracing/tests/test_tracing_middleware.py +0 -0
  38. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/features/workers/conftest.fragment.py +0 -0
  39. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/features/workers/docker-compose.fragment.yaml +0 -0
  40. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/features/workers/manifest.yaml +0 -0
  41. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/features/workers/requirements.fragment +0 -0
  42. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/features/workers/settings.fragment.py +0 -0
  43. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/features/workers/src/app/workers/__init__.py +0 -0
  44. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/features/workers/src/app/workers/celery_app.py +0 -0
  45. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/features/workers/src/app/workers/tasks.py +0 -0
  46. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/features/workers/tests/test_workers.py +0 -0
  47. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/fragments.py +0 -0
  48. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/generator.py +0 -0
  49. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/resources.py +0 -0
  50. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/README.md +0 -0
  51. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/cookiecutter.json +0 -0
  52. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/hooks/post_gen_project.py +0 -0
  53. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/.dockerignore +0 -0
  54. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/.env.example +0 -0
  55. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/.gitignore +0 -0
  56. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/Dockerfile +0 -0
  57. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/README.md +0 -0
  58. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/docker-compose.yml +0 -0
  59. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/pyproject.toml +0 -0
  60. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/requirements.txt +0 -0
  61. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/__init__.py +0 -0
  62. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/cache.py +0 -0
  63. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/delegates/__init__.py +0 -0
  64. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/delegates/upstreams.py +0 -0
  65. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/dependencies/__init__.py +0 -0
  66. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/dependencies/delegates.py +0 -0
  67. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/dependencies/item_repository.py +0 -0
  68. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/dependencies/worker_broker.py +0 -0
  69. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/middleware/__init__.py +0 -0
  70. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/middleware/logging.py +0 -0
  71. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/py.typed +0 -0
  72. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/repositories/__init__.py +0 -0
  73. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/repositories/item_repository.py +0 -0
  74. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/settings.py +0 -0
  75. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/views/__init__.py +0 -0
  76. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/views/unversioned/__init__.py +0 -0
  77. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/views/unversioned/health_view.py +0 -0
  78. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/workers/__init__.py +0 -0
  79. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/workers/tasks.py +0 -0
  80. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/__init__.py +0 -0
  81. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/acceptance/test_logging_middleware.py +0 -0
  82. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/conftest.py +0 -0
  83. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/test_items.py +0 -0
  84. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/unit/test_cache.py +0 -0
  85. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/unit/test_database_settings.py +0 -0
  86. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/unit/test_delegates.py +0 -0
  87. {csrd_utils-0.3.18 → csrd_utils-0.3.19}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/unit/test_worker_broker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: csrd-utils
3
- Version: 0.3.18
3
+ Version: 0.3.19
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
@@ -51,7 +51,7 @@ csrd audit --service . # explicitly audit one servi
51
51
  - Non-interactive generation errors when no workspace marker is present.
52
52
  - Workspace cluster source of truth is persisted at `.csrd/cluster.yaml`.
53
53
  - If `.csrd/cluster.yaml` already exists, `csrd new service` appends the generated service and refreshes scaffold files (`docker-compose.yml`, `.env.example`, `README.md`, `smoke.sh`). Service source directories and running containers are never touched.
54
- - If `.csrd/cluster.yaml` is missing and a second service is added to the workspace, `csrd new service` bootstraps `.csrd/cluster.yaml` and writes initial cluster scaffold files.
54
+ - If `.csrd/cluster.yaml` is missing, `csrd new service` bootstraps `.csrd/cluster.yaml` and writes initial cluster scaffold files.
55
55
  - `csrd new service` runs a compact interactive flow by default (core prompts 1-6), then asks whether to show advanced options.
56
56
  - `csrd new workspace` creates a workspace directory and prompts whether to generate `none`, `service`, or `cluster` immediately.
57
57
  - In compact mode, advanced options are hidden by default.
@@ -38,7 +38,7 @@ csrd audit --service . # explicitly audit one servi
38
38
  - Non-interactive generation errors when no workspace marker is present.
39
39
  - Workspace cluster source of truth is persisted at `.csrd/cluster.yaml`.
40
40
  - If `.csrd/cluster.yaml` already exists, `csrd new service` appends the generated service and refreshes scaffold files (`docker-compose.yml`, `.env.example`, `README.md`, `smoke.sh`). Service source directories and running containers are never touched.
41
- - If `.csrd/cluster.yaml` is missing and a second service is added to the workspace, `csrd new service` bootstraps `.csrd/cluster.yaml` and writes initial cluster scaffold files.
41
+ - If `.csrd/cluster.yaml` is missing, `csrd new service` bootstraps `.csrd/cluster.yaml` and writes initial cluster scaffold files.
42
42
  - `csrd new service` runs a compact interactive flow by default (core prompts 1-6), then asks whether to show advanced options.
43
43
  - `csrd new workspace` creates a workspace directory and prompts whether to generate `none`, `service`, or `cluster` immediately.
44
44
  - In compact mode, advanced options are hidden by default.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "csrd-utils"
3
- version = "0.3.18"
3
+ version = "0.3.19"
4
4
  description = "CLI utilities for csrd service generation and feature augmentation"
5
5
  license = { text = "MIT" }
6
6
  requires-python = ">=3.12"
@@ -489,21 +489,27 @@ def _append_service_to_workspace_cluster(
489
489
 
490
490
 
491
491
  def _workspace_service_roots(workspace_root: Path) -> list[Path]:
492
- roots: list[Path] = []
493
- services_root = _workspace_services_dir(workspace_root)
494
- if not services_root.is_dir():
495
- return roots
492
+ roots: set[Path] = set()
493
+
494
+ def _collect_from(base_dir: Path, skip_names: set[str] | None = None) -> None:
495
+ if not base_dir.is_dir():
496
+ return
497
+ for child in base_dir.iterdir():
498
+ if not child.is_dir() or child.name.startswith("."):
499
+ continue
500
+ if skip_names and child.name in skip_names:
501
+ continue
502
+ if (
503
+ (child / "src" / "app").is_dir()
504
+ and (child / "tests").is_dir()
505
+ and (child / "requirements.txt").is_file()
506
+ ):
507
+ roots.add(child.resolve())
496
508
 
497
- for child in services_root.iterdir():
498
- if not child.is_dir() or child.name.startswith("."):
499
- continue
500
- if (
501
- (child / "src" / "app").is_dir()
502
- and (child / "tests").is_dir()
503
- and (child / "requirements.txt").is_file()
504
- and (child / "docker-compose.yml").is_file()
505
- ):
506
- roots.append(child)
509
+ services_root = _workspace_services_dir(workspace_root)
510
+ _collect_from(services_root)
511
+ # Legacy fallback: older workspaces stored services at workspace root.
512
+ _collect_from(workspace_root, skip_names={"src"})
507
513
  return sorted(roots)
508
514
 
509
515
 
@@ -514,20 +520,24 @@ def _resolve_feature_service_root(service_arg: Path) -> Path:
514
520
 
515
521
  service_roots = _workspace_service_roots(candidate)
516
522
  if len(service_roots) == 1:
517
- print(f"Detected workspace service target: {service_roots[0]}")
518
523
  return service_roots[0]
519
524
  if not service_roots:
520
- raise ValueError(
521
- "No services found under workspace src/. Pass --service with a service root path."
522
- )
525
+ raise ValueError("No services found in workspace. Pass --service with a service root path.")
523
526
  raise ValueError(
524
- "Multiple services found under workspace src/. Pass --service with a specific service root path."
527
+ "Multiple services found in workspace. Pass --service with a specific service root path."
525
528
  )
526
529
 
527
530
 
528
- def _infer_port_from_service_root(service_root: Path) -> int:
531
+ def _workspace_root_for_service(service_root: Path) -> Path | None:
532
+ parent = service_root.parent
533
+ if parent.name == "src" and _is_workspace(parent.parent):
534
+ return parent.parent
535
+ return None
536
+
537
+
538
+ def _infer_port_from_compose(compose_path: Path, service_name: str | None = None) -> int:
529
539
  try:
530
- payload = yaml.safe_load((service_root / "docker-compose.yml").read_text(encoding="utf-8"))
540
+ payload = yaml.safe_load(compose_path.read_text(encoding="utf-8"))
531
541
  except (OSError, yaml.YAMLError):
532
542
  return 8080
533
543
 
@@ -537,10 +547,15 @@ def _infer_port_from_service_root(service_root: Path) -> int:
537
547
  if not isinstance(services, dict) or not services:
538
548
  return 8080
539
549
 
540
- first_service = next(iter(services.values()))
541
- if not isinstance(first_service, dict):
550
+ selected: object
551
+ if service_name and service_name in services:
552
+ selected = services[service_name]
553
+ else:
554
+ selected = next(iter(services.values()))
555
+ if not isinstance(selected, dict):
542
556
  return 8080
543
- ports = first_service.get("ports")
557
+
558
+ ports = selected.get("ports")
544
559
  if not isinstance(ports, list) or not ports:
545
560
  return 8080
546
561
 
@@ -554,6 +569,17 @@ def _infer_port_from_service_root(service_root: Path) -> int:
554
569
  return 8080
555
570
 
556
571
 
572
+ def _infer_port_from_service_root(service_root: Path) -> int:
573
+ service_compose = service_root / "docker-compose.yml"
574
+ if service_compose.is_file():
575
+ return _infer_port_from_compose(service_compose)
576
+
577
+ workspace_root = _workspace_root_for_service(service_root)
578
+ if workspace_root is not None:
579
+ return _infer_port_from_compose(workspace_root / "docker-compose.yml", service_root.name)
580
+ return 8080
581
+
582
+
557
583
  def _infer_existing_workspace_service(service_root: Path) -> ServiceSpec:
558
584
  return {
559
585
  "name": service_root.name,
@@ -80,7 +80,7 @@ class ServiceAugmentor:
80
80
  True if valid
81
81
  """
82
82
  required_dirs = ["src/app", "tests"]
83
- required_files = ["docker-compose.yml", "requirements.txt"]
83
+ required_files = ["requirements.txt"]
84
84
 
85
85
  for d in required_dirs:
86
86
  if not (self.service_root / d).is_dir():
@@ -92,8 +92,24 @@ class ServiceAugmentor:
92
92
  print(f"❌ Missing required file: {f}")
93
93
  return False
94
94
 
95
+ if not self._docker_compose_path().is_file():
96
+ print("❌ Missing required file: docker-compose.yml")
97
+ return False
98
+
95
99
  return True
96
100
 
101
+ def _docker_compose_path(self) -> Path:
102
+ local = self.service_root / "docker-compose.yml"
103
+ if local.is_file():
104
+ return local
105
+
106
+ parent = self.service_root.parent
107
+ if parent.name == "src" and (parent.parent / ".csrd-workspace").is_file():
108
+ workspace_compose = parent.parent / "docker-compose.yml"
109
+ if workspace_compose.is_file():
110
+ return workspace_compose
111
+ return local
112
+
97
113
  def validate_requirements(self) -> list[str]:
98
114
  """
99
115
  Validate that feature requirements are met.
@@ -189,7 +205,7 @@ class ServiceAugmentor:
189
205
  return True # Not required
190
206
 
191
207
  try:
192
- dc_path = self.service_root / "docker-compose.yml"
208
+ dc_path = self._docker_compose_path()
193
209
  self.backup_file(dc_path)
194
210
 
195
211
  # Load YAMLs
@@ -20,7 +20,11 @@ def run_doctor(service_root: Path) -> DoctorReport:
20
20
  warnings: list[str] = []
21
21
 
22
22
  required_dirs = ["src/app", "tests"]
23
- required_files = ["docker-compose.yml", "requirements.txt"]
23
+ required_files = ["requirements.txt"]
24
+
25
+ compose_candidates = [root / "docker-compose.yml"]
26
+ if root.parent.name == "src" and (root.parent.parent / ".csrd-workspace").is_file():
27
+ compose_candidates.append(root.parent.parent / "docker-compose.yml")
24
28
 
25
29
  for rel in required_dirs:
26
30
  if not (root / rel).is_dir():
@@ -30,6 +34,9 @@ def run_doctor(service_root: Path) -> DoctorReport:
30
34
  if not (root / rel).is_file():
31
35
  errors.append(f"Missing required file: {rel}")
32
36
 
37
+ if not any(path.is_file() for path in compose_candidates):
38
+ errors.append("Missing required file: docker-compose.yml")
39
+
33
40
  marker_files = {
34
41
  "src/app/settings.py": "# [INSERT: fields]",
35
42
  "tests/conftest.py": "# [INSERT: fixtures]",
@@ -31,6 +31,33 @@ class Update{{ cookiecutter.model_name }}Request(BaseModel):
31
31
  name: str
32
32
 
33
33
 
34
+ class {{ cookiecutter.model_name }}Response(BaseModel):
35
+ id: str
36
+ name: str
37
+ created_at: str
38
+
39
+
40
+ class {{ cookiecutter.model_name }}ListResponse(BaseModel):
41
+ {{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}: list[{{ cookiecutter.model_name }}Response]
42
+
43
+
44
+ class {{ cookiecutter.model_name }}UpdateResponse(BaseModel):
45
+ id: str
46
+ name: str
47
+ updated: bool
48
+
49
+
50
+ class {{ cookiecutter.model_name }}UpsertResponse(BaseModel):
51
+ id: str
52
+ name: str
53
+ rows_affected: int
54
+
55
+
56
+ class {{ cookiecutter.model_name }}DeleteResponse(BaseModel):
57
+ id: str
58
+ deleted: bool
59
+
60
+
34
61
  {%- if cookiecutter.has_delegates and not cookiecutter.has_database %}
35
62
  async def _delegate_request(
36
63
  method: str,
@@ -59,13 +86,13 @@ def _dummy_item(record_id: str, name: str, created_at: str) -> dict[str, str]:
59
86
  {%- endif %}
60
87
 
61
88
 
62
- @router.post(ITEMS_ROUTE)
89
+ @router.post(ITEMS_ROUTE, response_model={{ cookiecutter.model_name }}Response)
63
90
  async def create_{{ cookiecutter.model_name | lower }}(
64
91
  payload: Create{{ cookiecutter.model_name }}Request,
65
92
  {%- if cookiecutter.has_database %}
66
93
  repo: RepositoryDep,
67
94
  {%- endif %}
68
- ) -> dict[str, str]:
95
+ ) -> {{ cookiecutter.model_name }}Response:
69
96
  {%- if cookiecutter.has_database %}
70
97
  return await repo.create(payload.name)
71
98
  {%- elif cookiecutter.has_delegates %}
@@ -84,12 +111,12 @@ async def create_{{ cookiecutter.model_name | lower }}(
84
111
  {%- endif %}
85
112
 
86
113
 
87
- @router.get(ITEMS_ROUTE)
114
+ @router.get(ITEMS_ROUTE, response_model={{ cookiecutter.model_name }}ListResponse)
88
115
  async def list_{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}(
89
116
  {%- if cookiecutter.has_database %}
90
117
  repo: RepositoryDep,
91
118
  {%- endif %}
92
- ) -> dict[str, list[dict[str, str]]]:
119
+ ) -> {{ cookiecutter.model_name }}ListResponse:
93
120
  {%- if cookiecutter.has_database %}
94
121
  return {"{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}": await repo.list()}
95
122
  {%- elif cookiecutter.has_delegates %}
@@ -104,13 +131,13 @@ async def list_{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}(
104
131
  {%- endif %}
105
132
 
106
133
 
107
- @router.get(f"{ITEMS_ROUTE}" + "/{entity_id}")
134
+ @router.get(f"{ITEMS_ROUTE}" + "/{entity_id}", response_model={{ cookiecutter.model_name }}Response)
108
135
  async def get_{{ cookiecutter.model_name | lower }}(
109
136
  entity_id: str,
110
137
  {%- if cookiecutter.has_database %}
111
138
  repo: RepositoryDep,
112
139
  {%- endif %}
113
- ) -> dict[str, str]:
140
+ ) -> {{ cookiecutter.model_name }}Response:
114
141
  {%- if cookiecutter.has_database %}
115
142
  item = await repo.get(entity_id)
116
143
  {%- elif cookiecutter.has_delegates %}
@@ -128,14 +155,16 @@ async def get_{{ cookiecutter.model_name | lower }}(
128
155
  return item
129
156
 
130
157
 
131
- @router.put(f"{ITEMS_ROUTE}" + "/{entity_id}")
158
+ @router.put(
159
+ f"{ITEMS_ROUTE}" + "/{entity_id}", response_model={{ cookiecutter.model_name }}UpdateResponse
160
+ )
132
161
  async def update_{{ cookiecutter.model_name | lower }}(
133
162
  entity_id: str,
134
163
  payload: Update{{ cookiecutter.model_name }}Request,
135
164
  {%- if cookiecutter.has_database %}
136
165
  repo: RepositoryDep,
137
166
  {%- endif %}
138
- ) -> dict[str, object]:
167
+ ) -> {{ cookiecutter.model_name }}UpdateResponse:
139
168
  {%- if cookiecutter.has_database %}
140
169
  rows = await repo.update(entity_id, payload.name)
141
170
  {%- elif cookiecutter.has_delegates %}
@@ -161,14 +190,17 @@ async def update_{{ cookiecutter.model_name | lower }}(
161
190
  return {"id": entity_id, "name": payload.name, "updated": True}
162
191
 
163
192
 
164
- @router.put(f"{ITEMS_ROUTE}" + "/{entity_id}/upsert")
193
+ @router.put(
194
+ f"{ITEMS_ROUTE}" + "/{entity_id}/upsert",
195
+ response_model={{ cookiecutter.model_name }}UpsertResponse,
196
+ )
165
197
  async def upsert_{{ cookiecutter.model_name | lower }}(
166
198
  entity_id: str,
167
199
  payload: Update{{ cookiecutter.model_name }}Request,
168
200
  {%- if cookiecutter.has_database %}
169
201
  repo: RepositoryDep,
170
202
  {%- endif %}
171
- ) -> dict[str, object]:
203
+ ) -> {{ cookiecutter.model_name }}UpsertResponse:
172
204
  {%- if cookiecutter.has_database %}
173
205
  rows = await repo.upsert(entity_id, payload.name)
174
206
  {%- elif cookiecutter.has_delegates %}
@@ -194,13 +226,13 @@ async def upsert_{{ cookiecutter.model_name | lower }}(
194
226
  return {"id": entity_id, "name": payload.name, "rows_affected": rows}
195
227
 
196
228
 
197
- @router.delete(f"{ITEMS_ROUTE}" + "/{entity_id}")
229
+ @router.delete(f"{ITEMS_ROUTE}" + "/{entity_id}", response_model={{ cookiecutter.model_name }}DeleteResponse)
198
230
  async def delete_{{ cookiecutter.model_name | lower }}(
199
231
  entity_id: str,
200
232
  {%- if cookiecutter.has_database %}
201
233
  repo: RepositoryDep,
202
234
  {%- endif %}
203
- ) -> dict[str, object]:
235
+ ) -> {{ cookiecutter.model_name }}DeleteResponse:
204
236
  {%- if cookiecutter.has_database %}
205
237
  rows = await repo.delete(entity_id)
206
238
  {%- elif cookiecutter.has_delegates %}
File without changes