csrd-utils 0.3.16__tar.gz → 0.3.18__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.16 → csrd_utils-0.3.18}/PKG-INFO +9 -9
  2. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/README.md +8 -8
  3. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/pyproject.toml +1 -1
  4. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/__main__.py +56 -13
  5. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/cluster.py +11 -2
  6. csrd_utils-0.3.18/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/.gitignore +64 -0
  7. csrd_utils-0.3.18/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/pyproject.toml +61 -0
  8. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/views/unversioned/items_view.py +4 -4
  9. csrd_utils-0.3.18/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/__init__.py +0 -0
  10. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/.gitignore +0 -0
  11. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/__init__.py +0 -0
  12. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/audit.py +0 -0
  13. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/augmentor.py +0 -0
  14. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/doctor.py +0 -0
  15. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/features/caching/conftest.fragment.py +0 -0
  16. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/features/caching/docker-compose.fragment.yaml +0 -0
  17. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/features/caching/manifest.yaml +0 -0
  18. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/features/caching/requirements.fragment +0 -0
  19. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/features/caching/settings.fragment.py +0 -0
  20. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/features/caching/src/app/cache.py +0 -0
  21. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/features/caching/tests/test_cache.py +0 -0
  22. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/features/logging/conftest.fragment.py +0 -0
  23. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/features/logging/manifest.yaml +0 -0
  24. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/features/logging/requirements.fragment +0 -0
  25. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/features/logging/settings.fragment.py +0 -0
  26. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/features/logging/src/app/middleware/logging.py +0 -0
  27. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/features/logging/tests/test_logging_middleware.py +0 -0
  28. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/features/metrics/conftest.fragment.py +0 -0
  29. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/features/metrics/docker-compose.fragment.yaml +0 -0
  30. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/features/metrics/manifest.yaml +0 -0
  31. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/features/metrics/requirements.fragment +0 -0
  32. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/features/metrics/settings.fragment.py +0 -0
  33. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/features/metrics/src/app/middleware/metrics.py +0 -0
  34. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/features/metrics/tests/test_metrics_middleware.py +0 -0
  35. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/features/tracing/conftest.fragment.py +0 -0
  36. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/features/tracing/manifest.yaml +0 -0
  37. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/features/tracing/requirements.fragment +0 -0
  38. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/features/tracing/settings.fragment.py +0 -0
  39. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/features/tracing/src/app/middleware/tracing.py +0 -0
  40. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/features/tracing/tests/test_tracing_middleware.py +0 -0
  41. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/features/workers/conftest.fragment.py +0 -0
  42. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/features/workers/docker-compose.fragment.yaml +0 -0
  43. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/features/workers/manifest.yaml +0 -0
  44. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/features/workers/requirements.fragment +0 -0
  45. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/features/workers/settings.fragment.py +0 -0
  46. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/features/workers/src/app/workers/__init__.py +0 -0
  47. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/features/workers/src/app/workers/celery_app.py +0 -0
  48. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/features/workers/src/app/workers/tasks.py +0 -0
  49. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/features/workers/tests/test_workers.py +0 -0
  50. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/fragments.py +0 -0
  51. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/generator.py +0 -0
  52. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/resources.py +0 -0
  53. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/templates/cookiecutter-service/README.md +0 -0
  54. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/templates/cookiecutter-service/cookiecutter.json +0 -0
  55. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/templates/cookiecutter-service/hooks/post_gen_project.py +0 -0
  56. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/.dockerignore +0 -0
  57. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/.env.example +0 -0
  58. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/Dockerfile +0 -0
  59. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/README.md +0 -0
  60. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/docker-compose.yml +0 -0
  61. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/requirements.txt +0 -0
  62. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/__init__.py +0 -0
  63. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/cache.py +0 -0
  64. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/delegates/__init__.py +0 -0
  65. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/delegates/upstreams.py +0 -0
  66. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/dependencies/__init__.py +0 -0
  67. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/dependencies/delegates.py +0 -0
  68. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/dependencies/item_repository.py +0 -0
  69. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/dependencies/worker_broker.py +0 -0
  70. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/middleware/__init__.py +0 -0
  71. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/middleware/logging.py +0 -0
  72. /csrd_utils-0.3.16/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/__init__.py → /csrd_utils-0.3.18/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/py.typed +0 -0
  73. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/repositories/__init__.py +0 -0
  74. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/repositories/item_repository.py +0 -0
  75. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/settings.py +0 -0
  76. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/views/__init__.py +0 -0
  77. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/views/unversioned/__init__.py +0 -0
  78. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/views/unversioned/health_view.py +0 -0
  79. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/workers/__init__.py +0 -0
  80. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/workers/tasks.py +0 -0
  81. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/acceptance/test_logging_middleware.py +0 -0
  82. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/conftest.py +0 -0
  83. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/test_items.py +0 -0
  84. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/unit/test_cache.py +0 -0
  85. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/unit/test_database_settings.py +0 -0
  86. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/unit/test_delegates.py +0 -0
  87. {csrd_utils-0.3.16 → csrd_utils-0.3.18}/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.16
