# andrea-svc-web — the common basement for Andrea web services

The shared web-service base. It pours the foundation once — framework-only Django settings, a
`/health` endpoint, a storm-safe base view, drf-spectacular OpenAPI, an API-key/OIDC auth class
(via `andrea_svc_auth`), and a **bounded gunicorn worker pool** — so authoring a new service means
writing **only its business logic**. It is the answer to the operator directive *"we do not need a
single-threaded HTTP server anywhere; Django fixes this brilliantly"* (the #250 lesson), built once.

Sibling to `andrea-svc-auth` (auth) — same shape: a versioned pip package installed from the devpi
proxy. **Composable, not prescriptive:** a service that wasn't ours from the start can adopt just
the pieces it wants (the auth class alone, or the storm-safe view alone) without a rewrite.

## Author a service in four small files

```python
# myservice/settings.py
from andrea_svc_web.settings import *                       # noqa: F401,F403
SERVICE_NAME       = "myservice"
INSTALLED_APPS     = INSTALLED_APPS + ["myservice.app"]     # noqa: F405
ROOT_URLCONF       = "myservice.urls"
WSGI_APPLICATION   = "myservice.wsgi.application"
SPECTACULAR_SETTINGS = {**SPECTACULAR_SETTINGS,             # noqa: F405
    "TITLE": "My Service", "VERSION": "1.0.0", "DESCRIPTION": "..."}
SVC_HEALTHCHECK    = "myservice.app.service:healthcheck"    # optional deep check
```
```python
# myservice/urls.py
from andrea_svc_web.urls import service_urls
from django.urls import path
from myservice.app.views import ThingView
urlpatterns = service_urls(extra=[path("thing", ThingView.as_view(), name="thing")])
```
```python
# myservice/wsgi.py
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myservice.settings")
application = get_wsgi_application()
```
```python
# myservice/app/views.py  — the ONLY real work
from andrea_svc_web.views import StormSafeAPIView
from andrea_svc_web.auth import ServiceOrOIDCAuthentication, ServiceKeyRequiredIfConfigured
from rest_framework.response import Response

class ThingView(StormSafeAPIView):
    authentication_classes = [ServiceOrOIDCAuthentication]
    permission_classes     = [ServiceKeyRequiredIfConfigured]
    def post(self, request):
        return Response(do_the_thing(request.data))
```

## Run it

```
SVC_WSGI=myservice.wsgi:application SVC_BIND=0.0.0.0:8769 \
  gunicorn -c python:andrea_svc_web.gunicorn_conf "$SVC_WSGI"
```
or copy the shared `andrea_svc_web/entrypoint.sh` (adds the tcmalloc runtime toggle).

## What you inherit

| concern | where | note |
|---|---|---|
| Django/DRF settings | `andrea_svc_web.settings` | star-import; no ORM apps, `DATABASES={}` (override for ORM) |
| `/health` | `andrea_svc_web.health.HealthView` | pluggable deep-check via `SVC_HEALTHCHECK` |
| OpenAPI `/api/schema` + `/api/docs` | `andrea_svc_web.urls.service_urls` | drf-spectacular |
| never-500-into-a-retry-storm | `andrea_svc_web.views.StormSafeAPIView` | the #250 lesson |
| auth (API-key / OIDC) | `andrea_svc_web.auth` | composes `andrea_svc_auth`; config-driven via env |
| optional FastAPI-compat 422 | `andrea_svc_web.exceptions` | opt-in `ANDREA_FASTAPI_COMPAT_422=1` → validation errors render as FastAPI's 422 `{"detail":[{loc,msg,type}]}`; **default off** (standard DRF 400) |
| bounded **threaded** worker pool | `andrea_svc_web.gunicorn_conf` | defaults to `gthread` (NOT gunicorn's single-threaded `sync` default); concurrency = workers × threads; all env-tunable |
| **syslog logging** | `andrea_svc_web.logging_conf` | logs to stdout **and** syslog, tagged with `SERVICE_NAME`; env-tunable (`SYSLOG_ADDRESS`/`FACILITY`, `LOG_LEVEL`); fail-soft (no `/dev/log` → stdout only) |
| **ConfigServer** (secrets + tweaks) | `andrea_svc_web.config` | opt-in `ANDREA_CONFIG_SERVICE=1` → overlays central config-service values over env before settings load; retires dot-envs; stdlib-only + fail-soft |

First consumer / worked example: `search/pdffeed/` (pdf_feed_service migrated off its hand-rolled
single-threaded `http.server`).
