Metadata-Version: 2.4
Name: baltimore-patapsco
Version: 0.16.0
Summary: Shared Django runtime foundations for Baltimore civic applications
Author: Mayor's Office of Performance and Innovation
License-Expression: MIT
License-File: LICENSE
Requires-Dist: asgiref>=3.9,<4.0
Requires-Dist: django>=6.0,<6.1
Requires-Dist: djangorestframework>=3.16,<4.0
Requires-Dist: drf-spectacular>=0.28,<0.31
Requires-Dist: pyyaml>=6.0.3,<7.0
Requires-Dist: sentry-sdk[django]>=2.64.0,<3.0 ; extra == 'observability'
Requires-Dist: dj-database-url>=3.0,<4.0 ; extra == 'runtime'
Requires-Dist: django-environ>=0.12,<0.15 ; extra == 'runtime'
Requires-Dist: whitenoise>=6.9,<7.0 ; extra == 'runtime'
Requires-Python: >=3.13
Provides-Extra: observability
Provides-Extra: runtime
Description-Content-Type: text/markdown

# baltimore-patapsco (Python)

Reusable Django runtime primitives for Baltimore civic applications.

```python
INSTALLED_APPS += ["baltimore.patapsco"]

REST_FRAMEWORK = {
    "EXCEPTION_HANDLER": "baltimore.patapsco.api.exception_handler",
}

urlpatterns = [path("", include("baltimore.patapsco.urls"))]
```

Place `RequestContextMiddleware` early in `MIDDLEWARE` — after
`SecurityMiddleware` (and WhiteNoise when present), before session, auth, and
anything that can short-circuit a response — so every later middleware, view,
and log line sees the bound request ID. The reference app shows the canonical
order:

```python
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",
    "baltimore.patapsco.observability.RequestContextMiddleware",
    # sessions, common, csrf, auth, messages, clickjacking...
]
```

## Stable health and readiness views

Applications that mount or extend the operational views directly use the public
facade:

```python
from baltimore.patapsco.api import HealthView, ReadinessView
```

`HealthView` is process-only liveness. `ReadinessView` runs the established
database check and returns its public-safe 200 or 503 status document. Both use
no authentication classes and `AllowAny`, and both remain ordinary DRF
`APIView` classes with `as_view()`. A database failure is reported once per
worker as it begins, at error level with the exception; later failed probes log
`readiness.database_unavailable` at warning level with only the exception type,
and the first healthy probe logs `readiness.database_recovered`. A probe every
few seconds during an outage therefore sends one Sentry event, not one per probe.

`HealthView` and `DiagnosticsView` render their errors in the Patapsco envelope
their OpenAPI declares, whatever the app's `REST_FRAMEWORK["EXCEPTION_HANDLER"]`
is. A subclass may override `get_exception_handler()` and then owns its schema.

An application may subclass either class, override `get()`, and call
`super().get(request)` before composing app-owned public-safe checks. The parent
response status, request ID, and database result remain the starting contract;
the application owns an explicit serializer and OpenAPI annotation for any
extended payload.

The previous `baltimore.patapsco.api.views` imports resolve to the same class
objects for compatibility. `DiagnosticsView` remains available only from that
module and through the packaged URL configuration; it is intentionally absent
from `baltimore.patapsco.api.__all__` because its staff/public policy is a
separate operational surface. The settings-free package root exports neither
view class.

`api_error_response()` and the DRF exception handler force every 5xx response to
the public `internal_error` code, generic message, and no details. Application
callers cannot opt a server failure out of that safety boundary. An unexpected
exception also rolls back every active database transaction managed by Django's
`ATOMIC_REQUESTS`, so a failed request cannot commit partial writes. Applications
retain ownership of explicitly managed transactions.

Use `build_logging_config()` for the shared JSON/pretty log shape. Modules are
incrementally adoptable; installing the package does not enable auth, email, or
Sentry by itself. Structured log context is copied and recursively redacted,
including credentials nested in mappings and sequences. Credential keys are
matched across case and separator conventions, including `clientAPIKey` and
`X-API-Key`; repeated safe context is preserved and cycles are cut safely.
A Django or DRF request object anywhere in a record, such as the `request` extra
Django adds to every `django.request` 4xx/5xx record, renders as
`{"method": ..., "path": ...}`, never as its repr, so a request object never
carries its query string into a log. Lines other loggers write are their own:
`runserver`'s `django.server` request line still prints the full URL.
`configure_sentry()` renders request objects in Sentry extras and log
breadcrumbs the same way.