3
+ Version: 0.3.18
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
@@ -70,19 +70,19 @@ csrd new workspace --name local-dev --output . --generate none
70
70
  csrd new service --name inventory --output ./local-dev --database postgres --workers
71
71
 
72
72
  # 1) Verify service compatibility
73
- csrd doctor --service ./local-dev/inventory
73
+ csrd doctor --service ./local-dev/src/inventory
74
74
 
75
75
  # 1b) Audit for weak/insecure defaults
76
- csrd audit --service ./local-dev/inventory
76
+ csrd audit --service ./local-dev/src/inventory
77
77
 
78
78
  # 2) Inspect available bundled features
79
79
  csrd feature list
80
80
 
81
81
  # 3) Dry-run a feature
82
- csrd feature plan workers --service ./local-dev/inventory
82
+ csrd feature plan workers --service ./local-dev/src/inventory
83
83
 
84
84
  # 4) Apply feature files/merges
85
- csrd feature add workers --service ./local-dev/inventory
85
+ csrd feature add workers --service ./local-dev/src/inventory
86
86
 
87
87
  # 5) Add a second service — workspace auto-upgrades to cluster
88
88
  csrd new service --name pricing --output ./local-dev --database postgres
@@ -135,8 +135,8 @@ Then run a clean smoke flow:
135
135
  mkdir -p /tmp/csrd-smoke
136
136
  csrd new workspace --name ws --output /tmp/csrd-smoke --generate none
137
137
  csrd new service --name demo-svc --output /tmp/csrd-smoke/ws --no-interactive
