Skip to main content

baltimore-patapsco (Python)

Reusable Django runtime primitives for Baltimore civic applications.

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:

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:

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(). Both stay outside ATOMIC_REQUESTS on every database, subclasses included, so an outage is answered by the view, not a 500. 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:

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.

File downloads

stream_csv() streams a CSV export that a client can always tell is incomplete, and content_disposition() names a file safely for every client:

from baltimore.patapsco.api import stream_csv


class CaseExportView(APIView):
    def get(self, request):
        rows = cases_for(request).values_list("number", "opened", "status").iterator()
        return stream_csv(request, rows, header=["Case", "Opened", "Status"], filename="cases.csv")

A download holds one 64 Ki-character chunk in memory whatever its row count, under WSGI and ASGI. A failure before the first chunk is the view's ordinary error response. A failure after it is logged as download.stream_failed with the request ID and re-raised, so the server aborts the transfer instead of ending it. max_rows= answers 413 with the error envelope (export_too_large, details.max_rows) before anything is sent, and otherwise sends the file with a Content-Length. bom=True adds the byte order mark Excel needs for non-ASCII text. A request that carries X-Download-End-Marker, as downloadFile() in @city-of-baltimore/patapsco sends, gets its token echoed and appended to the body, so a cut is visible even where HTTP framing hides it (runserver, or a proxy that speaks HTTP/1.0 to gunicorn). Call it from a synchronous view: it reads the first rows before returning.

Every cell of the header and rows goes through spreadsheet_safe_cell(), with no opt-out, so no cell opens in a spreadsheet as a formula. Text that starts with TAB, CR or LF, or whose first character after whitespace is =, +, -, @ or a full-width form of one, gets a leading '. Typed numbers and text that is exactly a negative decimal (-12.5, not -0500) are left alone. Vertical tab and form feed become a space, and the other C0 controls except TAB, LF and CR are removed, so a pasted control never fails an export. Call it yourself for a CSV written anywhere else, such as an import report on disk:

from baltimore.patapsco.api import spreadsheet_safe_cell

writer.writerow([spreadsheet_safe_cell(value) for value in row])

For an XLSX text cell, spreadsheet_safe_text() gives the same text with the controls scrubbed and no '. The writer's string type is the protection (openpyxl data_type = "s", xlsxwriter write_string); a ' prefix never is, because XLSX stores it as visible text:

from baltimore.patapsco.api import spreadsheet_safe_text

cell = sheet.cell(row=row, column=column, value=spreadsheet_safe_text(value))
cell.data_type = "s"

The wire contract states the whole policy. The TypeScript SDK's spreadsheetSafeText, spreadsheetSafeCell and buildCsv apply it in the browser, tested against the same vectors.

content_disposition(filename, disposition="attachment") adds what Django's header lacks: an ASCII filename="..." fallback beside filename* for non-ASCII names, and a name reduced to its final path component, without control or bidi-control characters, and cut to 255 UTF-8 bytes.

Session lifecycle

An application that wants one session policy applies it in settings, installs the middleware after AuthenticationMiddleware, and authenticates its API with Patapsco's SessionAuthentication:

from datetime import timedelta

from baltimore.patapsco.sessions import session_settings

globals().update(session_settings(idle_timeout=timedelta(hours=1)))
MIDDLEWARE += ["baltimore.patapsco.sessions.middleware.SessionActivityMiddleware"]
REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] = [
    "baltimore.patapsco.sessions.authentication.SessionAuthentication",
]

Only requests a person starts renew the session. Health, readiness, the status read and views marked @session_passive never do. A signed-out API request gets 401 with a WWW-Authenticate challenge. baltimore.patapsco.urls then mounts GET /api/session/status and POST /api/session/renew. Session lifecycle is the adoption sequence, and the wire contract is the exact behaviour. Nothing changes until session_settings() is applied.

Sentry privacy defaults

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

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
PATAPSCO_SESSIONS Django setting unset The session policy session_settings() writes; setting it mounts the session endpoints and lets SessionActivityMiddleware load. Do not write it by hand

Metadata

Release files for baltimore-patapsco 0.18.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for baltimore-patapsco 0.18.1
File Size Uploaded
baltimore_patapsco-0.18.1.tar.gz 420.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for baltimore-patapsco 0.18.1
File Interpreter ABI Platform
baltimore_patapsco-0.18.1-py3-none-any.whl Python 3 none any Details

Total release size: 925.0 kB

Release files / baltimore_patapsco-0.18.1.tar.gz

Download URL baltimore_patapsco-0.18.1.tar.gz
Size 420.4 kB
Tags Source
SHA-256 checksum
How to use checksums
1b7c15e11cc51b409fa0c7bb6a25f3c5d06318b3f915862d12ea9e337fce9a43
BLAKE2b-256 checksum
How to use checksums
b86a1d727d5cc8bdc41ed567f8a2f71c559ff74ff341bf8eabacd038a733ef1b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.15 {"installer":{"name":"uv","version":"0.12.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / baltimore_patapsco-0.18.1-py3-none-any.whl

Download URL baltimore_patapsco-0.18.1-py3-none-any.whl
Size 504.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0219c993018a5d03d24a6cbf9e0cc4b9e4568bd197fd4c3c6dd62d0af8bd4082
BLAKE2b-256 checksum
How to use checksums
6d9b9eedffdcbe5c2b97558a7ddb2c9fb22c207d7cbbb655aba554b97ab98742
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.15 {"installer":{"name":"uv","version":"0.12.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

2.4.1

2 release files

2.4.0

2 release files

2.3.3

2 release files

2.3.1

2 release files

2.3.0

2 release files

2.2.0

2 release files

2.1.2

2 release files

2.1.1

2 release files

2.1.0

2 release files

2.0.1

2 release files

2.0.0

2 release files

1.0.0

2 release files

This release

0.18.1 This release

2 release files

0.18.0

2 release files

0.17.3

2 release files

0.17.2

2 release files

0.17.1

2 release files

0.17.0

2 release files

0.16.0

2 release files

0.15.3

2 release files

0.15.2

2 release files

0.15.1

2 release files

0.15.0

2 release files

0.14.2

2 release files

0.14.1

2 release files

0.14.0

2 release files

0.13.1

2 release files

0.13.0

2 release files

0.12.2

2 release files

0.9.9

2 release files

0.9.8

2 release files

0.9.7

2 release files

0.9.6

2 release files

0.9.5

2 release files

0.9.4

2 release files

0.9.3

2 release files

0.9.2

2 release files

0.9.1

2 release files

0.9.0

2 release files

0.8.2

2 release files

0.8.1

2 release files

0.8.0

2 release files

0.7.2

2 release files

0.7.1

2 release files

0.7.0

2 release files

0.6.24

2 release files

0.6.23

2 release files

0.6.9

2 release files

0.6.8

2 release files

0.6.7

2 release files

0.6.6

2 release files

0.6.5

2 release files

0.6.4

2 release files

0.6.3

2 release files

0.6.2

2 release files

0.6.1

2 release files

0.6.0

2 release files

0.5.5

2 release files

0.5.4

2 release files

0.5.3

2 release files

0.5.2

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.8

2 release files

0.4.7

2 release files

0.4.6

2 release files

0.4.5

2 release files

0.4.4

2 release files

0.4.3

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page