csrd-utils 0.3.44__tar.gz → 0.3.46__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.44 → csrd_utils-0.3.46}/PKG-INFO +1 -1
  2. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/pyproject.toml +1 -1
  3. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/__main__.py +4 -8
  4. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/cluster.py +31 -14
  5. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/generator.py +140 -0
  6. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/conftest.py +1 -5
  7. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/test_items.py +4 -4
  8. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/unit/test_cache.py +1 -1
  9. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/unit/test_settings_contracts.py +5 -5
  10. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/unit/test_worker_broker.py +1 -1
  11. csrd_utils-0.3.44/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/pyproject.toml +0 -61
  12. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/.gitignore +0 -0
  13. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/README.md +0 -0
  14. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/__init__.py +0 -0
  15. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/audit.py +0 -0
  16. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/augmentor.py +0 -0
  17. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/doctor.py +0 -0
  18. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/features/caching/conftest.fragment.py +0 -0
  19. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/features/caching/docker-compose.fragment.yaml +0 -0
  20. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/features/caching/manifest.yaml +0 -0
  21. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/features/caching/requirements.fragment +0 -0
  22. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/features/caching/settings.fragment.py +0 -0
  23. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/features/caching/src/app/cache.py +0 -0
  24. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/features/caching/tests/test_cache.py +0 -0
  25. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/features/logging/conftest.fragment.py +0 -0
  26. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/features/logging/manifest.yaml +0 -0
  27. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/features/logging/requirements.fragment +0 -0
  28. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/features/logging/settings.fragment.py +0 -0
  29. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/features/logging/src/app/middleware/logging.py +0 -0
  30. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/features/logging/tests/test_logging_middleware.py +0 -0
  31. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/features/metrics/conftest.fragment.py +0 -0
  32. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/features/metrics/docker-compose.fragment.yaml +0 -0
  33. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/features/metrics/manifest.yaml +0 -0
  34. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/features/metrics/requirements.fragment +0 -0
  35. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/features/metrics/settings.fragment.py +0 -0
  36. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/features/metrics/src/app/middleware/metrics.py +0 -0
  37. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/features/metrics/tests/test_metrics_middleware.py +0 -0
  38. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/features/tracing/conftest.fragment.py +0 -0
  39. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/features/tracing/manifest.yaml +0 -0
  40. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/features/tracing/requirements.fragment +0 -0
  41. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/features/tracing/settings.fragment.py +0 -0
  42. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/features/tracing/src/app/middleware/tracing.py +0 -0
  43. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/features/tracing/tests/test_tracing_middleware.py +0 -0
  44. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/features/workers/conftest.fragment.py +0 -0
  45. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/features/workers/docker-compose.fragment.yaml +0 -0
  46. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/features/workers/manifest.yaml +0 -0
  47. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/features/workers/requirements.fragment +0 -0
  48. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/features/workers/settings.fragment.py +0 -0
  49. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/features/workers/src/app/workers/__init__.py +0 -0
  50. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/features/workers/src/app/workers/celery_app.py +0 -0
  51. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/features/workers/src/app/workers/tasks.py +0 -0
  52. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/features/workers/tests/test_workers.py +0 -0
  53. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/fragments.py +0 -0
  54. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/resources.py +0 -0
  55. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/README.md +0 -0
  56. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/cookiecutter.json +0 -0
  57. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/hooks/post_gen_project.py +0 -0
  58. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/.cookiecutterignore +0 -0
  59. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/.dockerignore +0 -0
  60. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/Dockerfile +0 -0
  61. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/README.md +0 -0
  62. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/__init__.py +0 -0
  63. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/cache.py +0 -0
  64. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/delegates/__init__.py +0 -0
  65. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/delegates/upstreams.py +0 -0
  66. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/dependencies/__init__.py +0 -0
  67. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/dependencies/delegates.py +0 -0
  68. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/dependencies/item_repository.py +0 -0
  69. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/dependencies/worker_broker.py +0 -0
  70. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/docker-compose.yml +0 -0
  71. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/middleware/__init__.py +0 -0
  72. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/middleware/logging.py +0 -0
  73. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/py.typed +0 -0
  74. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/repositories/__init__.py +0 -0
  75. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/repositories/item_repository.py +0 -0
  76. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/requirements.txt +0 -0
  77. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/settings.py +0 -0
  78. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/__init__.py +0 -0
  79. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/acceptance/test_logging_middleware.py +0 -0
  80. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/unit/test_database_settings.py +0 -0
  81. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/tests/unit/test_delegates.py +0 -0
  82. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/views/__init__.py +0 -0
  83. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/views/unversioned/__init__.py +0 -0
  84. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/views/unversioned/health_view.py +0 -0
  85. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/views/unversioned/items_view.py +0 -0
  86. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.__service_name_snake}}/workers/__init__.py +0 -0
  87. {csrd_utils-0.3.44 → csrd_utils-0.3.46}/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.44