138
- csrd doctor --service /tmp/csrd-smoke/ws/demo-svc
139
- csrd audit --service /tmp/csrd-smoke/ws/demo-svc
140
- csrd feature plan workers --service /tmp/csrd-smoke/ws/demo-svc
141
- csrd feature add workers --service /tmp/csrd-smoke/ws/demo-svc
138
+ csrd doctor --service /tmp/csrd-smoke/ws/src/demo-svc
139
+ csrd audit --service /tmp/csrd-smoke/ws/src/demo-svc
140
+ csrd feature plan workers --service /tmp/csrd-smoke/ws/src/demo-svc
141
+ csrd feature add workers --service /tmp/csrd-smoke/ws/src/demo-svc
142
142
  ```
@@ -57,19 +57,19 @@ csrd new workspace --name local-dev --output . --generate none
57
57
  csrd new service --name inventory --output ./local-dev --database postgres --workers
58
58
 
59
59
  # 1) Verify service compatibility
60
- csrd doctor --service ./local-dev/inventory
60
+ csrd doctor --service ./local-dev/src/inventory
61
61
 
62
62
  # 1b) Audit for weak/insecure defaults
63
- csrd audit --service ./local-dev/inventory
63
+ csrd audit --service ./local-dev/src/inventory
64
64
 
65
65
  # 2) Inspect available bundled features
66
66
  csrd feature list
67
67
 
68
68
  # 3) Dry-run a feature
69
- csrd feature plan workers --service ./local-dev/inventory
69
+ csrd feature plan workers --service ./local-dev/src/inventory
70
70
 
71
71
  # 4) Apply feature files/merges
72
- csrd feature add workers --service ./local-dev/inventory
72
+ csrd feature add workers --service ./local-dev/src/inventory
73
73
 
74
74
  # 5) Add a second service — workspace auto-upgrades to cluster
75
75
  csrd new service --name pricing --output ./local-dev --database postgres
@@ -122,8 +122,8 @@ Then run a clean smoke flow:
122
122
  mkdir -p /tmp/csrd-smoke
123
123
  csrd new workspace --name ws --output /tmp/csrd-smoke --generate none
124
124
  csrd new service --name demo-svc --output /tmp/csrd-smoke/ws --no-interactive
125
- csrd doctor --service /tmp/csrd-smoke/ws/demo-svc
126
- csrd audit --service /tmp/csrd-smoke/ws/demo-svc
127
- csrd feature plan workers --service /tmp/csrd-smoke/ws/demo-svc
128
- csrd feature add workers --service /tmp/csrd-smoke/ws/demo-svc
125
+ csrd doctor --service /tmp/csrd-smoke/ws/src/demo-svc
126
+ csrd audit --service /tmp/csrd-smoke/ws/src/demo-svc
127
+ csrd feature plan workers --service /tmp/csrd-smoke/ws/src/demo-svc
128
+ csrd feature add workers --service /tmp/csrd-smoke/ws/src/demo-svc
129
129
  ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "csrd-utils"
3
- version = "0.3.16"
3
+ version = "0.3.18"
4
4
  description = "CLI utilities for csrd service generation and feature augmentation"
5
5
  license = { text = "MIT" }
6
6
  requires-python = ">=3.12"
@@ -61,6 +61,10 @@ WORKSPACE_MARKER = ".csrd-workspace"
61
61
  WORKSPACE_CLUSTER_SPEC = Path(".csrd") / "cluster.yaml"
62
62
 
63
63
 
64
+ def _workspace_services_dir(workspace_root: Path) -> Path:
65
+ return workspace_root / "src"
66
+
67
+
64
68
  def _prompt_text(label: str, default: str) -> str:
65
69
  value = input(f"{label} [{default}]: ").strip()
66
70
  return value or default
@@ -82,7 +86,15 @@ def _prompt_choice(label: str, choices: list[str], default: str) -> str:
82
86
  return default
83
87
  if value in choices:
84
88
  return value
85
- print(f"ERROR: Invalid choice '{value}'. Expected one of: {rendered}")
89
+ # Try prefix matching
90
+ matching = [choice for choice in choices if choice.startswith(value)]
91
+ if len(matching) == 1:
92
+ return matching[0]
93
+ if len(matching) > 1:
94
+ matching_str = ", ".join(matching)
95
+ print(f"ERROR: Ambiguous choice '{value}'. Matches: {matching_str}")
96
+ else:
97
+ print(f"ERROR: Invalid choice '{value}'. Expected one of: {rendered}")
86
98
 
87
99
 
88
100
  def _prompt_int(label: str, default: int) -> int:
@@ -478,7 +490,11 @@ def _append_service_to_workspace_cluster(
478
490
 
479
491
  def _workspace_service_roots(workspace_root: Path) -> list[Path]:
480
492
  roots: list[Path] = []
481
- for child in workspace_root.iterdir():
493
+ services_root = _workspace_services_dir(workspace_root)
494
+ if not services_root.is_dir():
495
+ return roots
496
+
497
+ for child in services_root.iterdir():
482
498
  if not child.is_dir() or child.name.startswith("."):
483
499
  continue
484
500
  if (
@@ -491,6 +507,24 @@ def _workspace_service_roots(workspace_root: Path) -> list[Path]:
491
507
  return sorted(roots)
492
508
 
493
509
 
510
+ def _resolve_feature_service_root(service_arg: Path) -> Path:
511
+ candidate = service_arg.resolve()
512
+ if not _is_workspace(candidate):
513
+ return candidate
514
+
515
+ service_roots = _workspace_service_roots(candidate)
516
+ if len(service_roots) == 1:
517
+ print(f"Detected workspace service target: {service_roots[0]}")
518
+ return service_roots[0]
519
+ if not service_roots:
520
+ raise ValueError(
521
+ "No services found under workspace src/. Pass --service with a service root path."
522
+ )
523
+ raise ValueError(
524
+ "Multiple services found under workspace src/. Pass --service with a specific service root path."
525
+ )
526
+
527
+
494
528
  def _infer_port_from_service_root(service_root: Path) -> int:
495
529
  try:
496
530
  payload = yaml.safe_load((service_root / "docker-compose.yml").read_text(encoding="utf-8"))
@@ -555,12 +589,9 @@ def _maybe_update_workspace_cluster(
555
589
  spec_path = _workspace_cluster_spec_path(workspace_root)
556
590
  if spec_path.is_file():
557
591
  updated_spec = _append_service_to_workspace_cluster(workspace_root, service_config)
558
- return generate_cluster(updated_spec, workspace_root, force=True)
592
+ return generate_cluster(updated_spec, workspace_root, force=True, workspace_mode=True)
559
593
 
560
594
  service_roots = _workspace_service_roots(workspace_root)
561
- if len(service_roots) < 2:
562
- return None
563
-
564
595
  generated_name = generated_service.name
565
596
  services: list[ServiceSpec] = [_service_config_to_cluster_service(service_config)]
566
597
  for service_root in service_roots:
@@ -575,7 +606,7 @@ def _maybe_update_workspace_cluster(
575
606
  }
576
607
  normalized = normalize_cluster_spec(bootstrap_spec)
577
608
  _save_workspace_cluster_spec(workspace_root, normalized)
578
- return generate_cluster(normalized, workspace_root, force=True)
609
+ return generate_cluster(normalized, workspace_root, force=True, workspace_mode=True)
579
610
 
580
611
 
581
612
  def _build_parser() -> argparse.ArgumentParser:
@@ -808,12 +839,13 @@ def main() -> int:
808
839
  if args.command == "new" and args.new_command == "service":
809
840
  try:
810
841
  service_config = _collect_new_service_config(args)
811
- service_config["output_dir"] = _ensure_workspace_for_output(
842
+ workspace_root = _ensure_workspace_for_output(
812
843
  service_config["output_dir"], allow_prompt=_service_is_interactive(args)
813
844
  )
845
+ service_config["output_dir"] = _workspace_services_dir(workspace_root)
814
846
  generated = generate_service(**service_config)
815
847
  generated_cluster = _maybe_update_workspace_cluster(
816
- service_config["output_dir"], service_config, generated
848
+ workspace_root, service_config, generated
817
849
  )
818
850
  if generated_cluster is not None:
819
851
  print(f"Refreshed cluster scaffold at: {generated_cluster}")
@@ -856,10 +888,15 @@ def main() -> int:
856
888
  generate_mode = workspace_config["generate"]
857
889
  if generate_mode == "service":
858
890
  service_config = _collect_new_service_config(
859
- _default_service_namespace(workspace_root)
891
+ _default_service_namespace(_workspace_services_dir(workspace_root))
860
892
  )
861
- service_config["output_dir"] = workspace_root
893
+ service_config["output_dir"] = _workspace_services_dir(workspace_root)
862
894
  generated_service = generate_service(**service_config)
895
+ generated_cluster = _maybe_update_workspace_cluster(
896
+ workspace_root, service_config, generated_service
897
+ )
898
+ if generated_cluster is not None:
899
+ print(f"Refreshed cluster scaffold at: {generated_cluster}")
863
900
  print(f"Generated service at: {generated_service}")
864
901
  elif generate_mode == "cluster":
865
902
  cluster_config = _collect_new_cluster_config(
@@ -893,8 +930,14 @@ def main() -> int:
893
930
  if args.feature_command == "list":
894
931
  return _list_features()
895
932
 
933
+ try:
934
+ resolved_service = _resolve_feature_service_root(args.service)
935
+ except ValueError as exc:
936
+ print(f"ERROR: {exc}")
937
+ return 1
938
+
896
939
  with features_path() as feature_lib:
897
- augmentor = ServiceAugmentor(args.service, feature_lib)
940
+ augmentor = ServiceAugmentor(resolved_service, feature_lib)
898
941
  if args.feature_command == "plan" and args.json:
899
942
  plan = augmentor.build_plan(args.feature)
900
943
  if plan is None:
@@ -903,7 +946,7 @@ def main() -> int:
903
946
  json.dumps(
904
947
  {
905
948
  "feature": args.feature,
906
- "service": str(args.service.resolve()),
949
+ "service": str(resolved_service),
907
950
  "modifies": plan["modifies"],
908
951
  "creates": plan["creates"],
909
952
  },
@@ -452,9 +452,18 @@ def _build_smoke(spec: ClusterSpec) -> str:
452
452
  return "\n".join(lines) + "\n"
453
453
 
454
454
 
455
- def generate_cluster(spec: ClusterSpec, output_dir: Path, force: bool = False) -> Path:
455
+ def generate_cluster(
456
+ spec: ClusterSpec, output_dir: Path, force: bool = False, workspace_mode: bool = False
457
+ ) -> Path:
456
458
  normalized = normalize_cluster_spec(spec)
457
- cluster_root = output_dir.resolve() / normalized["cluster"]["name"]
459
+
460
+ if workspace_mode:
461
+ # In workspace mode, write files directly to output_dir (workspace root)
462
+ cluster_root = output_dir.resolve()
463
+ else:
464
+ # In standalone mode, create a subdirectory with cluster name
465
+ cluster_root = output_dir.resolve() / normalized["cluster"]["name"]
466
+
458
467
  cluster_root.mkdir(parents=True, exist_ok=True)
459
468
 
460
469
  if any(cluster_root.iterdir()) and not force:
@@ -0,0 +1,64 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ pip-wheel-metadata/
20
+ share/python-wheels/
21
+ *.egg-info/
22
+ .installed.cfg
23
+ *.egg
24
+ MANIFEST
25
+
26
+ # Virtual environments
27
+ .venv
28
+ venv/
29
+ ENV/
30
+ env/
31
+
32
+ # IDEs
33
+ .vscode/
34
+ .idea/
35
+ *.swp
36
+ *.swo
37
+ *~
38
+ .DS_Store
39
+
40
+ # Testing
41
+ .pytest_cache/
42
+ .coverage
43
+ htmlcov/
44
+ .tox/
45
+ .hypothesis/
46
+
47
+ # Type checking
48
+ .mypy_cache/
49
+ .dmypy.json
50
+ dmypy.json
51
+
52
+ # Development
53
+ *.log
54
+ .env
55
+ .env.local
56
+ .env.*.local
57
+
58
+ # Docker
59
+ .dockerignore
60
+ Dockerfile
61
+
62
+ # Services
63
+ .csrd-workspace
64
+ .csrd/
@@ -0,0 +1,61 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "{{ cookiecutter.service_name }}"
7
+ version = "0.1.0"
8
+ description = "{{ cookiecutter.service_description }}"
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ license = {text = "MIT"}
12
+ authors = [{name = "Generated by csrd", email = "generated@csrd.local"}]
13
+ keywords = ["fastapi", "microservice", "csrd"]
14
+
15
+ dependencies = [
16
+ "fastapi>=0.110",
17
+ "uvicorn[standard]>=0.27",
18
+ "pydantic[email]>=2.5",
19
+ "csrd-versioning @ git+https://github.com/csrd-api/fastapi-common.git#subdirectory=packages/versioning",
20
+ "csrd-lifespan @ git+https://github.com/csrd-api/fastapi-common.git#subdirectory=packages/lifespan",
21
+ "csrd-repository @ git+https://github.com/csrd-api/fastapi-common.git#subdirectory=packages/repository",
22
+ "csrd-context @ git+https://github.com/csrd-api/fastapi-common.git#subdirectory=packages/context",
23
+ "httpx>=0.25",
24
+ "celery[redis]>=5.3",
25
+ "celery[amqp]>=5.3",
26
+ "redis>=5.0",
27
+ "csrd-logging @ git+https://github.com/csrd-api/fastapi-common.git#subdirectory=packages/logging",
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ dev = [
32
+ "pytest>=7.4",
33
+ "pytest-asyncio>=0.21",
34
+ "mypy>=1.7",
35
+ "ruff>=0.1",
36
+ ]
37
+
38
+ [tool.hatch.build.targets.wheel]
39
+ packages = ["src/app"]
40
+
41
+ [tool.pytest.ini_options]
42
+ asyncio_mode = "auto"
43
+ testpaths = ["tests"]
44
+
45
+ [tool.mypy]
46
+ python_version = "3.12"
47
+ warn_return_any = true
48
+ warn_unused_configs = true
49
+ disallow_untyped_defs = true
50
+ strict = true
51
+
52
+ [tool.ruff]
53
+ target-version = "py312"
54
+ line-length = 100
55
+
56
+ [tool.ruff.lint]
57
+ select = ["E", "F", "W", "I", "N", "UP", "BLE", "FBT", "A", "C4", "PIE", "SIM", "RUF"]
58
+ ignore = ["E501"]
59
+
60
+ [tool.ruff.lint.isort]
61
+ known-first-party = ["app"]
@@ -104,7 +104,7 @@ async def list_{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}(
104
104
  {%- endif %}
105
105
 
106
106
 
107
- @router.get(f"{ITEMS_ROUTE}/{entity_id}")
107
+ @router.get(f"{ITEMS_ROUTE}" + "/{entity_id}")
108
108
  async def get_{{ cookiecutter.model_name | lower }}(
109
109
  entity_id: str,
110
110
  {%- if cookiecutter.has_database %}
@@ -128,7 +128,7 @@ async def get_{{ cookiecutter.model_name | lower }}(
128
128
  return item
129
129
 
130
130
 
131
- @router.put(f"{ITEMS_ROUTE}/{entity_id}")
131
+ @router.put(f"{ITEMS_ROUTE}" + "/{entity_id}")
132
132
  async def update_{{ cookiecutter.model_name | lower }}(
133
133
  entity_id: str,
134
134
  payload: Update{{ cookiecutter.model_name }}Request,
@@ -161,7 +161,7 @@ async def update_{{ cookiecutter.model_name | lower }}(
161
161
  return {"id": entity_id, "name": payload.name, "updated": True}
162
162
 
163
163
 
164
- @router.put(f"{ITEMS_ROUTE}/{entity_id}/upsert")
164
+ @router.put(f"{ITEMS_ROUTE}" + "/{entity_id}/upsert")
165
165
  async def upsert_{{ cookiecutter.model_name | lower }}(
166
166
  entity_id: str,
167
167
  payload: Update{{ cookiecutter.model_name }}Request,
@@ -194,7 +194,7 @@ async def upsert_{{ cookiecutter.model_name | lower }}(
194
194
  return {"id": entity_id, "name": payload.name, "rows_affected": rows}
195
195
 
196
196
 
197
- @router.delete(f"{ITEMS_ROUTE}/{entity_id}")
197
+ @router.delete(f"{ITEMS_ROUTE}" + "/{entity_id}")
198
198
  async def delete_{{ cookiecutter.model_name | lower }}(
199
199
  entity_id: str,
200
200
  {%- if cookiecutter.has_database %}
File without changes