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.
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/PKG-INFO +1 -1
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/pyproject.toml +1 -1
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/__main__.py +22 -5
- csrd_utils-0.3.22/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/test_items.py +182 -0
- csrd_utils-0.3.22/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/unit/test_cache.py +31 -0
- {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
- csrd_utils-0.3.22/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/unit/test_worker_broker.py +26 -0
- csrd_utils-0.3.20/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/test_items.py +0 -55
- csrd_utils-0.3.20/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/unit/test_cache.py +0 -9
- csrd_utils-0.3.20/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/unit/test_worker_broker.py +0 -8
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/.gitignore +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/README.md +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/__init__.py +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/audit.py +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/augmentor.py +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/cluster.py +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/doctor.py +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/caching/conftest.fragment.py +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/caching/docker-compose.fragment.yaml +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/caching/manifest.yaml +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/caching/requirements.fragment +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/caching/settings.fragment.py +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/caching/src/app/cache.py +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/caching/tests/test_cache.py +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/logging/conftest.fragment.py +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/logging/manifest.yaml +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/logging/requirements.fragment +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/logging/settings.fragment.py +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/logging/src/app/middleware/logging.py +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/logging/tests/test_logging_middleware.py +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/metrics/conftest.fragment.py +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/metrics/docker-compose.fragment.yaml +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/metrics/manifest.yaml +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/metrics/requirements.fragment +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/metrics/settings.fragment.py +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/metrics/src/app/middleware/metrics.py +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/metrics/tests/test_metrics_middleware.py +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/tracing/conftest.fragment.py +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/tracing/manifest.yaml +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/tracing/requirements.fragment +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/tracing/settings.fragment.py +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/tracing/src/app/middleware/tracing.py +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/tracing/tests/test_tracing_middleware.py +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/workers/conftest.fragment.py +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/workers/docker-compose.fragment.yaml +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/workers/manifest.yaml +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/workers/requirements.fragment +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/workers/settings.fragment.py +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/workers/src/app/workers/__init__.py +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/workers/src/app/workers/celery_app.py +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/workers/src/app/workers/tasks.py +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/workers/tests/test_workers.py +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/fragments.py +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/generator.py +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/resources.py +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/README.md +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/cookiecutter.json +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/hooks/post_gen_project.py +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/.dockerignore +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/.env.example +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/.gitignore +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/Dockerfile +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/README.md +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/docker-compose.yml +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/pyproject.toml +0 -0
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/requirements.txt +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/__init__.py +0 -0
- {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
- {csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/{{cookiecutter.service_name}}/tests/conftest.py +0 -0
- {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.
|
|
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
|
|
@@ -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
|
-
|
|
482
|
-
|
|
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(
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/caching/conftest.fragment.py
RENAMED
|
File without changes
|
{csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/caching/docker-compose.fragment.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/caching/requirements.fragment
RENAMED
|
File without changes
|
{csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/caching/settings.fragment.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/logging/conftest.fragment.py
RENAMED
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/logging/requirements.fragment
RENAMED
|
File without changes
|
{csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/logging/settings.fragment.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/metrics/conftest.fragment.py
RENAMED
|
File without changes
|
{csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/metrics/docker-compose.fragment.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/metrics/requirements.fragment
RENAMED
|
File without changes
|
{csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/metrics/settings.fragment.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/tracing/conftest.fragment.py
RENAMED
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/tracing/requirements.fragment
RENAMED
|
File without changes
|
{csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/tracing/settings.fragment.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/workers/conftest.fragment.py
RENAMED
|
File without changes
|
{csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/workers/docker-compose.fragment.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/workers/requirements.fragment
RENAMED
|
File without changes
|
{csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/workers/settings.fragment.py
RENAMED
|
File without changes
|
{csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/workers/src/app/workers/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/workers/src/app/workers/tasks.py
RENAMED
|
File without changes
|
{csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/features/workers/tests/test_workers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{csrd_utils-0.3.20 → csrd_utils-0.3.22}/src/csrd_utils/templates/cookiecutter-service/README.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|