3
+ Version: 0.3.46
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.44"
3
+ version = "0.3.46"
4
4
  description = "CLI utilities for csrd service generation and feature augmentation"
5
5
  license = { text = "MIT" }
6
6
  requires-python = ">=3.12"
@@ -784,9 +784,7 @@ def _extract_service(config: ExtractWorkspaceServiceArgs) -> dict[str, object]:
784
784
  _load_workspace_cluster_spec(target_workspace), source_service_spec, target_workspace.name
785
785
  )
786
786
  _save_workspace_cluster_spec(target_workspace, target_spec)
787
- target_cluster_root = generate_cluster(
788
- target_spec, target_workspace, force=True, workspace_mode=True
789
- )
787
+ target_cluster_root = generate_cluster(target_spec, target_workspace, force=True)
790
788
 
791
789
  source_cluster_root = None
792
790
  if mode == "move":
@@ -795,9 +793,7 @@ def _extract_service(config: ExtractWorkspaceServiceArgs) -> dict[str, object]:
795
793
  _load_workspace_cluster_spec(source_workspace), service_root.name, source_workspace.name
796
794
  )
797
795
  _save_workspace_cluster_spec(source_workspace, source_spec)
798
- source_cluster_root = generate_cluster(
799
- source_spec, source_workspace, force=True, workspace_mode=True
800
- )
796
+ source_cluster_root = generate_cluster(source_spec, source_workspace, force=True)
801
797
 
