csrd-utils 0.3.20__tar.gz → 0.3.22__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 (90) hide show
  1. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/PKG-INFO +1 -1
  2. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/pyproject.toml +1 -1
  3. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/__main__.py +22 -5
  4. csrd_utils-0.3.22/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/test_items.py +182 -0
  5. csrd_utils-0.3.22/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/unit/test_cache.py +31 -0
  6. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/unit/test_delegates.py +9 -0
  7. csrd_utils-0.3.22/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/unit/test_worker_broker.py +26 -0
  8. csrd_utils-0.3.20/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/test_items.py +0 -55
  9. csrd_utils-0.3.20/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/unit/test_cache.py +0 -9
  10. csrd_utils-0.3.20/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/unit/test_worker_broker.py +0 -8
  11. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/.gitignore +0 -0
  12. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/README.md +0 -0
  13. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/__init__.py +0 -0
  14. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/audit.py +0 -0
  15. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/augmentor.py +0 -0
  16. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/cluster.py +0 -0
  17. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/doctor.py +0 -0
  18. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/caching/conftest.fragment.py +0 -0
  19. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/caching/docker-compose.fragment.yaml +0 -0
  20. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/caching/manifest.yaml +0 -0
  21. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/caching/requirements.fragment +0 -0
  22. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/caching/settings.fragment.py +0 -0
  23. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/caching/src/app/cache.py +0 -0
  24. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/caching/tests/test_cache.py +0 -0
  25. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/logging/conftest.fragment.py +0 -0
  26. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/logging/manifest.yaml +0 -0
  27. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/logging/requirements.fragment +0 -0
  28. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/logging/settings.fragment.py +0 -0
  29. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/logging/src/app/middleware/logging.py +0 -0
  30. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/logging/tests/test_logging_middleware.py +0 -0
  31. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/metrics/conftest.fragment.py +0 -0
  32. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/metrics/docker-compose.fragment.yaml +0 -0
  33. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/metrics/manifest.yaml +0 -0
  34. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/metrics/requirements.fragment +0 -0
  35. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/metrics/settings.fragment.py +0 -0
  36. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/metrics/src/app/middleware/metrics.py +0 -0
  37. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/metrics/tests/test_metrics_middleware.py +0 -0
  38. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/tracing/conftest.fragment.py +0 -0
  39. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/tracing/manifest.yaml +0 -0
  40. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/tracing/requirements.fragment +0 -0
  41. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/tracing/settings.fragment.py +0 -0
  42. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/tracing/src/app/middleware/tracing.py +0 -0
  43. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/tracing/tests/test_tracing_middleware.py +0 -0
  44. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/workers/conftest.fragment.py +0 -0
  45. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/workers/docker-compose.fragment.yaml +0 -0
  46. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/workers/manifest.yaml +0 -0
  47. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/workers/requirements.fragment +0 -0
  48. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/workers/settings.fragment.py +0 -0
  49. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/workers/src/app/workers/__init__.py +0 -0
  50. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/workers/src/app/workers/celery_app.py +0 -0
  51. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/workers/src/app/workers/tasks.py +0 -0
  52. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/workers/tests/test_workers.py +0 -0
  53. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/fragments.py +0 -0
  54. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/generator.py +0 -0
  55. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/resources.py +0 -0
  56. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/README.md +0 -0
  57. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/cookiecutter.json +0 -0
  58. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/hooks/post_gen_project.py +0 -0
  59. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/.dockerignore +0 -0
  60. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/.env.example +0 -0
  61. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/.gitignore +0 -0
  62. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/Dockerfile +0 -0
  63. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/README.md +0 -0
  64. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/docker-compose.yml +0 -0
  65. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/pyproject.toml +0 -0
  66. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/requirements.txt +0 -0
  67. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/__init__.py +0 -0
  68. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/cache.py +0 -0
  69. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/delegates/__init__.py +0 -0
  70. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/delegates/upstreams.py +0 -0
  71. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/dependencies/__init__.py +0 -0
  72. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/dependencies/delegates.py +0 -0
  73. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/dependencies/item_repository.py +0 -0
  74. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/dependencies/worker_broker.py +0 -0
  75. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/middleware/__init__.py +0 -0
  76. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/middleware/logging.py +0 -0
  77. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/py.typed +0 -0
  78. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/repositories/__init__.py +0 -0
  79. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/repositories/item_repository.py +0 -0
  80. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/settings.py +0 -0
  81. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/views/__init__.py +0 -0
  82. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/views/unversioned/__init__.py +0 -0
  83. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/views/unversioned/health_view.py +0 -0
  84. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/views/unversioned/items_view.py +0 -0
  85. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/workers/__init__.py +0 -0
  86. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/src/app/workers/tasks.py +0 -0
  87. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/__init__.py +0 -0
  88. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/acceptance/test_logging_middleware.py +0 -0
  89. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/conftest.py +0 -0
  90. {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/unit/test_database_settings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: csrd-utils
3
- Version: 0.3.20
3
+ Version: 0.3.22
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.20"
3
+ version = "0.3.22"
4
4
  description = "CLI utilities for csrd service generation and feature augmentation"
5
5
  license = { text = "MIT" }
6
6
  requires-python = ">=3.12"
@@ -475,14 +475,27 @@ def _service_config_to_cluster_service(config: GenerateServiceArgs) -> ServiceSp
475
475
 
476
476
 
477
477
  def _append_service_to_workspace_cluster(
478
- workspace_root: Path, config: GenerateServiceArgs
478
+ workspace_root: Path, config: GenerateServiceArgs, overwrite: bool = False
479
479
  ) -> ClusterSpec:
480
480
  spec = _load_workspace_cluster_spec(workspace_root)
481
- service_names = {str(service.get("name", "")).strip() for service in spec.get("services", [])}
482
- if config["name"] in service_names:
481
+ new_service = _service_config_to_cluster_service(config)
482
+ services = spec.get("services", [])
483
+ existing_idx = next(
484
+ (
485
+ idx
486
+ for idx, service in enumerate(services)
487
+ if str(service.get("name", "")).strip() == config["name"]
488
+ ),
489
+ None,
490
+ )
491
+
492
+ if existing_idx is None:
493
+ services.append(new_service)
494
+ elif overwrite:
495
+ services[existing_idx] = new_service
496
+ else:
483
497
  raise ValueError(f"Service '{config['name']}' already exists in workspace cluster spec")
484
498
 
485
- spec["services"].append(_service_config_to_cluster_service(config))
486
499
  normalized = normalize_cluster_spec(spec)
487
500
  _save_workspace_cluster_spec(workspace_root, normalized)
488
501
  return normalized
@@ -614,7 +627,11 @@ def _maybe_update_workspace_cluster(
614
627
  ) -> Path | None:
615
628
  spec_path = _workspace_cluster_spec_path(workspace_root)
616
629
  if spec_path.is_file():
617
- updated_spec = _append_service_to_workspace_cluster(workspace_root, service_config)
630
+ updated_spec = _append_service_to_workspace_cluster(
631
+ workspace_root,
632
+ service_config,
633
+ overwrite=service_config["overwrite_if_exists"],
634
+ )
618
635
  return generate_cluster(updated_spec, workspace_root, force=True, workspace_mode=True)
619
636
 
620
637
  service_roots = _workspace_service_roots(workspace_root)
@@ -0,0 +1,182 @@
1
+ """Tests for {{ cookiecutter.service_name }} starter endpoints."""
2
+
3
+ from fastapi import HTTPException
4
+
5
+
6
+ def test_health(client):
7
+ response = client.get("/health")
8
+ assert response.status_code == 200
9
+ assert response.json() == {"status": "ok", "service": "{{ cookiecutter.service_name }}"}
10
+
11
+
12
+ {%- if cookiecutter.has_database or not cookiecutter.has_delegates %}
13
+ def test_create_item_returns_data(client):
14
+ create_response = client.post(
15
+ "/api/{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}", json={"name": "Test Item"}
16
+ )
17
+ assert create_response.status_code == 200
18
+ item = create_response.json()
19
+ assert item["name"] == "Test Item"
20
+ assert "id" in item
21
+ assert "created_at" in item
22
+
23
+
24
+ def test_list_items(client):
25
+ client.post("/api/{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}", json={"name": "Item 1"})
26
+ client.post("/api/{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}", json={"name": "Item 2"})
27
+
28
+ response = client.get("/api/{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}")
29
+ assert response.status_code == 200
30
+ items = response.json()["{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}"]
31
+ names = {item["name"] for item in items}
32
+ assert "Item 1" in names
33
+ assert "Item 2" in names
34
+
35
+
36
+ def test_get_item_missing_returns_404(client):
37
+ response = client.get("/api/{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}/missing-id")
38
+ assert response.status_code == 404
39
+
40
+
41
+ def test_get_item_returns_created_item(client):
42
+ created = client.post(
43
+ "/api/{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}", json={"name": "Lookup"}
44
+ ).json()
45
+ response = client.get(
46
+ f"/api/{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}/{created['id']}"
47
+ )
48
+ assert response.status_code == 200
49
+ assert response.json()["id"] == created["id"]
50
+ assert response.json()["name"] == "Lookup"
51
+
52
+
53
+ def test_update_and_delete_item(client):
54
+ created = client.post(
55
+ "/api/{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}", json={"name": "Original"}
56
+ ).json()
57
+ item_id = created["id"]
58
+
59
+ updated = client.put(
60
+ f"/api/{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}/{item_id}",
61
+ json={"name": "Updated"},
62
+ )
63
+ assert updated.status_code == 200
64
+ assert updated.json()["updated"] is True
65
+
66
+ deleted = client.delete(f"/api/{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}/{item_id}")
67
+ assert deleted.status_code == 200
68
+ assert deleted.json()["deleted"] is True
69
+
70
+
71
+ def test_upsert_item(client):
72
+ item_id = "custom-upsert-id"
73
+ first = client.put(
74
+ f"/api/{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}/{item_id}/upsert",
75
+ json={"name": "Created"},
76
+ )
77
+ assert first.status_code == 200
78
+ assert first.json()["rows_affected"] == 1
79
+
80
+ second = client.put(
81
+ f"/api/{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}/{item_id}/upsert",
82
+ json={"name": "Updated"},
83
+ )
84
+ assert second.status_code == 200
85
+ assert second.json()["rows_affected"] == 1
86
+
87
+
88
+ def test_update_missing_returns_404(client):
89
+ response = client.put(
90
+ "/api/{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}/missing-id",
91
+ json={"name": "Nope"},
92
+ )
93
+ assert response.status_code == 404
94
+
95
+
96
+ def test_delete_missing_returns_404(client):
97
+ response = client.delete(
98
+ "/api/{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}/missing-id"
99
+ )
100
+ assert response.status_code == 404
101
+ {%- endif %}
102
+
103
+
104
+ {%- if cookiecutter.has_delegates and not cookiecutter.has_database %}
105
+ def test_delegate_create_and_list(monkeypatch, client):
106
+ calls: list[tuple[str, str]] = []
107
+
108
+ async def fake_delegate_request(method: str, path: str, payload=None):
109
+ calls.append((method, path))
110
+ if method == "POST":
111
+ return {"id": "d1", "name": payload["name"], "created_at": "2026-01-01T00:00:00Z"}
112
+ return {"{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}": [{"id": "d1", "name": "X", "created_at": "t"}]}
113
+
114
+ monkeypatch.setattr("app.views.unversioned.items_view._delegate_request", fake_delegate_request)
115
+
116
+ created = client.post(
117
+ "/api/{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}", json={"name": "X"}
118
+ )
119
+ assert created.status_code == 200
120
+ assert created.json()["id"] == "d1"
121
+
122
+ listed = client.get("/api/{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}")
123
+ assert listed.status_code == 200
124
+ assert len(listed.json()["{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}"]) == 1
125
+ assert calls[0][0] == "POST"
126
+ assert calls[1][0] == "GET"
127
+
128
+
129
+ def test_delegate_error_mapping(monkeypatch, client):
130
+ async def fake_delegate_request(_method: str, _path: str, payload=None):
131
+ raise HTTPException(status_code=404, detail="not found")
132
+
133
+ monkeypatch.setattr("app.views.unversioned.items_view._delegate_request", fake_delegate_request)
134
+ response = client.get("/api/{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}/missing")
135
+ assert response.status_code == 404
136
+
137
+
138
+ def test_delegate_get_update_upsert_delete(monkeypatch, client):
139
+ async def fake_delegate_request(method: str, path: str, payload=None):
140
+ if method == "GET":
141
+ return {"id": "d2", "name": "From delegate", "created_at": "2026-01-01T00:00:00Z"}
142
+ if method == "PUT" and path.endswith("/upsert"):
143
+ return {"id": "d2", "name": payload["name"], "rows_affected": 1}
144
+ if method == "PUT":
145
+ return {"id": "d2", "name": payload["name"], "updated": True}
146
+ if method == "DELETE":
147
+ return {"id": "d2", "deleted": True}
148
+ return {}
149
+
150
+ monkeypatch.setattr("app.views.unversioned.items_view._delegate_request", fake_delegate_request)
151
+
152
+ fetched = client.get("/api/{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}/d2")
153
+ assert fetched.status_code == 200
154
+ assert fetched.json()["id"] == "d2"
155
+
156
+ updated = client.put(
157
+ "/api/{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}/d2",
158
+ json={"name": "Updated"},
159
+ )
160
+ assert updated.status_code == 200
161
+ assert updated.json()["updated"] is True
162
+
163
+ upserted = client.put(
164
+ "/api/{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}/d2/upsert",
165
+ json={"name": "Upserted"},
166
+ )
167
+ assert upserted.status_code == 200
168
+ assert upserted.json()["rows_affected"] == 1
169
+
170
+ deleted = client.delete("/api/{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}/d2")
171
+ assert deleted.status_code == 200
172
+ assert deleted.json()["deleted"] is True
173
+
174
+
175
+ def test_delegate_list_invalid_payload_returns_502(monkeypatch, client):
176
+ async def fake_delegate_request(_method: str, _path: str, payload=None):
177
+ return {"{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}": "bad"}
178
+
179
+ monkeypatch.setattr("app.views.unversioned.items_view._delegate_request", fake_delegate_request)
180
+ response = client.get("/api/{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}")
181
+ assert response.status_code == 502
182
+ {%- endif %}
@@ -0,0 +1,31 @@
1
+ """Unit tests for cache dependency wiring."""
2
+
3
+ import importlib
4
+
5
+ from app.cache import build_cache_client
6
+
7
+
8
+ def test_cache_client_uses_configured_host() -> None:
9
+ cache_client = build_cache_client()
10
+ host = cache_client.connection_pool.connection_kwargs.get("host")
11
+ assert host in {"localhost", "redis"}
12
+
13
+
14
+ def test_cache_client_uses_decode_responses() -> None:
15
+ cache_client = build_cache_client()
16
+ assert cache_client.connection_pool.connection_kwargs.get("decode_responses") is True
17
+
18
+
19
+ def test_cache_client_uses_runtime_cache_url() -> None:
20
+ settings_mod = importlib.import_module("app.settings")
21
+ original = settings_mod.settings.cache_url
22
+ try:
23
+ settings_mod.settings.cache_url = "redis://cache-host:6381/2"
24
+ cache_client = build_cache_client()
25
+ kwargs = cache_client.connection_pool.connection_kwargs
26
+ assert kwargs.get("host") == "cache-host"
27
+ assert kwargs.get("port") == 6381
28
+ assert kwargs.get("db") == 2
29
+ finally:
30
+ settings_mod.settings.cache_url = original
31
+
@@ -3,6 +3,7 @@
3
3
  import asyncio
4
4
 
5
5
  from app.dependencies.delegates import upstream_delegate_factory
6
+ from app.delegates.upstreams import configured_upstreams
6
7
 
7
8
 
8
9
  async def _collect_upstreams() -> list[str]:
@@ -15,3 +16,11 @@ def test_upstream_delegate_factory_returns_urls() -> None:
15
16
  upstreams = asyncio.run(_collect_upstreams())
16
17
  assert upstreams
17
18
  assert all(url.startswith("http") for url in upstreams)
19
+
20
+
21
+ def test_configured_upstreams_strips_and_filters() -> None:
22
+ upstreams = configured_upstreams()
23
+ assert upstreams
24
+ assert all(entry == entry.strip() for entry in upstreams)
25
+ assert all(entry for entry in upstreams)
26
+
@@ -0,0 +1,26 @@
1
+ """Unit tests for worker broker dependency wiring."""
2
+
3
+ import importlib
4
+
5
+ from app.dependencies.worker_broker import get_worker_broker_url
6
+ from app.workers.tasks import ping_task
7
+
8
+
9
+ def test_worker_broker_url_uses_expected_scheme() -> None:
10
+ broker_url = get_worker_broker_url()
11
+ assert broker_url.startswith(("redis://", "amqp://"))
12
+
13
+
14
+ def test_ping_task_returns_pong() -> None:
15
+ assert ping_task() == "pong"
16
+
17
+
18
+ def test_worker_broker_url_reflects_runtime_settings() -> None:
19
+ settings_mod = importlib.import_module("app.settings")
20
+ original = settings_mod.settings.worker_broker_url
21
+ try:
22
+ settings_mod.settings.worker_broker_url = "amqp://runtime-test"
23
+ assert get_worker_broker_url() == "amqp://runtime-test"
24
+ finally:
25
+ settings_mod.settings.worker_broker_url = original
26
+
@@ -1,55 +0,0 @@
1
- """Tests for {{ cookiecutter.service_name }} starter endpoints."""
2
-
3
-
4
- def test_health(client):
5
- response = client.get("/health")
6
- assert response.status_code == 200
7
- assert response.json() == {"status": "ok", "service": "{{ cookiecutter.service_name }}"}
8
-
9
-
10
- {%- if cookiecutter.has_database or not cookiecutter.has_delegates %}
11
- def test_create_item_returns_data(client):
12
- create_response = client.post(
13
- "/api/{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}", json={"name": "Test Item"}
14
- )
15
- assert create_response.status_code == 200
16
- item = create_response.json()
17
- assert item["name"] == "Test Item"
18
- assert "id" in item
19
- assert "created_at" in item
20
-
21
-
22
- def test_list_items(client):
23
- client.post("/api/{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}", json={"name": "Item 1"})
24
- client.post("/api/{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}", json={"name": "Item 2"})
25
-
26
- response = client.get("/api/{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}")
27
- assert response.status_code == 200
28
- items = response.json()["{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}"]
29
- names = {item["name"] for item in items}
30
- assert "Item 1" in names
31
- assert "Item 2" in names
32
-
33
-
34
- def test_get_item_missing_returns_404(client):
35
- response = client.get("/api/{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}/missing-id")
36
- assert response.status_code == 404
37
-
38
-
39
- def test_update_and_delete_item(client):
40
- created = client.post(
41
- "/api/{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}", json={"name": "Original"}
42
- ).json()
43
- item_id = created["id"]
44
-
45
- updated = client.put(
46
- f"/api/{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}/{item_id}",
47
- json={"name": "Updated"},
48
- )
49
- assert updated.status_code == 200
50
- assert updated.json()["updated"] is True
51
-
52
- deleted = client.delete(f"/api/{{ cookiecutter.model_name_plural | lower | replace(' ', '_') }}/{item_id}")
53
- assert deleted.status_code == 200
54
- assert deleted.json()["deleted"] is True
55
- {%- endif %}
@@ -1,9 +0,0 @@
1
- """Unit tests for cache dependency wiring."""
2
-
3
- from app.cache import build_cache_client
4
-
5
-
6
- def test_cache_client_uses_configured_host() -> None:
7
- cache_client = build_cache_client()
8
- host = cache_client.connection_pool.connection_kwargs.get("host")
9
- assert host in {"localhost", "redis"}
@@ -1,8 +0,0 @@
1
- """Unit tests for worker broker dependency wiring."""
2
-
3
- from app.dependencies.worker_broker import get_worker_broker_url
4
-
5
-
6
- def test_worker_broker_url_uses_expected_scheme() -> None:
7
- broker_url = get_worker_broker_url()
8
- assert broker_url.startswith(("redis://", "amqp://"))
File without changes
File without changes