diff --git a/templates/django-api/AGENTS.md b/templates/django-api/AGENTS.md new file mode 100644 index 0000000..6a3e027 --- /dev/null +++ b/templates/django-api/AGENTS.md @@ -0,0 +1,30 @@ +# AGENTS.md – AI Interaction & Execution Guide (Humans: see CONTRIBUTING.md & docs/) + +## Authoritative references + +| Topic | Source | +|-------|--------| +| Architecture | docs/PROJECT_STRUCTURE.md | +| API | docs/API.md | +| Testing | docs/TESTING_GUIDE.md | +| Deployment | docs/DEPLOYMENT.md | +| Configuration | docs/CONFIGURATION.md | +| Typing | docs/TYPING.md | + +## Key commands + +| Command | Purpose | +|---------|---------| +| `uv run python manage.py runserver` | Dev server | +| `uv run gunicorn config.wsgi:application` | Prod-style WSGI | +| `uv run ruff check .` | Lint | +| `uv run pytest` | Tests | +| `uv run mypy apps config` | Types | + +## Feature work protocol + +1. Copy `docs/examples/feature-app/` → `apps//`. +2. Register in `INSTALLED_APPS`. +3. Wire urls under `API_PREFIX`. +4. Add tests; keep response envelope `{data,error,meta}`. +5. Update OpenAPI-facing serializers when shapes change (`/api/.../docs/`). diff --git a/templates/django-api/CONTRIBUTING.md b/templates/django-api/CONTRIBUTING.md new file mode 100644 index 0000000..6f213c6 --- /dev/null +++ b/templates/django-api/CONTRIBUTING.md @@ -0,0 +1,24 @@ +# Contributing + +## Setup + +```bash +uv sync +uv run python manage.py migrate +uv run pytest +``` + +## Style + +- Feature apps under `apps/` +- Copy new features from `docs/examples/feature-app/` +- Ruff for lint/format +- Typed public APIs (serializers + view annotations) + +## Docs + +Update the matching file under `docs/` when behaviour changes. Keep `AGENTS.md` as a pointer table, not a second docs tree. + +## Extensions + +Optional: `django-docker`, `postgres`, `github-setup`, `development-container`. diff --git a/templates/django-api/QUALITY.md b/templates/django-api/QUALITY.md new file mode 100644 index 0000000..57c87a0 --- /dev/null +++ b/templates/django-api/QUALITY.md @@ -0,0 +1,12 @@ +# Quality checklist (django-api) — M1 + +## Required + +- [x] Feature apps under `apps/` with serializers / services / views / urls +- [x] Docs suite: PROJECT_STRUCTURE, API, CONFIGURATION, TESTING_GUIDE, DEPLOYMENT, TYPING +- [x] README, AGENTS, CONTRIBUTING present +- [x] OpenAPI via drf-spectacular (`/schema/`, `/docs/`) +- [x] pytest-django health test +- [x] Ruff + mypy/pyright configured +- [x] Feature scaffold lives under `docs/examples/` (not an installed app) +- [x] Compatible with `django-docker` and `postgres` extensions diff --git a/templates/django-api/README.md b/templates/django-api/README.md index e80109a..84abfb4 100644 --- a/templates/django-api/README.md +++ b/templates/django-api/README.md @@ -1,45 +1,51 @@ -# Django API +# Django + DRF API -Django + Django REST Framework API starter with [uv](https://docs.astral.sh/uv/), [Ruff](https://docs.astral.sh/ruff/), and [pytest-django](https://pytest-django.readthedocs.io/). +Production-oriented Django REST Framework starter with a **feature-app** layout, +uv, Ruff, pytest-django, mypy/pyright stubs, and docs that match the CPA quality bar. -## Quick start +## Quickstart -```sh +```bash uv sync uv run python manage.py migrate uv run python manage.py runserver +curl -s http://127.0.0.1:8000/api/v1/healthz/ +# OpenAPI UI: http://127.0.0.1:8000/api/v1/docs/ ``` -## Commands +Tests and lint: -| Command | Description | -|---------|-------------| -| `uv run python manage.py runserver` | Start dev server | -| `uv run python manage.py migrate` | Apply migrations | -| `uv run ruff check .` | Lint | -| `uv run pytest` | Run tests | - -## Health probes +```bash +uv run pytest +uv run ruff check . +uv run mypy apps config +``` -| Endpoint | Purpose | -|----------|---------| -| `GET /ping/` | Minimal probe | -| `GET {apiPrefix}/healthz` | API readiness probe | +## Architecture -## Compatible extensions +Domain code lives under `apps//` (serializers, services, views, urls). +Project wiring lives under `config/`. Copy `docs/examples/feature-app/` when adding a feature. -| Slug | Notes | -|------|-------| -| `github-setup` | CI / Dependabot | -| `python-devcontainer` | VS Code Dev Container | -| `python-postgres` | Postgres Compose + driver (wire `DATABASES` manually) | +See [docs/PROJECT_STRUCTURE.md](./docs/PROJECT_STRUCTURE.md). -`python-docker` currently targets FastAPI (`uvicorn app.main:app`) and is not compatible yet. +## Documentation -## Configuration +| Doc | Topic | +|-----|-------| +| [docs/README.md](./docs/README.md) | Index | +| [docs/API.md](./docs/API.md) | Endpoints, envelope, OpenAPI | +| [docs/CONFIGURATION.md](./docs/CONFIGURATION.md) | Env and tooling | +| [docs/TESTING_GUIDE.md](./docs/TESTING_GUIDE.md) | pytest-django | +| [docs/DEPLOYMENT.md](./docs/DEPLOYMENT.md) | Containers and prod | +| [docs/TYPING.md](./docs/TYPING.md) | mypy / pyright | +| [AGENTS.md](./AGENTS.md) | AI assistant guide | +| [CONTRIBUTING.md](./CONTRIBUTING.md) | Human contributor guide | -Copy `.env.example` to `.env`. Scaffold option: +## Compatible extensions -| Option | Default | Description | -|--------|---------|-------------| -| `apiPrefix` | `/api/v1` | Prefix for API routes including `/healthz` | +| Slug | Purpose | +|------|---------| +| `github-setup` | CI / Dependabot / PR automation | +| `development-container` | VS Code Dev Container | +| `django-docker` | Dockerfile + Compose for Django | +| `postgres` | Postgres Compose service (wire `DATABASES` / URL) | diff --git a/templates/django-api/api/urls.py b/templates/django-api/api/urls.py deleted file mode 100644 index d8be3e0..0000000 --- a/templates/django-api/api/urls.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.urls import path - -from api.views import HealthzView - -urlpatterns = [ - path("healthz", HealthzView.as_view(), name="healthz"), -] diff --git a/templates/django-api/api/urls_ping.py b/templates/django-api/api/urls_ping.py deleted file mode 100644 index 1d9c996..0000000 --- a/templates/django-api/api/urls_ping.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.urls import path - -from api.views import PingView - -urlpatterns = [ - path("", PingView.as_view(), name="ping"), -] diff --git a/templates/django-api/api/views.py b/templates/django-api/api/views.py deleted file mode 100644 index 45f6b3c..0000000 --- a/templates/django-api/api/views.py +++ /dev/null @@ -1,25 +0,0 @@ -"""API views for the django-api starter.""" - -from rest_framework.response import Response -from rest_framework.views import APIView - - -class HealthzView(APIView): - authentication_classes: list = [] - permission_classes: list = [] - - def get(self, request): # noqa: ARG002 - return Response( - { - "data": {"status": "healthy"}, - "message": "Service is healthy", - } - ) - - -class PingView(APIView): - authentication_classes: list = [] - permission_classes: list = [] - - def get(self, request): # noqa: ARG002 - return Response({"status": "ok"}) diff --git a/templates/django-api/api/__init__.py b/templates/django-api/apps/__init__.py similarity index 100% rename from templates/django-api/api/__init__.py rename to templates/django-api/apps/__init__.py diff --git a/templates/django-api/apps/health/__init__.py b/templates/django-api/apps/health/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/templates/django-api/api/apps.py b/templates/django-api/apps/health/apps.py similarity index 54% rename from templates/django-api/api/apps.py rename to templates/django-api/apps/health/apps.py index 878e7d5..64553da 100644 --- a/templates/django-api/api/apps.py +++ b/templates/django-api/apps/health/apps.py @@ -1,6 +1,7 @@ from django.apps import AppConfig -class ApiConfig(AppConfig): +class HealthConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" - name = "api" + name = "apps.health" + label = "health" diff --git a/templates/django-api/apps/health/serializers.py b/templates/django-api/apps/health/serializers.py new file mode 100644 index 0000000..cd9584e --- /dev/null +++ b/templates/django-api/apps/health/serializers.py @@ -0,0 +1,15 @@ +"""Health check serializers.""" + +from __future__ import annotations + +from rest_framework import serializers + + +class HealthStatusSerializer(serializers.Serializer): + status = serializers.CharField() + + +class HealthEnvelopeSerializer(serializers.Serializer): + data = HealthStatusSerializer() # type: ignore[assignment] + error = serializers.JSONField(allow_null=True) + meta = serializers.DictField() diff --git a/templates/django-api/apps/health/services.py b/templates/django-api/apps/health/services.py new file mode 100644 index 0000000..bff46ac --- /dev/null +++ b/templates/django-api/apps/health/services.py @@ -0,0 +1,7 @@ +"""Health check business logic.""" + +from __future__ import annotations + + +def get_health_status() -> dict[str, str]: + return {"status": "healthy"} diff --git a/templates/django-api/apps/health/urls.py b/templates/django-api/apps/health/urls.py new file mode 100644 index 0000000..de1c1d5 --- /dev/null +++ b/templates/django-api/apps/health/urls.py @@ -0,0 +1,9 @@ +"""Health feature URLs.""" + +from django.urls import path + +from apps.health.views import HealthzView + +urlpatterns = [ + path("healthz/", HealthzView.as_view(), name="healthz"), +] diff --git a/templates/django-api/apps/health/views.py b/templates/django-api/apps/health/views.py new file mode 100644 index 0000000..4ab8dfd --- /dev/null +++ b/templates/django-api/apps/health/views.py @@ -0,0 +1,22 @@ +"""Health check HTTP endpoints.""" + +from __future__ import annotations + +from drf_spectacular.utils import extend_schema +from rest_framework.permissions import AllowAny +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.views import APIView + +from apps.health.serializers import HealthEnvelopeSerializer, HealthStatusSerializer +from apps.health.services import get_health_status + + +class HealthzView(APIView): + authentication_classes = [] + permission_classes = [AllowAny] + + @extend_schema(responses={200: HealthEnvelopeSerializer}) + def get(self, request: Request) -> Response: + serializer = HealthStatusSerializer(get_health_status()) + return Response({"data": serializer.data, "error": None, "meta": {}}) diff --git a/templates/django-api/config/settings.py.template b/templates/django-api/config/settings.py.template index 23c8e80..d74a7f4 100644 --- a/templates/django-api/config/settings.py.template +++ b/templates/django-api/config/settings.py.template @@ -27,7 +27,8 @@ INSTALLED_APPS = [ "django.contrib.messages", "django.contrib.staticfiles", "rest_framework", - "api", + "drf_spectacular", + "apps.health", ] MIDDLEWARE = [ @@ -66,6 +67,9 @@ DATABASES = { } } +# When using the `postgres` extension, prefer DATABASE_URL / psycopg and replace +# the sqlite engine above. See docs/CONFIGURATION.md and docs/DEPLOYMENT.md. + AUTH_PASSWORD_VALIDATORS = [ {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"}, {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, @@ -90,4 +94,11 @@ REST_FRAMEWORK = { "DEFAULT_PARSER_CLASSES": [ "rest_framework.parsers.JSONParser", ], + "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", +} + +SPECTACULAR_SETTINGS = { + "TITLE": "Django API", + "DESCRIPTION": "API scaffolded with create-awesome-python-app", + "VERSION": "0.1.0", } diff --git a/templates/django-api/config/urls.py b/templates/django-api/config/urls.py index 3798709..5640f51 100644 --- a/templates/django-api/config/urls.py +++ b/templates/django-api/config/urls.py @@ -3,11 +3,17 @@ from django.conf import settings from django.contrib import admin from django.urls import include, path +from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView api_prefix = getattr(settings, "API_PREFIX", "/api/v1").lstrip("/") urlpatterns = [ path("admin/", admin.site.urls), - path("ping/", include("api.urls_ping")), - path(f"{api_prefix}/", include("api.urls")), + path(f"{api_prefix}/schema/", SpectacularAPIView.as_view(), name="schema"), + path( + f"{api_prefix}/docs/", + SpectacularSwaggerView.as_view(url_name="schema"), + name="swagger-ui", + ), + path(f"{api_prefix}/", include("apps.health.urls")), ] diff --git a/templates/django-api/docs/API.md b/templates/django-api/docs/API.md new file mode 100644 index 0000000..fb9f9fb --- /dev/null +++ b/templates/django-api/docs/API.md @@ -0,0 +1,23 @@ +# API + +## Health + +`GET {{ apiPrefix }}/healthz/` + +```json +{ + "data": { "status": "healthy" }, + "error": null, + "meta": {} +} +``` + +## OpenAPI + +| Path | Purpose | +|------|---------| +| `{{ apiPrefix }}/schema/` | OpenAPI 3 schema (drf-spectacular) | +| `{{ apiPrefix }}/docs/` | Swagger UI | + +Keep DRF serializers as the source of truth for request/response shapes; Spectacular +reads them via `DEFAULT_SCHEMA_CLASS`. diff --git a/templates/django-api/docs/CONFIGURATION.md b/templates/django-api/docs/CONFIGURATION.md new file mode 100644 index 0000000..90f083b --- /dev/null +++ b/templates/django-api/docs/CONFIGURATION.md @@ -0,0 +1,24 @@ +# Configuration + +## Environment + +| Variable | Default | Purpose | +|----------|---------|---------| +| `DJANGO_SECRET_KEY` | `dev-only-change-me` | Django secret | +| `DJANGO_DEBUG` | `true` | Debug mode | +| `DJANGO_ALLOWED_HOSTS` | `localhost,127.0.0.1` | Comma-separated hosts | + +Scaffold option `apiPrefix` (default `/api/v1`) becomes `API_PREFIX` in settings. + +## Tooling + +| Tool | Command | +|------|---------| +| Ruff | `uv run ruff check .` | +| mypy | `uv run mypy apps config` | +| pyright | `uv run pyright` | +| pytest | `uv run pytest` | + +## Database + +Default is SQLite. With the `postgres` extension, add `psycopg` (merged via extension `pyproject.toml`) and point Django `DATABASES` (or a URL helper) at the Compose service hostname `db`. diff --git a/templates/django-api/docs/DEPLOYMENT.md b/templates/django-api/docs/DEPLOYMENT.md new file mode 100644 index 0000000..1b20b53 --- /dev/null +++ b/templates/django-api/docs/DEPLOYMENT.md @@ -0,0 +1,22 @@ +# Deployment + +## Local process + +```bash +uv run python manage.py migrate +uv run python manage.py runserver +``` + +## Containers + +Add the `django-docker` extension for `Dockerfile` + Compose (`runserver` locally, `gunicorn config.wsgi` in prod overlay). + +Pair with `postgres` when you need a database service. Inside Compose, use hostname `db` instead of `localhost`. + +## Checklist + +- [ ] Set a strong `DJANGO_SECRET_KEY` +- [ ] `DJANGO_DEBUG=false` in production +- [ ] Restrict `DJANGO_ALLOWED_HOSTS` +- [ ] Run migrations before traffic +- [ ] Terminate TLS at the proxy / platform diff --git a/templates/django-api/docs/PROJECT_STRUCTURE.md b/templates/django-api/docs/PROJECT_STRUCTURE.md new file mode 100644 index 0000000..6f135ac --- /dev/null +++ b/templates/django-api/docs/PROJECT_STRUCTURE.md @@ -0,0 +1,29 @@ +# Project structure + +``` +apps/ + health/ # example feature app + serializers.py + services.py + views.py + urls.py +config/ # Django project package (settings, urls, asgi/wsgi) +docs/ + examples/feature-app/ # copy-me scaffold (not installed) +manage.py +tests/ +``` + +## Adding a feature + +1. Copy `docs/examples/feature-app/` → `apps//`. +2. Set `name = "apps."` in `apps.py` and fix imports. +3. Register in `INSTALLED_APPS`. +4. Wire urls under `API_PREFIX` in `config/urls.py`. +5. Add tests; keep the `{data, error, meta}` envelope for JSON APIs. + +## Conventions + +- Business logic in `services.py`; HTTP in `views.py`; shapes in `serializers.py`. +- Do not put domain logic in `config/`. +- Prefer one Django app per bounded context. diff --git a/templates/django-api/docs/README.md b/templates/django-api/docs/README.md index 8dbfc93..6f4c47f 100644 --- a/templates/django-api/docs/README.md +++ b/templates/django-api/docs/README.md @@ -1,5 +1,9 @@ -# Django API docs +# Documentation index -- Prefer DRF `APIView` / viewsets under `api/` as the project grows. -- Keep health probes unauthenticated. -- When adding Postgres, update `DATABASES` in `config/settings.py` and prefer env-driven DSN parsing. +- [Project structure](./PROJECT_STRUCTURE.md) +- [API](./API.md) +- [Configuration](./CONFIGURATION.md) +- [Testing](./TESTING_GUIDE.md) +- [Deployment](./DEPLOYMENT.md) +- [Typing](./TYPING.md) +- [Feature app example](./examples/feature-app/README.md) diff --git a/templates/django-api/docs/TESTING_GUIDE.md b/templates/django-api/docs/TESTING_GUIDE.md new file mode 100644 index 0000000..369b8ca --- /dev/null +++ b/templates/django-api/docs/TESTING_GUIDE.md @@ -0,0 +1,16 @@ +# Testing Guide + +Uses **pytest-django** with `DJANGO_SETTINGS_MODULE=config.settings`. + +## Patterns + +- Prefer `APIClient` for DRF views. +- Keep feature tests close to behaviour (status codes + envelope keys). +- Use eager, isolated DB — default SQLite is fine for unit tests. + +## Commands + +```bash +uv run pytest +uv run pytest -k healthz +``` diff --git a/templates/django-api/docs/TYPING.md b/templates/django-api/docs/TYPING.md new file mode 100644 index 0000000..0cdfe91 --- /dev/null +++ b/templates/django-api/docs/TYPING.md @@ -0,0 +1,17 @@ +# Typing + +This starter ships **mypy** (with `django-stubs` / `djangorestframework-stubs`) and +**pyright**. + +```bash +uv run mypy apps config +uv run pyright +``` + +## Conventions + +- Annotate public view methods and service functions +- Prefer concrete serializer fields over untyped `dict` at API boundaries +- Avoid unjustified `# type: ignore` + +See also FastAPI starter `docs/TYPING.md` for the shared CPA typed-Python bar. diff --git a/templates/django-api/docs/examples/feature-app/README.md b/templates/django-api/docs/examples/feature-app/README.md new file mode 100644 index 0000000..e774a2b --- /dev/null +++ b/templates/django-api/docs/examples/feature-app/README.md @@ -0,0 +1,10 @@ +# Feature app scaffold (copy into `apps/`) + +This directory is documentation — it is **not** an installed Django app. + +1. Copy `docs/examples/feature-app/` → `apps//`. +2. Rename the app config `name` in `apps.py` to `apps.`. +3. Fix imports from `apps....`. +4. Add the app to `INSTALLED_APPS` in `config/settings.py`. +5. Include its urls from `config/urls.py` under `API_PREFIX`. +6. Add tests under `tests/`. diff --git a/templates/django-api/docs/examples/feature-app/apps.py b/templates/django-api/docs/examples/feature-app/apps.py new file mode 100644 index 0000000..da8b1ae --- /dev/null +++ b/templates/django-api/docs/examples/feature-app/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig + + +class FeatureAppConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + # After copying to apps//, set: + # name = "apps." + name = "apps.REPLACE_ME" diff --git a/templates/django-api/docs/examples/feature-app/serializers.py b/templates/django-api/docs/examples/feature-app/serializers.py new file mode 100644 index 0000000..8401084 --- /dev/null +++ b/templates/django-api/docs/examples/feature-app/serializers.py @@ -0,0 +1,9 @@ +"""Example serializers — rename package imports after copying into apps/.""" + +from __future__ import annotations + +from rest_framework import serializers + + +class ExampleSerializer(serializers.Serializer): + message = serializers.CharField() diff --git a/templates/django-api/docs/examples/feature-app/services.py b/templates/django-api/docs/examples/feature-app/services.py new file mode 100644 index 0000000..2ea1746 --- /dev/null +++ b/templates/django-api/docs/examples/feature-app/services.py @@ -0,0 +1,7 @@ +"""Example service layer.""" + +from __future__ import annotations + + +def example_message() -> dict[str, str]: + return {"message": "replace-me"} diff --git a/templates/django-api/docs/examples/feature-app/urls.py b/templates/django-api/docs/examples/feature-app/urls.py new file mode 100644 index 0000000..65eb3f4 --- /dev/null +++ b/templates/django-api/docs/examples/feature-app/urls.py @@ -0,0 +1,7 @@ +from django.urls import path + +from .views import ExampleView + +urlpatterns = [ + path("example/", ExampleView.as_view(), name="example"), +] diff --git a/templates/django-api/docs/examples/feature-app/views.py b/templates/django-api/docs/examples/feature-app/views.py new file mode 100644 index 0000000..fd54681 --- /dev/null +++ b/templates/django-api/docs/examples/feature-app/views.py @@ -0,0 +1,18 @@ +"""Example views — after copy, import from apps..*.""" + +from __future__ import annotations + +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.views import APIView + +# from apps..serializers import ExampleSerializer +# from apps..services import example_message +from .serializers import ExampleSerializer +from .services import example_message + + +class ExampleView(APIView): + def get(self, request: Request) -> Response: + serializer = ExampleSerializer(example_message()) + return Response({"data": serializer.data, "error": None, "meta": {}}) diff --git a/templates/django-api/pyproject.toml b/templates/django-api/pyproject.toml index 5356a91..9b6bbb7 100644 --- a/templates/django-api/pyproject.toml +++ b/templates/django-api/pyproject.toml @@ -7,6 +7,8 @@ requires-python = ">=3.12" dependencies = [ "django>=5.1.0", "djangorestframework>=3.15.0", + "drf-spectacular>=0.27.0", + "gunicorn>=23.0.0", "python-dotenv>=1.0.0", ] @@ -26,7 +28,7 @@ requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] -packages = ["config", "api"] +packages = ["config", "apps"] [tool.pytest.ini_options] DJANGO_SETTINGS_MODULE = "config.settings" @@ -36,12 +38,13 @@ testpaths = ["tests"] [tool.ruff] line-length = 100 target-version = "py312" +extend-exclude = ["docs"] [tool.ruff.lint] select = ["E", "F", "I", "UP", "B"] [tool.pyright] -include = ["api", "config", "tests"] +include = ["apps", "config", "tests"] pythonVersion = "3.12" typeCheckingMode = "basic" reportMissingImports = false @@ -49,7 +52,8 @@ reportMissingTypeStubs = false [tool.mypy] python_version = "3.12" -files = ["api", "config", "tests"] +files = ["apps", "config", "tests"] +exclude = ["docs/"] plugins = ["mypy_django_plugin.main"] warn_return_any = true warn_unused_configs = true diff --git a/templates/django-api/tests/test_health.py.template b/templates/django-api/tests/test_health.py.template index 1bf45c9..0f9c48b 100644 --- a/templates/django-api/tests/test_health.py.template +++ b/templates/django-api/tests/test_health.py.template @@ -10,13 +10,16 @@ def client() -> APIClient: def test_healthz_returns_healthy(client: APIClient) -> None: - response = client.get(f"{API_PREFIX}/healthz") + response = client.get(f"{API_PREFIX}/healthz/") assert response.status_code == 200 body = response.json() assert body["data"]["status"] == "healthy" + assert body["error"] is None -def test_ping_returns_ok(client: APIClient) -> None: - response = client.get("/ping/") +def test_openapi_schema_available(client: APIClient) -> None: + response = client.get(f"{API_PREFIX}/schema/") assert response.status_code == 200 - assert response.json()["status"] == "ok" + body = response.content.decode() + assert "openapi" in body + assert "healthy" in body or "healthz" in body.lower() or "Healthz" in body