802
798
  return {
803
799
  "ok": True,
@@ -897,7 +893,7 @@ def _maybe_update_workspace_cluster(
897
893
  service_config,
898
894
  overwrite=service_config["overwrite_if_exists"],
899
895
  )
900
- return generate_cluster(updated_spec, workspace_root, force=True, workspace_mode=True)
896
+ return generate_cluster(updated_spec, workspace_root, force=True)
901
897
 
902
898
  service_roots = _workspace_service_roots(workspace_root)
903
899
  generated_name = generated_service.name
@@ -914,7 +910,7 @@ def _maybe_update_workspace_cluster(
914
910
  }
915
911
  normalized = normalize_cluster_spec(bootstrap_spec)
916
912
  _save_workspace_cluster_spec(workspace_root, normalized)
917
- return generate_cluster(normalized, workspace_root, force=True, workspace_mode=True)
913
+ return generate_cluster(normalized, workspace_root, force=True)
918
914
 
919
915
 
920
916
  def _build_parser() -> argparse.ArgumentParser:
@@ -426,6 +426,21 @@ def _build_readme(spec: ClusterSpec) -> str:
426
426
  lines.append(f"- `{service['name']}` ({service['type']}) on port `{service['port']}`")
427
427
  lines.extend(
428
428
  [
429
+ "",
430
+ "## Tests",
431
+ "",
432
+ "From the workspace root, run all tests:",
433
+ "",
434
+ "```bash",
435
+ "pytest",
436
+ "```",
437
+ "",
438
+ "Run tests for a specific service:",
439
+ "",
440
+ "```bash",
441
+ "pytest tests/unit/{service_name}",
442
+ "pytest tests/acceptance/{service_name}",
443
+ "```",
429
444
  "",
430
445
  "## Run",
431
446
  "",
@@ -456,23 +471,20 @@ def _build_smoke(spec: ClusterSpec) -> str:
456
471
  return "\n".join(lines) + "\n"
457
472
 
458
473
 
459
- def generate_cluster(
460
- spec: ClusterSpec, output_dir: Path, force: bool = False, workspace_mode: bool = False
461
- ) -> Path:
462
- normalized = normalize_cluster_spec(spec)
463
-
464
- if workspace_mode:
465
- # In workspace mode, write files directly to output_dir (workspace root)
466
- cluster_root = output_dir.resolve()
467
- else:
468
- # In standalone mode, create a subdirectory with cluster name
469
- cluster_root = output_dir.resolve() / normalized["cluster"]["name"]
474
+ def generate_cluster(spec: ClusterSpec, output_dir: Path, force: bool = False) -> Path:
475
+ """Generate cluster scaffold in workspace mode.
470
476
 
477
+ Always operates at workspace root (no standalone mode).
478
+ """
479
+ normalized = normalize_cluster_spec(spec)
480
+ cluster_root = output_dir.resolve()
471
481
  cluster_root.mkdir(parents=True, exist_ok=True)
472
482
 
473
- if any(cluster_root.iterdir()) and not force:
483
+ # In workspace mode, check if we'd overwrite existing cluster artifacts
484
+ compose_file = cluster_root / "docker-compose.yml"
485
+ if compose_file.exists() and not force:
474
486
  raise FileExistsError(
475
- f"Target directory already exists and is not empty: {cluster_root}. Use --force to overwrite."
487
+ f"Cluster files already exist in {cluster_root}. Use --force to overwrite."
476
488
  )
477
489
 
478
490
  compose = render_compose(normalized)
@@ -480,7 +492,12 @@ def generate_cluster(
480
492
  yaml.safe_dump(compose, sort_keys=False), encoding="utf-8"
481
493
  )
482
494
  (cluster_root / ".env.example").write_text(_build_env(normalized), encoding="utf-8")
483
- (cluster_root / "README.md").write_text(_build_readme(normalized), encoding="utf-8")
495
+
496
+ # In workspace mode, preserve existing README (workspace-level, not cluster-specific)
497
+ readme_path = cluster_root / "README.md"
498
+ if not readme_path.exists():
499
+ readme_path.write_text(_build_readme(normalized), encoding="utf-8")
500
+
484
501
  smoke_path = cluster_root / "smoke.sh"
485
502
  smoke_path.write_text(_build_smoke(normalized), encoding="utf-8")
486
503
  smoke_path.chmod(0o755)
@@ -9,6 +9,111 @@ from cookiecutter.main import cookiecutter
9
9
 
10
10
  from .resources import templates_path
11
11
 
12
+ _WORKSPACE_PYPROJECT_BASE = """\
13
+ [build-system]
14
+ requires = ["hatchling"]
15
+ build-backend = "hatchling.build"
16
+
17
+ [project]
18
+ name = "workspace"
19
+ version = "0.1.0"
20
+ description = "Multi-service workspace"
21
+ readme = "README.md"
22
+ requires-python = ">=3.12"
23
+ license = {text = "MIT"}
24
+
25
+ dependencies = [
26
+ "fastapi>=0.110",
27
+ "uvicorn[standard]>=0.27",
28
+ "pydantic[email]>=2.5",
29
+ "csrd-versioning @ git+https://github.com/csrd-api/fastapi-common.git#subdirectory=packages/versioning",
30
+ "csrd-lifespan @ git+https://github.com/csrd-api/fastapi-common.git#subdirectory=packages/lifespan",
31
+ "csrd-repository @ git+https://github.com/csrd-api/fastapi-common.git#subdirectory=packages/repository",
32
+ "csrd-context @ git+https://github.com/csrd-api/fastapi-common.git#subdirectory=packages/context",
33
+ "httpx>=0.25",
34
+ "celery[redis]>=5.3",
35
+ "celery[amqp]>=5.3",
36
+ "redis>=5.0",
37
+ "csrd-logging @ git+https://github.com/csrd-api/fastapi-common.git#subdirectory=packages/logging",
38
+ ]
39
+
40
+ [project.optional-dependencies]
41
+ dev = [
42
+ "pytest>=7.4",
43
+ "pytest-asyncio>=0.21",
44
+ "mypy>=1.7",
45
+ "ruff>=0.1",
46
+ ]
47
+
48
+ [tool.pytest.ini_options]
49
+ asyncio_mode = "auto"
50
+ testpaths = ["tests"]
51
+ pythonpath = ["src"]
52
+
53
+ [tool.mypy]
54
+ python_version = "3.12"
55
+ warn_return_any = true
56
+ warn_unused_configs = true
57
+ disallow_untyped_defs = true
58
+ strict = true
59
+
60
+ [tool.ruff]
61
+ target-version = "py312"
62
+ line-length = 100
63
+
64
+ [tool.ruff.lint]
65
+ select = ["E", "F", "W", "I", "N", "UP", "BLE", "FBT", "A", "C4", "PIE", "SIM", "RUF"]
66
+ ignore = ["E501"]
67
+
68
+ [tool.ruff.lint.isort]
69
+ known-first-party = [""]
70
+ """
71
+
72
+ _WORKSPACE_README = """\
73
+ # Workspace
74
+
75
+ Generated workspace with one or more microservices.
76
+
77
+ ## Run tests
78
+
79
+ From the workspace root, run all tests:
80
+
81
+ ```bash
82
+ pytest
83
+ ```
84
+
85
+ Run tests for a specific service:
86
+
87
+ ```bash
88
+ pytest tests/unit/{service_name}
89
+ pytest tests/acceptance/{service_name}
90
+ ```
91
+
92
+ Run tests in watch mode (requires `pytest-watch`):
93
+
94
+ ```bash
95
+ ptw
96
+ ```
97
+
98
+ ## Run services
99
+
100
+ See individual service README files under `src/{service_name}/README.md` for instructions on running each service.
101
+
102
+ ## Docker Compose
103
+
104
+ Run all services and infrastructure:
105
+
106
+ ```bash
107
+ docker compose up --build
108
+ ```
109
+
110
+ Check service health:
111
+
112
+ ```bash
113
+ ./smoke.sh
114
+ ```
115
+ """
116
+
12
117
 
13
118
  def _build_context(
14
119
  *,
@@ -123,6 +228,12 @@ def generate_service(
123
228
  _move_docker_files_to_workspace_root(generated_path, output_root, name)
124
229
  _move_tests_to_workspace_root(generated_path, output_root, name)
125
230
 
231
+ # Ensure workspace root has a pyproject.toml with pytest config.
232
+ _write_workspace_pyproject(output_root)
233
+
234
+ # Ensure workspace root has a README with test running instructions.
235
+ _write_workspace_readme(output_root)
236
+
126
237
  return generated_path
127
238
 
128
239
 
@@ -193,3 +304,32 @@ def _move_tests_to_workspace_root(
193
304
  shutil.copy2(src_file, acceptance_dst / filename)
194
305
 
195
306
  shutil.rmtree(tests_src, ignore_errors=True)
307
+
308
+
309
+ def _write_workspace_pyproject(workspace_root: Path) -> None:
310
+ """Write workspace-level pyproject.toml with full build/project/tool configuration.
311
+
312
+ Creates complete pyproject.toml on first generation. On subsequent generations,
313
+ only writes if the file doesn't exist or lacks [project] section (user customization).
314
+ """
315
+ dst = workspace_root / "pyproject.toml"
316
+ if dst.exists():
317
+ existing = dst.read_text(encoding="utf-8")
318
+ # If user already has a [project] section, assume they customized it
319
+ if "[project]" in existing:
320
+ return
321
+ # Otherwise write the full base config
322
+ dst.write_text(_WORKSPACE_PYPROJECT_BASE, encoding="utf-8")
323
+ else:
324
+ dst.write_text(_WORKSPACE_PYPROJECT_BASE, encoding="utf-8")
325
+
326
+
327
+ def _write_workspace_readme(workspace_root: Path) -> None:
328
+ """Write workspace-level README.md with test running and setup instructions.
329
+
330
+ If the file does not exist it is created from scratch. If it already exists
331
+ it is left untouched (the user may have customised it).
332
+ """
333
+ dst = workspace_root / "README.md"
334
+ if not dst.exists():
335
+ dst.write_text(_WORKSPACE_README, encoding="utf-8")
@@ -1,15 +1,11 @@
1
1
  """Shared test fixtures for {{ cookiecutter.service_name }}."""
2
2
 
3
3
  import os
4
- import sys
5
- from pathlib import Path
6
4
 
7
5
  import pytest
8
6
  from fastapi.testclient import TestClient
9
7
 
10
- sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
11
-
12
- from app import build_app
8
+ from {{cookiecutter.__service_name_snake}} import build_app
13
9
 
14
10
  HAS_DATABASE = {{"True" if cookiecutter.has_database else "False"}}
15
11
  DB_KIND = "{{ cookiecutter.database }}"
@@ -111,7 +111,7 @@ def test_delegate_create_and_list(monkeypatch, client):
111
111
  return {"id": "d1", "name": payload["name"], "created_at": "2026-01-01T00:00:00Z"}
112
112
  return {"{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}": [{"id": "d1", "name": "X", "created_at": "t"}]}
113
113
 
114
- monkeypatch.setattr("app.views.unversioned.items_view._delegate_request", fake_delegate_request)
114
+ monkeypatch.setattr("{{cookiecutter.__service_name_snake}}.views.unversioned.items_view._delegate_request", fake_delegate_request)
115
115
 
116
116
  created = client.post(
117
117
  "/api/{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}", json={"name": "X"}
@@ -130,7 +130,7 @@ def test_delegate_error_mapping(monkeypatch, client):
130
130
  async def fake_delegate_request(_method: str, _path: str, payload=None):
131
131
  raise HTTPException(status_code=404, detail="not found")
132
132
 
133
- monkeypatch.setattr("app.views.unversioned.items_view._delegate_request", fake_delegate_request)
133
+ monkeypatch.setattr("{{cookiecutter.__service_name_snake}}.views.unversioned.items_view._delegate_request", fake_delegate_request)
134
134
  response = client.get("/api/{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}/missing")
135
135
  assert response.status_code == 404
136
136
 
@@ -147,7 +147,7 @@ def test_delegate_get_update_upsert_delete(monkeypatch, client):
147
147
  return {"id": "d2", "deleted": True}
148
148
  return {}
149
149
 
150
- monkeypatch.setattr("app.views.unversioned.items_view._delegate_request", fake_delegate_request)
150
+ monkeypatch.setattr("{{cookiecutter.__service_name_snake}}.views.unversioned.items_view._delegate_request", fake_delegate_request)
151
151
 
152
152
  fetched = client.get("/api/{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}/d2")
153
153
  assert fetched.status_code == 200
@@ -176,7 +176,7 @@ def test_delegate_list_invalid_payload_returns_502(monkeypatch, client):
176
176
  async def fake_delegate_request(_method: str, _path: str, payload=None):
177
177
  return {"{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}": "bad"}
178
178
 
179
- monkeypatch.setattr("app.views.unversioned.items_view._delegate_request", fake_delegate_request)
179
+ monkeypatch.setattr("{{cookiecutter.__service_name_snake}}.views.unversioned.items_view._delegate_request", fake_delegate_request)
180
180
  response = client.get("/api/{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}")
181
181
  assert response.status_code == 502
182
182
  {%- endif %}
@@ -17,7 +17,7 @@ def test_cache_client_uses_decode_responses() -> None:
17
17
 
18
18
 
19
19
  def test_cache_client_uses_runtime_cache_url() -> None:
20
- settings_mod = importlib.import_module("app.settings")
20
+ settings_mod = importlib.import_module("{{cookiecutter.__service_name_snake}}.settings")
21
21
  original = settings_mod.settings.cache_url
22
22
  try:
23
23
  settings_mod.settings.cache_url = "redis://cache-host:6381/2"
@@ -73,11 +73,11 @@ def test_validate_db_credentials_rejects_weak_defaults(
73
73
 
74
74
  def test_app_package_init_modules_expose_tuple_all() -> None:
75
75
  for module_name in [
76
- "app.delegates",
77
- "app.dependencies",
78
- "app.middleware",
79
- "app.repositories",
80
- "app.workers",
76
+ "{{cookiecutter.__service_name_snake}}.delegates",
77
+ "{{cookiecutter.__service_name_snake}}.dependencies",
78
+ "{{cookiecutter.__service_name_snake}}.middleware",
79
+ "{{cookiecutter.__service_name_snake}}.repositories",
80
+ "{{cookiecutter.__service_name_snake}}.workers",
81
81
  ]:
82
82
  module = importlib.import_module(module_name)
83
83
  exports = getattr(module, "__all__", ())
@@ -16,7 +16,7 @@ def test_ping_task_returns_pong() -> None:
16
16
 
17
17
 
18
18
  def test_worker_broker_url_reflects_runtime_settings() -> None:
19
- settings_mod = importlib.import_module("app.settings")
19
+ settings_mod = importlib.import_module("{{cookiecutter.__service_name_snake}}.settings")
20
20
  original = settings_mod.settings.worker_broker_url
21
21
  try:
22
22
  settings_mod.settings.worker_broker_url = "amqp://runtime-test"
@@ -1,61 +0,0 @@
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 = ["{{cookiecutter.__service_name_snake}}"]
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 = ["{{cookiecutter.__service_name_snake}}"]
File without changes
File without changes