Bind a record's identity once it is known and every later line of the request,
including `request.complete`, carries it under `context`:

```python
from baltimore.patapsco.observability import bind_log_context

bind_log_context(case_id=case.public_id)
```

Bind identifiers, never resident data: logs redact credentials, not civic
contact or location fields. `None` values are ignored, an explicit record
`extra` wins over a bound value, and outside a request the binding persists
unless the work runs inside `request_context()`. Bound values do not tag Sentry
events.

## Sentry privacy defaults

Install the `observability` extra and call the shared initializer only when a DSN
is configured:

```python
from baltimore.patapsco.observability import configure_sentry

configure_sentry(
    environment="production",
    release="my-app@1.2.3",
    traces_sample_rate=0.1,
)
```

`configure_sentry()` is deny-by-default: it disables automatic PII, request-body
capture, and stack-frame local variables. Its event pipeline also removes request
cookies and query strings, strips query/fragment data from request URLs and the
`Referer` request header, reduces
breadcrumb, span, and trace data URLs (`url`, `http.url`, `url.full`) to origin
plus path and removes `http.query`, `http.fragment`, `url.query`, and
`url.fragment` from them, and recursively redacts the shared credential-key fragments, Sentry's credential/session
denylist, and common civic contact and precise-location fields. This protection
also applies to transaction events. Safe operational fields such as request IDs,
routes, and HTTP methods remain available.

`traces_sampler` optionally accepts a callable with the SDK sampling-context
dictionary and a return rate from zero to one. It decides whether a trace is
recorded at creation, before event processing; it takes precedence over
`traces_sample_rate` and the parent decision. Apps own route exclusions and
whether to honor `context["parent_sampled"]`. Omitting the callback preserves
SDK defaults. Callback exceptions retain SDK behavior and can propagate, so app
samplers must handle their expected failure cases. Initialization failures still
return `False` without exposing configuration in logs.

Pass `dsn=""` to explicitly disable reporting even when `SENTRY_DSN` exists;
omitting the argument or passing `None` permits that environment fallback.
For domain enrichment, register a public SDK global event processor once, after
successful initialization. It runs before the shared scrubber and final privacy
hooks for errors and transactions. Retain stronger app policies there; do not
import private Patapsco filters or replace the client's privacy options.

Redaction is key-based. Applications must not attach sensitive values under
misleading keys or place resident data in exception messages. Product-specific
fields still require a privacy review before being added to Sentry context.

## Configuration surface

Everything the library reads from the environment or Django settings:

| Name                          | Kind           | Default               | Meaning                                                                                                                                                       |
| ----------------------------- | -------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `APP_ENV`                     | env var        | `local`               | Environment label in diagnostics and Sentry events; overridden by `PATAPSCO_ENVIRONMENT` when set                                                             |
| `APP_RELEASE`                 | env var        | —                     | Release identifier in diagnostics and Sentry events; overridden by `PATAPSCO_RELEASE` when set                                                                |
| `APP_LOG_FORMAT`              | env var        | `json`                | `json` or `pretty` output from `build_logging_config()`                                                                                                       |
| `APP_LOG_LEVEL`               | env var        | `INFO`                | Root log level from `build_logging_config()`                                                                                                                  |
| `SENTRY_DSN`                  | env var        | —                     | Enables `configure_sentry()`; absent means Sentry stays off                                                                                                   |
| `PATAPSCO_SERVICE_NAME`       | Django setting | project name          | Service label in the diagnostics payload                                                                                                                      |
| `PATAPSCO_ENVIRONMENT`        | Django setting | `APP_ENV` env var     | Overrides the diagnostics `environment` field via Django settings instead of the process environment (a real `override_settings` test seam)                   |
| `PATAPSCO_RELEASE`            | Django setting | `APP_RELEASE` env var | Overrides the diagnostics `release` field via Django settings instead of the process environment (a real `override_settings` test seam)                       |
| `PATAPSCO_DIAGNOSTICS_PUBLIC` | Django setting | `False`               | **Security-relevant:** flips `/api/diagnostics` from `IsAdminUser` to `AllowAny`. Leave `False` unless the deployment deliberately publishes runtime metadata |
