v0.150.0

plain changelog

0.150.0 (2026-06-09)

What's changed

  • PREFLIGHT_SILENCED_RESULTS now accepts obj-qualified entries in the form "<id>:<obj>", silencing a result for one specific object while the same result ID keeps warning everywhere else. For example, "postgres.missing_fk_index:app.InsightEvent.sender_account" silences the missing-FK-index warning for that one field only. The object label is whatever appears before the result ID in the preflight output. (efd02c5ee2)
  • A new preflight.unused_silences check runs last on full preflight runs (plain preflight --deploy) and warns about PREFLIGHT_SILENCED_RESULTS entries that matched nothing — an unused entry is either a typo or stale (the issue it silenced has been fixed). It can be silenced via PREFLIGHT_SILENCED_CHECKS like any other check, and the matching logic is available as plain.preflight.unused_silenced_results(). (f5863c70be)

Upgrade instructions

  • No changes required.

0.149.1 (2026-06-08)

What's changed

  • Internal: typing-only changes for the ty 0.0.45 upgrade. @deconstructible gains explicit @overload signatures so it type-checks correctly whether applied bare (@deconstructible) or with arguments (@deconstructible(path=...)), Worker's sockets parameter widens from list to Sequence, and a few ty: ignore comments were added or removed to match. No runtime behavior changes. (95f54e880d)

Upgrade instructions

  • No changes required.

0.149.0 (2026-06-07)

What's changed

  • JsonResponse no longer accepts a safe argument. Previously it defaulted to safe=True and raised TypeError unless data was a dict; you passed safe=False to serialize a list or any other non-dict value. The dict-only guard and the parameter are both gone — JsonResponse now serializes any JSON-serializable value directly. (52338f58da)

Upgrade instructions

  • Drop safe= from JsonResponse(...) calls. If you passed safe=False to serialize a list (or any non-dict), just remove the argument — the value now serializes as-is. safe=True was the default and is likewise gone.

0.148.1 (2026-06-03)

What's changed

  • plain.observer has been retired. Its references are removed from core: the package listing, the plain docs known-packages list, and the internal request-span plumbing that passed cookies/headers to the observer sampler. (1bab9f784a)
  • The plain-upgrade skill now summarizes what changed in each release, not just the upgrade steps. (bfdfb9a45a)
  • Docs: model write examples updated to create()/update(). (f75deb3ba2)

Upgrade instructions

  • No changes required. If you used plain.observer, switch to plain.connect for production telemetry export and plain.pytest/plain.connect for the OpenTelemetry SDK — observer is no longer published.

0.148.0 (2026-05-22)

What's changed

  • plain request now captures the OpenTelemetry spans emitted during the request and prints a trace analysis — query counts, duplicate-query (N+1) detection grouped by SQL with source locations, recorded exceptions, and a span tree. The text output gains a Trace section; --json returns structured response metadata plus the same analysis under a top-level trace key (with both derived analysis and raw spans). Use --json for context-frugal agent output — no response body, just metadata and trace data. The /plain-optimize skill moves from plain.observer to core now that trace analysis lives there. (afba897157)
  • plain request now surfaces followed redirects and auth state. A request to an auth-gated view used to silently follow the 302 to the login page and leave the trace looking empty for no obvious reason. The output now prints a Redirected: line listing the hop chain (status → path) when redirects were followed, and always reports the resolved user (or anonymous) — in both text and --json output. (c4283e48a6)
  • Preflight queries are no longer counted in plain request traces. The admin toolbar's preflight badge lazy-runs the full preflight suite on first render (pg_class introspection, migration-state queries), and those were landing in captured traces — inflating query counts and duration in ways that looked like real request work. The check counts are now pre-seeded before dispatch so the badge skips the run. (6e18fc4361)

Upgrade instructions

  • If you were running plain observer request /path for trace analysis, switch to plain request /path --json — same JSON shape philosophy, no observer install required.
  • No code changes required. The /plain-optimize skill now ships with plain itself — plain.observer no longer needs to be installed for the workflow.

0.147.0 (2026-05-21)

What's changed

  • The plain.utils.dotenv module has moved to plain.dev.dotenv. Production deployments (which don't install plain.dev) no longer ship dotenv parsing code at all — load environment variables via your deployment platform instead. (9932738450)
  • The CLI dispatcher now sets PLAIN_ENV automatically based on the active command: plain devdev, plain testtest. Export PLAIN_ENV yourself to override. This lets plain.dev's new .env precedence loader pick up the right env-specific files without users having to set PLAIN_ENV manually. (9932738450)
  • SuspiciousOperationError400 exceptions are now logged at WARNING without an attached exc_info. The rejection is working-as-designed (same noise category as 404s once a scanner is probing), so the full traceback was just adding noise to error trackers. (7727f0545c)

Upgrade instructions

  • If you imported load_dotenv or parse_dotenv from plain.utils.dotenv, install plain.dev and import from plain.dev.dotenv instead.
  • If you were filtering SuspiciousOperationError400 events out of your error tracker by exception type, switch to filtering by log level (warnings vs. errors) or by the plain.security.* logger namespace.

0.146.0 (2026-05-20)

What's changed

  • plain docs --search now substring-matches by default; pass --regex to opt into regex patterns (with alternation, anchors, etc.). The previous default treated the search term as a regex, which silently broke any search containing regex metacharacters (., ?, [, (, etc.). (c8c1a2bd7b)
  • The request object is no longer attached as a log extra on the 400 host-validation warning, the 405 method-not-allowed warning, or log_exception()'s base context. Downstream log processors that called methods on the live request were a footgun — the request lifecycle had often already ended by the time the log was processed. (6324e21a67)

Upgrade instructions

  • If a saved plain docs --search invocation relied on regex behavior, add --regex to it.
  • If your logging configuration consumed record.request from these entries, switch to another extra field — request.path is still attached where it was before.

0.145.3 (2026-05-20)

What's changed

  • The plain-support package has been removed and no longer appears in the package docs list or the plain docs CLI. Customer support forms now live in plain.connect via the {% connect_support_fields %} template tag. (a1a1da39c5)

Upgrade instructions

  • No changes required.

0.145.2 (2026-05-19)

What's changed

  • The plain-pageviews package has been removed and no longer appears in the package docs list or the plain docs CLI. First-party pageview tracking now lives in plain.connect via the {% connect_pageviews %} template tag. (8fa042fc19)
  • plain docs --api now reads __all__ whether it is declared as a list or a tuple, so a module using a tuple no longer silently drops its API surface from the output. (64ee8a4de0)

Upgrade instructions

  • No changes required. If you depended on plain.pageviews, migrate to plain.connect's pageview tracking.

0.145.1 (2026-05-16)

What's changed

  • Documentation only: expanded the 0.145.0 trailing-slash upgrade notes to call out relative URLs in templates. <form action="."> and <a href="."> resolve against the request URL's last slash, so flipping a route from /login/ to /login changes where action="." submits — the notes now explain how to spot and fix this. (1ae963c6df)

Upgrade instructions

  • No changes required.

0.145.0 (2026-05-13)

What's changed

  • Trailing slash is now an app-wide setting. New URLS_TRAILING_SLASH setting (default False) decides the canonical form for every route in the project. path("about") and path("about/") produce identical routes — the slash on the route string is stripped silently. Requests at the non-canonical form 308-redirect to the canonical one. (48ca69bafa)
  • path(..., force_trailing_slash=True|False) per-route override. Lets file-extension routes (sitemap.xml, robots.txt) stay slash-less under a slashed app, or keep individual legacy URLs stable while flipping the rest. None (the default) follows the global setting. (48ca69bafa)
  • Catchall route semantics. path("<path:NAME>") — a sole-segment terminal multi-segment capture — is recognized structurally as a catchall: slash-agnostic at match time, yields to sibling SlashMismatch so a specific route's slash redirect isn't shadowed, and propagates through include() boundaries via ResolverMatch.is_catchall. (90d8fd983b, 48ca69bafa)
  • Error template rendering moved into plain.templates. Plain core's exception handler returns plain text (404 Not Found, 500 Internal Server Error); the {status}.html rendering lives on TemplateView.handle_exception in plain.templates 0.2.0. View-level 5xx attribution still happens in core via _respond_to_exception. (90d8fd983b)
  • URL routing internals refactor. Dropped the Route dataclass — URLPattern and URLResolver hold segments: tuple[Segment, ...] directly, with URLPattern.converters as a cached_property for OpenAPI consumers. Resolver lookup-table construction (_build_lookups, _collect_endpoint, _collect_resolver, _register_namespace) and reverse-URL helpers (_try_reverse, _reverse_segment, _reverse_capture) are now methods on URLResolver. Removed _effective_trailing_slash and the prefix_trailing_slash parameter that threaded through resolve/reverse. (48ca69bafa)
  • include() slashes are irrelevant. include("admin") and include("admin/") are identical mounts. The separator between an include's prefix and its children is enforced structurally by segment matching — /adminhome collisions are impossible regardless of how the route string is spelled. (48ca69bafa)
  • plain urls list renders canonical slashes from segment tuples. Both --flat and the tree view now show the slash form resolve()/reverse() actually produce — URLS_TRAILING_SLASH=True makes endpoints render as admin/home/ rather than admin/home. (48ca69bafa)

Upgrade instructions

  • Add URLS_TRAILING_SLASH = True to app/settings.py to preserve existing slashed-route behavior. Without it, every route's canonical form flips to no-slash; in-flight requests still work via 308 redirects but the URLs you ship in HTML change. Watch for relative URLs in templates<form action=".">, <a href=".">, <a href="./next"> resolve against the request URL's last slash, so flipping /login//login makes action="." POST to / instead of /login. Either keep URLS_TRAILING_SLASH = True, or grep templates for action="\."/href="\." and replace with action="" (empty action submits to the current URL — slash-agnostic) or an explicit {{ url('name') }}.
  • Drop trailing slashes from path()/include() route strings as a cosmetic cleanup — they're stripped silently now. Optional but matches the new convention.
  • Replace path("…/") + matching path("…") for the same view with a single path("…") plus a force_trailing_slash decision. The slash form is no longer the discriminator.
  • url_pattern.route is gone. Reach for url_pattern.segments or url_pattern.converters (the latter replaces url_pattern.route.converters).
  • Catchall routes (path("<path:_>")) drop the slash check at registration. If you previously had path("<path:_>/", ...) to mean "slash-only catchall," it now behaves the same as path("<path:_>", ...).

0.144.0 (2026-05-13)

What's changed

  • URL routing rewritten as a segment-based resolver. plain.urls splits into focused modules: segments.py parses route strings into Literal/Capture/Pattern tuples, patterns.py matches endpoints, resolvers.py walks a prefix tree and builds reverse/namespace lookup tables eagerly at __init__, paths.py normalizes request paths (RFC 3986 dot/slash collapse → 308/400), and reverse.py exposes reverse()/_lazy/_absolute. The regex-decompiler in regex_helper.py is gone; only _lazy_re_compile remains for the rest of the codebase. (5025de26be)
  • path() and include() accept string routes only — raw re.compile(...) is no longer public API. The <converter:name> syntax (<int:>, <str:>, <uuid:>, <path:>, <slug:>) plus register_converter() covers anything the raw-regex form did. RegexPattern and the converter classes (IntConverter, UUIDConverter, etc.) are now internal. URLPattern.pattern narrows to RoutePattern; the _check_pattern_startswith_slash and _check_include_trailing_dollar preflight checks are gone. (28dba1d2ed)
  • Bidirectional 308 trailing-slash redirects; APPEND_SLASH setting removed. The route definition is the source of truth for the canonical URL — path("users/", V) makes /users/ canonical and 308-redirects /users; path("users", V) does the reverse. 308 preserves the HTTP method and body, so POST/PUT/PATCH survive intact (no more silent body loss like 301 caused). RedirectSlashMiddleware is gone. Request.get_full_path() loses its force_append_slash parameter. (db4ac49f72)
  • path() and include() normalize slash boundaries. include("admin"), include("admin/"), and include("/admin/") all resolve to "admin/". path("/users/") is equivalent to path("users/"). Fixes silent corruption where include("admin") (no trailing slash) built URLs like /adminhome/ instead of /admin/home/. (ba35b1f846)
  • Test client follows 307/308 redirects with HTTP-correct semantics: GET/HEAD use the Location's query string instead of re-encoding the original data; POST/PUT/etc. with data=None no longer crashes the multipart encoder. (db4ac49f72)

Upgrade instructions

  • Replace path(re.compile(...), ...) with converter syntax. The <converter:name> form (<int:>, <str:>, <uuid:>, <path:>, <slug:>) plus a custom register_converter() covers anything raw regex did. The /plain-upgrade skill rewrites the common cases.
  • Drop APPEND_SLASH from app/settings.py — it has no effect. Trailing-slash behavior is now decided per-route by whether path("…/") or path("…") is registered.
  • Remove RedirectSlashMiddleware from MIDDLEWARE if you had it.
  • Update request.get_full_path() callers to drop force_append_slash=. There was only one such caller in the framework itself (the deleted middleware).
  • Inline IntConverter/UUIDConverter/_get_converters imports from plain.urls.converters were never public; if you reached into them, switch to register_converter() and check converter.keyword or url_pattern.raw_route / url_pattern.route.converters on the public side.

0.143.0 (2026-05-12)

What's changed

  • plain.templates extracted to a sibling plain.templates package. The Jinja engine, Template/TemplateFileMissing, register_template_global/filter/extension, DefaultEnvironment, the TEMPLATES_JINJA_ENVIRONMENT setting, and the template-touching view classes (TemplateView, FormView, DetailView, CreateView, UpdateView, DeleteView, ListView) all moved out of plain core. plain.views slims to View, RedirectView, ServerSentEventsView; jinja2 is no longer a plain dependency. (19b622a7ca)
  • Plain core's exception handler degrades to plain text when plain.templates isn't installed or registered. The framework still tries to render {status}.html when plain.templates is in INSTALLED_PACKAGES; otherwise it returns a plain-text body. (1d293be8fd)
  • request.path_info removed; use request.path. SCRIPT_NAME handling is dropped entirely — Plain doesn't run as a mounted sub-app. The two attributes were always equal in practice. URL resolution, CSRF exemption matching, and OTel url.path all use request.path consistently now. (69a6897723, 7893013a98)
  • OTel boundary spans aligned with APM error-attribution conventions. Entry spans (SERVER, CONSUMER, PRODUCER) are the only kinds that carry application errors. Chore execution is now a CONSUMER span (plain/cli/chores.py), with error.type + record_exception stamped on failure. (144c3b4822, 05ea71d6d7)
  • View._respond_to_exception is now the single 5xx logging/observability point. Centralized so log_exception + response.exception attachment happens in one place per request. (2634fd1d1c)
  • Stopped emitting exception.escaped on framework spans. The attribute is deprecated upstream and unreliable in the Python SDK. Status + error.type + recorded exception event is the canonical failure signal. (bb9251f165)

Upgrade instructions

  • Install plain.templates if your app renders HTML (almost certainly yes):
    uv add plain.templates
    
  • Add it to INSTALLED_PACKAGES:
    INSTALLED_PACKAGES = [
        ...
        "plain.templates",
    ]
    
  • Rewrite view imports: anything that was from plain.views import TemplateView (or FormView, DetailView, CreateView, UpdateView, DeleteView, ListView) is now from plain.templates.views import .... The /plain-upgrade skill rewrites these automatically.
  • Rewrite from plain.templates import Template, register_template_* — the import path is unchanged, but you now need the plain.templates package installed for those imports to resolve at all.
  • Drop any direct use of request.path_info — replace with request.path. They've been equal in practice; there's no behavior change beyond the name.
  • Custom subclasses of Request that accepted a path_info= constructor kwarg must drop it.

0.142.0 (2026-05-12)

What's changed

  • plain.assets extracted to a sibling plain.assets package. The asset finder, manifest, compile pipeline, URL routing, view, settings (ASSETS_REDIRECT_ORIGINAL, ASSETS_CDN_URL, ASSETS_LOG_304), and the asset() template global all moved out of plain core into a dedicated plain.assets package. Core no longer carries any HTML-asset-serving code or settings. This fixes a layering inversion (core templates previously imported from plain.assets.urls) and lets REST-only services skip installing it entirely. (844f46e428)
  • plain build removed; use plain assets build instead. The build orchestrator (user [tool.plain.build.run] commands + plain.build entry points + asset compile) lives in plain.assets now. plain.tailwind and plain.esbuild keep registering plain.build entry points; the runner that iterates them just moved.

Upgrade instructions

  • Install plain.assets:
    uv add plain.assets
    
  • Add it to INSTALLED_PACKAGES:
    INSTALLED_PACKAGES = [
        ...
        "plain.assets",
    ]
    
  • Replace plain build with plain assets build in deploy scripts, Procfiles, and CI.
  • from plain.assets import ... continues to work as a namespace import, but you must have plain.assets installed for the package's INSTALLED_PACKAGES registration (CLI command, template global, default settings) to take effect.

0.141.1 (2026-05-08)

What's changed

  • Annotation-only custom settings now work. Declaring APP_FOO: str (no value) in app/settings.py previously dropped the setting on the floor — dir() doesn't include unassigned names, so PLAIN_APP_FOO env vars were ignored and settings.APP_FOO raised AttributeError. They now register as required settings, with from __future__ import annotations string annotations resolved via typing.get_type_hints() so env parsing receives int and not 'int'. (71535d6a30)

Upgrade instructions

  • No changes required. If you were working around this by assigning a sentinel default, you can drop it.

0.141.0 (2026-05-07)

What's changed

  • New UnsupportedMediaTypeError415 exception in plain.http. request.json_data and request.form_data previously raised plain ValueError when the Content-Type didn't match what they parse, which fell through to a generic 500. They now raise UnsupportedMediaTypeError415 (an HTTPException subclass with status_code=415) so APIView and downstream handlers can render it as a clean 4xx without per-endpoint try/except. (f30d2ef3f6cd)

Upgrade instructions

  • If you were catching ValueError around request.json_data / request.form_data to handle bad Content-Type, you can drop the try/except — the new 415 will be rendered automatically by any view that handles HTTPException (including APIView).

0.140.1 (2026-05-06)

What's changed

  • 5xx response span exceptions now carry exception.escaped=True. The OTel SDK's auto-record path stamps escaped=False, which made the attribute useless for distinguishing workflow-level failures (a request that 500'd) from internal child-span exceptions. The SERVER span now records via the explicit escaped=True path so downstream tooling can filter on it. (e9c35b855f)

Upgrade instructions

  • No changes required.

0.140.0 (2026-05-05)

What's changed

  • plain shell now runs piped/heredoc stdin through plain.runtime.setup() instead of relying on PYTHONSTARTUP. Previously echo "User.query.count()" | plain shell raised PackageRegistryNotReady because PYTHONSTARTUP is only honored for interactive sessions. Non-TTY stdin is now detected and exec'd after setup, matching the -c and plain run paths. Fixes #66. (8777eb40)
  • Log formatter no longer pollutes records for downstream handlers. The keyvalue/JSON formatters were setting working attrs on vars(record) for the lifetime of the process, so any other handler reading the record afterwards would ship those as extra fields. Mutations are now scoped to the format() call. (c793ed71)
  • Tightened type annotations across plain.http.request, plain.server.http.request, plain.test.client, and plain.utils.inspect for ty 0.0.33 — introduces a RequestStream Protocol for Request._stream. (4b9d1db1)

Upgrade instructions

  • No changes required.

0.139.0 (2026-04-30)

What's changed

  • T | None settings now unwrap from env vars without JSON quoting. A setting annotated str | None (or any single-arm union with None) accepts PLAIN_X=value directly — empty string maps to None, anything else parses as T. Previously you had to JSON-encode the value (PLAIN_X='"value"') for nullable types. Richer unions still fall through to JSON. (602ce084b017)

Upgrade instructions

  • No changes required.

0.138.0 (2026-04-30)

What's changed

  • Dropped --section and --outline from plain docs. Both flags are removed from the CLI surface. --search covers the same ground (it returns full matching sections, scoped to a module if you give one), and a plain plain docs <name> is short enough to read top to bottom. The internal _extract_section helper is simplified to ## sections only, and the agent-rules / skill prompts that referenced the old flags are updated. (e03c3bd8b6d3)
  • Tidied the preflight count loop in get_check_counts() and the unused CheckRegistry.get_checks() helper was removed. Preflight imports are also reformatted onto multiple lines for readability. No behavior change. (687d2f53a036)

Upgrade instructions

  • Replace any plain docs <module> --section <name> invocations with plain docs <module> --search <term> (which prints all sections matching <term>), or just plain docs <module> and read the section directly.
  • Replace any plain docs --outline / plain docs <module> --outline invocations with plain docs --search <term> to find headings, or plain docs <module> for the full doc.
  • If you import get_checks from plain.preflight.registry, switch to iterating run_checks(...) directly — get_checks() was unused and has been removed.

0.137.1 (2026-04-28)

What's changed

  • plain docs --search now matches inside fenced code blocks. API names like CheckConstraint mostly appear in code examples, so skipping fenced blocks meant exact-symbol queries returned nothing. The search now matches both prose and code, while still preferring a prose line for the section preview when one exists. (83af8fb1ac66)

Upgrade instructions

  • No changes required.

0.137.0 (2026-04-27)

What's changed

  • Added plain.test.otel for capturing OpenTelemetry signals in tests. New install_test_tracer() and install_test_meter() helpers install in-memory providers and return an InMemorySpanExporter / InMemoryMetricReader so tests can read the spans and metrics emitted during a request. Both are idempotent — they handle OpenTelemetry's install-once-per-process global providers — so they're safe to call from multiple test modules. If you're using plain.pytest, prefer the new otel_spans / otel_metrics fixtures. (e650b556447f)
  • Documented the OTel context propagation in the request handler. No behavior change — the existing request_ctx.run(context.attach, context.get_current()) line is now flagged as load-bearing with a comment explaining why removing it would silently turn child spans into root spans. (aeaef9fd6a8e)

Upgrade instructions

  • No changes required.

0.136.1 (2026-04-26)

What's changed

  • Fixed empty-string routes losing their OTel http.route attribute and span name. Homepage routes (URL pattern "") were being treated like unmatched 404s because the truthy-check on resolver_match.route skipped them. Switched to is not None so matched routes with empty patterns get http.route="/" and a span name of "GET /" (or the matching method). (57c3255b36cc)

Upgrade instructions

  • No changes required.

0.136.0 (2026-04-24)

What's changed

  • View is now generic over its handler return type. The base class is View[HandlerResult = Response], and get / post / put / patch / delete / head are typed to return HandlerResult instead of Response. Subclasses that accept richer return types (e.g. APIView in plain-api) can parameterize the base as View[APIResult] and have their handler signatures reflect that in type checkers. (9c0c12df13fd)
  • Renamed View.convert_value_to_responseView.convert_result_to_response. The parameter is now result: HandlerResult instead of value: Any, which keeps the type flowing through the generic parameter. (11c8fe16b544)

Upgrade instructions

  • If you overrode convert_value_to_response on a View subclass — rename it to convert_result_to_response and update the parameter name from value to result. The signature is otherwise unchanged.

0.135.0 (2026-04-23)

What's changed

  • Merged ResponseBase into Response as the common supertype. ResponseBase is no longer exported from plain.httpResponse is now the base class for StreamingResponse and AsyncStreamingResponse. Type hints across the framework (View.after_response, View.handle_exception, View.convert_value_to_response, etc.) use Response. (f5007281d7fa)

Upgrade instructions

  • If you imported ResponseBase — replace it with Response. from plain.http import Response covers bytes-body, streaming, and async-streaming responses.

0.134.0 (2026-04-22)

What's changed

  • Removed plain.signals. The request_started / request_finished built-in signals and the entire plain.signals module (including plain.signals.dispatch.Signal, receiver, and the dispatcher internals) are gone. Plain no longer emits request_started / request_finished around request handling — pool-backed connection lifecycle in plain-postgres replaced the one consumer. (2a51b25)
  • Narrowed View handler return types to Response. get, post, put, patch, delete, and head must now return a Response (or subclass like JsonResponse, RedirectResponse). The shorthand coercion that accepted str, int, dict, list, tuple, and None has moved to APIView in plain-api — base views raise TypeError if you return something that isn't a ResponseBase. TemplateView.get and the form_valid hooks on CreateView / UpdateView / DeleteView are typed to return Response too. (1935f3f)
  • Per-request contextvars.Context bridges async view pipelines. Each request now runs through a single contextvars.Context — sync executor hops go through ctx.run(...) and async view coroutines run on a task bound to that context. before_request, the view, and after_response see a consistent view of request-scoped ContextVars (e.g. the plain-postgres connection wrapper) even when they land on different worker threads. Starts from a fresh context rather than copying ambient state, so stale values from a previous keep-alive request's streaming body can't leak in. (2a51b25)

Upgrade instructions

  • If you imported anything from plain.signals — remove the imports. request_started / request_finished listeners need to move off signals; hook into the request lifecycle with middleware or View.before_request / View.after_response instead. Custom Signal() usage has no replacement in Plain — roll your own event dispatch or use an external library if you need it.
  • If a View handler returned a dict, list, str, int, tuple, or None — wrap it in a Response (or JsonResponse(...), Response(status_code=...), etc.). For JSON APIs that want the shorthand back, switch to plain.api.APIView — it accepts dict/list/int/tuple/None returns via its convert_value_to_response override.
  • If you subclassed View and overrode convert_value_to_response — it now receives an Any-typed value and must raise TypeError for non-Response inputs unless your override coerces them. Re-check your logic against the trimmed base implementation.

0.133.0 (2026-04-21)

What's changed

  • Added View lifecycle hooks: before_request, after_response, and handle_exception. before_request runs before the method handler (raise to reject the request). after_response runs after the response is built — including error responses — and can mutate or replace it. handle_exception converts an exception raised during dispatch into a response; the base re-raises to defer to the framework error renderer. Subclasses can now override these hooks instead of wrapping get_response(). (c1234c14be1d, 0da5639d17e2, 48effac976a9)
  • Added HTTPException base class in plain.http. All HTTP exceptions (BadRequestError400, ForbiddenError403, NotFoundError404, etc.) now inherit from it and carry a status_code attribute. Subclass it to define your own status-mapped exceptions (e.g. class PaymentRequiredError402(HTTPException): status_code = 402). FormFieldMissingError and MultiPartParserError now subclass BadRequestError400. (48effac976a9)
  • Restricted View dispatch to IANA HTTP methods. Handlers for get, post, put, patch, delete, head, and options dispatch; TRACE and CONNECT are no longer dispatched to. Non-HTTP-method attribute names on a view class can no longer collide with the dispatcher. (5da708a057db)
  • View now tracks implemented methods via __init_subclass__. A View.implemented_methods frozenset is computed once per subclass, replacing the runtime hasattr() scan. This is what OpenAPI generation and Allow: header construction should read. (23baeea0653a)
  • HEAD now falls back to get at dispatch time without mutating self.head. Previously __init__ aliased self.head = self.get, which polluted the instance. (5848c010d66c)
  • Error rendering simplified. The internal ErrorView / TemplateView-based path is gone — response_for_exception now renders {status}.html directly via Template(...).render(...) and falls back to a plain-text body if the template is missing. (48effac976a9)
  • Added plain.logs.log_exception. A single idempotent entry point for request-exception logging used by both View.get_response and the framework error renderer. A sentinel on the exception prevents double-logging when it surfaces at multiple layers. (48effac976a9)
  • Exception logging deferred to handle_exception. Logging now happens once — inside View.get_response when an exception reaches the handler. ResponseException is unwrapped before handle_exception runs, so overrides focus purely on response shape. Returning a response from handle_exception suppresses logging (the view opted to map the exception to a handled outcome); re-raising lets the framework log and render {status}.html. (48effac976a9)

Upgrade instructions

  • If you override get_response() on a subclass of View, migrate to the new hooks — before_request for pre-handler work, after_response(response) for post-handler mutation, handle_exception(exc) for exception-to-response mapping.
  • If you catch specific HTTP exception classes, nothing changes — they still exist. If you want your own status-mapped exceptions, subclass HTTPException with a status_code attribute instead of catching and re-raising.
  • If you import ErrorView from plain.views, it has been removed. Use the default framework renderer (which reads {status}.html) or override View.handle_exception for custom formats.
  • If you read hasattr(view, "get") / hasattr(view, "post") to detect implemented methods, switch to view.implemented_methods (a frozenset of lowercase method names) — base View now provides stub handlers for every verb.

0.132.1 (2026-04-14)

What's changed

  • Updated the chores README example to reflect the new QuerySet.delete() return type (int instead of a (count, by_label) tuple). (29e10dba51d9)

Upgrade instructions

  • No changes required.

0.132.0 (2026-04-13)

What's changed

  • Removed AUTH_USER_MODEL setting. The User model is now fixed at app.users.models.User — a required convention, not a configurable setting. Code that used SettingsReference("AUTH_USER_MODEL") or imported from a user-configured location must be updated. (0861c9915cb6)
  • Removed plain.runtime.SettingsReference. It was only used to defer resolution of AUTH_USER_MODEL in migrations, which no longer exists. (0861c9915cb6)
  • Made FormView generic over its form type. FormView[MyForm] now types self.form and related methods with the concrete form class. (8dbe9e413d30)
  • Server now closes listener sockets immediately on SIGTERM. Prevents new connections from landing on a worker that's about to exit, which could cause H13 errors on Heroku and similar platforms. (5fb7c2fb482f)
  • Updated plain request --user to resolve users via app.users.models.User instead of get_user_model(). (0861c9915cb6)
  • Migrated type suppression comments to ty: ignore and upgraded the ty checker to 0.0.29. (4ec631a7ef51)

Upgrade instructions

  • Move your User model to app/users/models.py (package label users, class name User) if it isn't already there. Remove AUTH_USER_MODEL from settings.py.
  • If you referenced plain.runtime.SettingsReference, remove the usage — the class no longer exists.

0.131.3 (2026-04-05)

What's changed

  • Fixed OTel baggage data leak. Request cookies and headers were passed to the observer sampler via OTel baggage, which propagates to downstream services. Any instrumented outbound HTTP client would have serialized cookies and auth tokens into the baggage: header. Now uses process-local context values instead. (b56a9edc9c7d)
  • HTTP server span name no longer includes raw URL path. Span names start as just the HTTP method (e.g. GET) and are updated to GET /users/<id>/ after URL resolution. Previously, the raw path was used from the start, causing high-cardinality span names for 404s and middleware failures. (b56a9edc9c7d)
  • Removed url.full from HTTP server spans. The HTTP semconv doesn't define url.full for server spans — url.path and url.query are already set separately. (b56a9edc9c7d)
  • Added recommended HTTP server span attributes. server.address, server.port, client.address, and user_agent.original are now set on HTTP server spans. (b56a9edc9c7d)
  • Unknown HTTP methods normalized to _OTHER. Per the HTTP semconv, unrecognized methods are now set to _OTHER with the original value in http.request.method_original. (b56a9edc9c7d)
  • Added error.type to HTTP server spans. Set to the exception class name on 5xx with an exception, or the status code string for 5xx without one. (b56a9edc9c7d)
  • Added network.protocol.name to http.server.request.duration metric. (b56a9edc9c7d)
  • Removed set_status(OK) from HTTP server span. Per the OTel spec, instrumentation libraries should leave span status as Unset on success. (b56a9edc9c7d)
  • Template tracer renamed from plain to plain.templates. (b56a9edc9c7d)
  • Template code.namespace replaced with fully-qualified code.function.name. Uses the stable semconv attribute. (b56a9edc9c7d)
  • Added plain.utils.otel module with format_exception_type() helper shared across packages. (b56a9edc9c7d)

Upgrade instructions

  • No changes required.

0.131.2 (2026-04-03)

What's changed

  • OTel span status and error.type metric now only flag 5xx responses as errors. Previously, 4xx responses were marked as StatusCode.ERROR on server spans and included error.type in request duration metrics. Per the OpenTelemetry HTTP semantic conventions, only 5xx responses should be treated as server errors — 4xx is expected behavior from the server's perspective. (1f058a6119ce)

Upgrade instructions

  • No changes required.

0.131.1 (2026-04-02)

What's changed

  • ServerSentEventsView.get() is now sync. The get() method was unnecessarily async — it only constructs an AsyncStreamingResponse without awaiting anything. Making it sync fixes compatibility with AuthView and other view mixins that override get_response(), which previously received a coroutine instead of a response object. (890d0d6ddb5a)

Upgrade instructions

  • If you override get() on a ServerSentEventsView subclass with async def get(self), change it to def get(self). The stream() method remains async — no changes needed there.

0.131.0 (2026-04-01)

What's changed

  • Added http.server.request.duration OTel histogram to the request handler. Records request duration in seconds with standard HTTP semantic convention attributes (http.request.method, http.response.status_code, url.scheme, http.route, error.type). (c40bfd42bdd8)

Upgrade instructions

  • No changes required.

0.130.2 (2026-04-01)

What's changed

  • plain request --data now auto-detects content type. If --content-type is not specified, JSON data (starting with { or [) is sent as application/json, otherwise as application/x-www-form-urlencoded. (0af889455ffa)

Upgrade instructions

  • No changes required.

0.130.1 (2026-03-29)

What's changed

  • Indented preflight check lines and summary under the "Running preflight checks..." header for readability in environments without ANSI colors. (b6b494dcc698)

Upgrade instructions

  • No changes required.

0.130.0 (2026-03-29)

What's changed

  • plain check now uses postgres sync --check instead of separate migrate --check and makemigrations --check calls. This validates migrations, pending model changes, and convergence in a single step. (13bd4b963394)
  • Removed the top-level makemigrations and migrate shortcut commands. Use migrations create and migrations apply (under plain postgres), or plain postgres sync for the combined workflow. (adf021688bf3, b026895edc4c)

Upgrade instructions

  • Replace plain migrate with plain postgres sync and plain makemigrations with plain migrations create in scripts and CI. Requires plain-postgres>=0.91.0.

0.129.0 (2026-03-27)

What's changed

  • Renamed forms.CharField to forms.TextField — all subclasses (EmailField, URLField, UUIDField, JSONField, RegexField) now extend TextField (4e29f5d6cade)
  • **Replaced **kwargswith explicit parameters in Response classes** —Response, StreamingResponse, AsyncStreamingResponse, FileResponse, RedirectResponse, NotModifiedResponse, NotAllowedResponse, and JsonResponse now declare all parameters explicitly (7d1cb9af3a06)
  • Removed signing.dumps() and signing.loads() — use TimestampSigner(salt=...).sign_object() / .unsign_object() or Signer directly (99b0e57bc175)
  • Disabled worker recycling in reload mode — file-change restarts already recycle workers, so max_requests retirement was causing unnecessary extra restart cycles during development (8eb9c6da485e)
  • Added TYPE_CHECKING stubs for View handler methods (get, post, put, patch, delete, head, trace) to improve IDE autocomplete and type checking (ca7ee03424d1)

Upgrade instructions

  • Rename forms.CharField to forms.TextField in all form definitions.
  • Replace signing.dumps(obj, salt=..., ...) with TimestampSigner(salt=...).sign_object(obj, ...) and signing.loads(s, salt=..., ...) with TimestampSigner(salt=...).unsign_object(s, ...).
  • If you pass **kwargs to Response subclasses, switch to named keyword arguments (content_type=, status_code=, headers=, etc.).

0.128.0 (2026-03-26)

What's changed

  • Zero-downtime worker recycling — when a worker hits SERVER_MAX_REQUESTS, it now signals for a replacement via a shared-memory flag and keeps serving traffic. The arbiter pre-spawns a replacement and only shuts down the retiring worker once the replacement is heartbeating, eliminating the capacity gap that previously occurred during worker recycling. (9eaeded599fa)

Upgrade instructions

  • No changes required.

0.127.2 (2026-03-24)

What's changed

  • Fixed post() data type annotation in test client (RequestFactory and Client) to accept Any instead of dict[str, Any] | None, allowing JSON payloads and other data types (a67018f94cfb)
  • Updated agent rules and skill descriptions (669e52eda37d, bdff05dfb9f6, 1be549a7fd31)

Upgrade instructions

  • No changes required.

0.127.1 (2026-03-22)

What's changed

  • Added plain-portal to workspace, package list, and plain docs registry (7c782e15a962)
  • Consistent format for server worker log messages (905c4f2ea051)

Upgrade instructions

  • No changes required.

0.127.0 (2026-03-20)

What's changed

  • Container-aware system metrics in plain.utils.os — added get_rss_bytes(), get_memory_usage(), and get_process_cpu_percent() helpers that work inside cgroups v1/v2 containers. Also added cgroup v1 CPU quota support to get_cpu_count() (40482feb2b)
  • Test client now creates OTel spansplain.test.Client wraps each request in a server span, so observer traces are captured during tests (aa54f27d95)
  • plain request --user accepts email addresses — in addition to user IDs, with a clearer error when plain-auth is not installed (aa54f27d95)
  • Updated the plain-upgrade skill to include a "plain agent install" step (5cae05a696)

Upgrade instructions

  • No changes required.

0.126.0 (2026-03-20)

What's changed

  • Worker recycling — workers now gracefully restart after a configurable number of requests to prevent memory accumulation from fragmentation, C extension leaks, or unbounded caches. Controlled by new SERVER_MAX_REQUESTS (default 1000, 0 to disable) and SERVER_MAX_REQUESTS_JITTER (default 100) settings. Both HTTP/1.1 requests and HTTP/2 streams count toward the limit (e953f62609)
  • Structured logging throughout the framework — all plain.* loggers now use the same key-value / JSON formatters as app_logger instead of bare [LEVEL] message format. Log messages use stable, greppable sentence fragments with variable data passed as structured extra={} fields rather than %s interpolation (75a8b60c91)
  • get_framework_logger() factory — new public function in plain.logs that auto-derives logger names from the caller's module (e.g. plain.server.workers.entry becomes plain.server), replacing scattered logging.getLogger("plain.xxx") calls across the codebase (2e25cae784)
  • AppLogger renamed to PlainLogger — the logger class in plain.logs has been renamed and moved from app.py to logger.py. The app_logger instance and its API are unchanged. A new exception() method was added to support context={} on exception logs (b79829ddbb)
  • Flat extra for structured formattersPlainLogger._log() now merges persistent context, extra, and per-call context into flat top-level LogRecord attributes instead of nesting under extra["context"]. The KeyValueFormatter and JSONFormatter extract context by diffing against standard LogRecord attributes, so both app_logger and standard plain.* loggers produce identical structured output (5148b2bc31)
  • Response body size in traces — the http.response.body.size OpenTelemetry attribute is now set on server spans for non-streaming responses, making response sizes visible in observer traces and the toolbar (46f981ff80)
  • Cgroup-aware CPU count shared via plain.utils.os — the get_cpu_count() helper was moved from the server CLI to plain.utils.os so the jobs worker can also use it for container-aware process counts (aa0e57b7eb)

Upgrade instructions

  • If you subclassed or imported AppLogger directly, update to PlainLogger (import path changed from plain.logs.app to plain.logs.logger).
  • If you relied on extra["context"] being a nested dict on log records from app_logger, note that context keys are now flat top-level attributes on the LogRecord. Standard extra={} usage and context={} on app_logger calls are unaffected.
  • Review any custom log parsing that expected [LEVEL] message format from plain.* loggers — they now use the same formatter as app_logger (key-value or JSON depending on APP_LOG_FORMAT).

0.125.0 (2026-03-19)

What's changed

  • plain memory baseline — new command that measures per-package memory cost at worker boot time, showing which dependencies are heaviest. Runs in an isolated subprocess for accurate measurements (4b747665fc2a)
  • plain memory leaks — new command that detects memory leaks on a running server. Uses a three-phase tracemalloc approach (snapshots A, B, C) and reports only allocations that grew in both halves, filtering one-time initialization noise. Includes auto-stop safety timeout, atomic file writes, and current RSS on Linux via /proc/self/statm (cdd7b2def319)
  • Server arbiter now handles SIGUSR1 to forward memory recording signals to all workers (cdd7b2def319)

Upgrade instructions

  • No changes required.

0.124.1 (2026-03-16)

What's changed

  • Added /plain-guide skill for researching framework questions using plain docs and source code (16597aa560af)
  • plain docs --search now supports regex patterns instead of only literal string matching (1b494cbe7d8f)

Upgrade instructions

  • No changes required.

0.124.0 (2026-03-12)

What's changed

  • Updated all references from plain.models to plain.postgres across views, CLI docs, registry docstrings, README doc links, and agent rules.

Upgrade instructions

  • Update imports: from plain.models to from plain.postgres, from plain import models to from plain import postgres.

0.123.4 (2026-03-12)

What's changed

  • Read cgroup v2 CPU quota for accurate container worker countos.process_cpu_count() only checks sched_getaffinity, not cgroup CPU quotas, so containers still saw the host CPU count. Now reads /proc/self/cgroup to resolve the process's cgroup path, then parses cpu.max for the actual quota. Uses ceiling division so fractional vCPUs (e.g. 1.5) round up to 2 workers. Silently falls through on non-Linux systems (32785b4634e8)

Upgrade instructions

  • No changes required.

0.123.3 (2026-03-12)

What's changed

  • Fix auto worker count in Docker/cgroup environments — replaced os.cpu_count() with os.process_cpu_count() (Python 3.13+) for cgroup-aware CPU detection. Previously, containers would see the host's CPU count instead of their allocated limit, spawning far too many workers (e.g. 48 on a 2 vCPU container) (c1e2c186c3aa)

Upgrade instructions

  • No changes required.

0.123.2 (2026-03-12)

What's changed

  • Server startup log now includes workers and threads — the startup message shows workers=N threads=N so you can confirm the server configuration at a glance (e63d9f90520c)
  • H2 max concurrent streams setting registered with default of 100SERVER_H2_MAX_CONCURRENT_STREAMS is now a proper setting with a default value instead of using getattr with a fallback (e72a4006515f)

Upgrade instructions

  • No changes required.

0.123.1 (2026-03-12)

What's changed

  • Health check moved from middleware to server event loop — the HEALTHCHECK_PATH endpoint now responds directly on the async event loop with a raw 200 OK before the request reaches the thread pool or any middleware. This means health checks continue to work even when the thread pool is fully saturated. Supports both HTTP/1.1 and HTTP/2 connections (ef8f020a86dc)
  • Removed HealthcheckMiddleware — no longer needed since the server handles health checks directly (ef8f020a86dc)

Upgrade instructions

  • No changes required.

0.123.0 (2026-03-11)

What's changed

  • Open redirect protection for RedirectResponse — external URLs are now rejected by default. Pass allow_external=True to explicitly allow redirects to external hosts (OAuth, CDN, etc.). Detects scheme-based URLs (http://, https://, ftp://), protocol-relative (//), and backslash variants (/\, \\) with whitespace stripping to prevent bypass attacks (5edfb2bedf90)
  • RedirectView.allow_external attribute — class-based redirect views now support allow_external = True for views that intentionally redirect to external URLs (5edfb2bedf90)

Upgrade instructions

  • If your code passes external URLs to RedirectResponse (e.g., OAuth providers, CDN URLs, SSO login pages), add allow_external=True:

    # Before
    RedirectResponse("https://example.com/callback")
    
    # After
    RedirectResponse("https://example.com/callback", allow_external=True)
    
  • For RedirectView subclasses that redirect externally, set allow_external = True on the class.

  • Relative paths, query-only URLs, and other internal redirects continue to work without changes.

0.122.1 (2026-03-11)

What's changed

  • Fixed is_bound for forms with no fields (e.g., delete confirmation forms) — empty POST requests produced a falsy QueryDict, causing the form to never validate. is_bound now checks the request method instead of data truthiness (f630b3b5fb22)

Upgrade instructions

  • No changes required.

0.122.0 (2026-03-10)

What's changed

  • Secret is now type-transparentSecret[str] is a type alias for Annotated[str, _SecretMarker()], so type checkers see the underlying type directly. No more type: ignore comments needed on default values (a90197b95315, 997afd9a558f)
  • TYPE_CHECKING ignored in settings modules — settings loading now skips TYPE_CHECKING (and other uppercase non-setting names) so you can use from __future__ import annotations and if TYPE_CHECKING: blocks in settings files without triggering duplicate-setting errors (f20869e0bd2b)
  • Adopted PEP 695 type parameter syntax (def foo[T]() instead of TypeVar) across the codebase (aa5b2db6e8ed)

Upgrade instructions

  • If you previously had type: ignore[assignment] comments on Secret default values, you can remove them.
  • No other changes required.

0.121.2 (2026-03-10)

What's changed

  • Added prescriptive "After making code changes" section to AI rules with plain check and plain request guidance (772345d4e1f1)
  • Distributed Django differences into package-specific rules (plain-models, plain-templates) instead of one monolithic list (772345d4e1f1)
  • Added request.query_params, request.form_data, request.json_data, request.files to Django differences (772345d4e1f1)

Upgrade instructions

  • No changes required.

0.121.1 (2026-03-10)

What's changed

  • Worker SIGTERM exits logged at info instead of error — during graceful shutdown (e.g. Heroku deploy), workers exit with SIGTERM which is expected behavior. These are now logged at info level instead of error, preventing false alerts in error tracking (1c3908d27aea)
  • Removed duplicate log messages for signal-killed workers — workers killed by a signal previously produced two error log lines (generic exit code + signal name). Now only the more descriptive signal-specific message is logged (1c3908d27aea)
  • Removed stack traces from intentional 4xx exception loggingPermissionDenied, MultiPartParserError, and BadRequestError400 exceptions no longer include exc_info in their log entries, reducing noise in error tracking (c395232acdb9)
  • Handle asyncio ConnectionResetError without errno — asyncio's _drain_helper raises ConnectionResetError('Connection lost') without an errno, which bypassed the existing errno-based check. Now caught explicitly before the OSError errno check (b623d4f78667)
  • Removed type: ignore comments across multiple modules with proper type fixes (cda461b1b4f6, f56c6454b164)

Upgrade instructions

  • No changes required.

0.121.0 (2026-03-09)

What's changed

  • Replaced threading.local() with ContextVar for async compatibility — timezone activation (plain.utils.timezone) and URL resolver population tracking now use ContextVar instead of threading.local(), making them safe for use in async contexts where multiple coroutines share a thread (e5c4073cafbc)
  • Graceful handling of client disconnects — the server now catches OSError during keepalive waits (e.g. client TCP reset) and ConnectionError during connection handling, preventing noisy tracebacks from abrupt client disconnects (e3aee49b32ac)
  • Updated BaseHandler._run_in_executor to propagate only the OpenTelemetry span context into executor threads, intentionally leaving DB connection ContextVars on their native thread context so connections persist across requests honoring CONN_MAX_AGE (cc2469b1260a)

Upgrade instructions

  • No changes required.

0.120.1 (2026-03-08)

What's changed

  • Simplified TLS handling using asyncio.start_server(ssl=...) — TLS is now handled at connection accept time by asyncio instead of a manual two-step process (accept raw socket, then handshake). This eliminates the _async_tls_handshake method, the handed_off flag, and all raw socket I/O fallback paths. Connection now always receives asyncio.StreamReader/StreamWriter instead of a raw socket, removing dual-path code in recv, sendall, wait_readable, close, and Response._async_send (c1e172eec835)
  • Removed RequestParser, SocketUnreader, IterUnreader, and several raw socket utility functions (close, write, write_chunk, write_nonblock, async_recv, async_sendall, has_fileno, ssl_wrap_socket) that are no longer needed (c1e172eec835)
  • Removed synchronous Response write methods (send_headers, write, sendfile, write_file, write_response, close) and the SERVER_SENDFILE setting — all response writing now uses the async path (c1e172eec835)
  • Connection backpressure now uses asyncio.Semaphore instead of a manual asyncio.Event with count checks (c1e172eec835)

Upgrade instructions

  • No changes required.

0.120.0 (2026-03-07)

What's changed

  • Stalled thread pool detection in worker heartbeat — the worker now submits a no-op to the thread pool during each heartbeat cycle and waits for it to complete within the timeout window. If the thread pool is stalled (all threads blocked), the heartbeat stops, causing the arbiter to kill and restart the worker automatically (ce09f41a9db5)

Upgrade instructions

  • No changes required.

0.119.0 (2026-03-07)

What's changed

  • Removed url_args and positional URL arguments — views no longer receive positional URL arguments via self.url_args. All URL parameters are now keyword-only via self.url_kwargs. reverse() and reverse_absolute() no longer accept *args — use keyword arguments instead. RegexPattern now rejects unnamed capture groups and requires named groups ((?P<name>...)) (6eecc35ff197)
  • Server I/O moved to async event loop — all network I/O (TLS handshakes, reading requests, writing responses, keep-alive) now runs on the asyncio event loop. The thread pool is reserved exclusively for application code (middleware and views). This improves connection handling efficiency and eliminates H2 reader threads (322fd51cf206, f60bcdd6f4d2)
  • New SERVER_CONNECTIONS setting — controls the maximum number of concurrent connections per worker (default: 1000). Replaces implicit connection limits (322fd51cf206)
  • New SERVER_H2_MAX_CONCURRENT_STREAMS setting — optionally limits the number of concurrent HTTP/2 streams per connection (322fd51cf206)
  • Asyncio debug mode in development — when DEBUG=True, the server enables asyncio debug mode which logs warnings when a callback blocks the event loop for more than 100ms, helping catch blocking calls in async views (3afdae32ef94)
  • Added documentation for async view safety, server request lifecycle, and three-layer architecture (3afdae32ef94, ca82fb46ad7e)

Upgrade instructions

  • Replace self.url_args with self.url_kwargs in all views. If you used positional URL arguments with unnamed regex groups, convert them to named groups ((?P<name>...)).
  • Replace any reverse(name, arg1, arg2) calls with reverse(name, param1=arg1, param2=arg2) using keyword arguments.
  • Replace any reverse_absolute(name, arg1) calls similarly.
  • In templates, replace url("name", arg1) with url("name", param1=arg1) using keyword arguments.
  • No server configuration changes are required — the new settings have sensible defaults.

0.118.0 (2026-03-06)

What's changed

  • Removed as_view() and setup() from View — views are now passed as classes directly to URL patterns (e.g., path("foo/", MyView) instead of path("foo/", MyView.as_view())). The setup() method is removed; request, URL args, and URL kwargs are set directly on the view instance. View.__init__ no longer accepts arguments. The view_class attribute moved from URLPattern.view to URLPattern.view_class directly (0d0c8a64cb45)
  • Removed --pidfile option from server — the --pidfile CLI option and SERVER_PIDFILE setting have been removed (3ac519e691b2)
  • Removed --max-requests option from server — the --max-requests CLI option and SERVER_MAX_REQUESTS setting have been removed (b48cdbafad33)
  • Unified server dispatch through handler.handle(), consolidating the request pipeline for both HTTP/1.1 and HTTP/2 (e47efeb99332)
  • Fixed thread affinity in production request pipeline to ensure views run on the correct thread (6827fe551702)

Upgrade instructions

  • Replace all MyView.as_view() calls in URL patterns with just MyView.
  • Remove any **kwargs passed to as_view() — set attributes on the class or override methods instead.
  • If you override setup() in a view, move that logic to get(), post(), or another view method.
  • Remove any --pidfile or --max-requests flags from server invocations and the SERVER_PIDFILE / SERVER_MAX_REQUESTS settings.

0.117.1 (2026-03-06)

What's changed

  • Fixed 500 error handling to pass the actual exception to ErrorView instead of None, allowing error views to access exception details (463177c8f0fa)

Upgrade instructions

  • No changes required.

0.117.0 (2026-03-06)

What's changed

  • Async view dispatch — views can now be async. The server detects async view methods and awaits them directly on the worker's asyncio event loop instead of dispatching to the thread pool, freeing thread pool slots for sync views (b62c283ecd3d)
  • AsyncStreamingResponse — new response class that streams from an async generator without occupying a thread pool slot. Available as from plain.http import AsyncStreamingResponse (b62c283ecd3d)
  • ServerSentEventsView — new view class for Server-Sent Events. Subclass it and implement an async events() generator that yields SentEvent objects. Handles SSE framing, Cache-Control, and X-Accel-Buffering headers automatically (b62c283ecd3d)

Upgrade instructions

  • No changes required.

0.116.0 (2026-03-06)

What's changed

  • HTTP/2 support — TLS connections now automatically negotiate HTTP/2 via ALPN. HTTP/2 requests are handled with full stream multiplexing using the h2 library, while HTTP/1.1 clients continue to work as before. Each HTTP/2 stream is dispatched to the thread pool independently, and idle connections time out after 5 minutes (c4d3a33671c6)
  • Middleware refactored to before/after phasesHttpMiddleware no longer uses the onion/wrapper model with process_request() and self.get_response(). Instead, middleware now implements before_request(request) -> Response | None (return a response to short-circuit, or None to continue) and after_response(request, response) -> Response (modify and return the response). The middleware pipeline runs before_request forward through the chain, then after_response in reverse. Middleware __init__ no longer receives get_response (9a1477ee8fa8)
  • Updated server architecture diagram and README to document HTTP/2 and the new middleware model (c4d3a33671c6, 9a1477ee8fa8)

Upgrade instructions

  • Rename process_request(self, request) to before_request(self, request) in custom middleware. The method should return None to continue to the next middleware/view, or return a Response to short-circuit.
  • Move any post-response logic (code after self.get_response(request)) into a new after_response(self, request, response) method that returns the response.
  • Remove self.get_response(request) calls — the framework now handles calling the next middleware/view automatically.
  • Update __init__ signatures: middleware __init__ no longer receives get_response. Change def __init__(self, get_response) to def __init__(self) and remove super().__init__(get_response).

0.115.0 (2026-03-05)

What's changed

  • Asyncio worker event loop — replaced the hand-rolled selectors event loop and PollableMethodQueue pipe with Python's asyncio as the worker's main loop. Connection acceptance, keepalive timeouts, and backpressure are now managed with native asyncio primitives (create_task, wait_for, Event, add_reader) while all request handling still runs synchronously in the thread pool via run_in_executor (bc3f998f3fda)
  • Accept-loop crash detection — if a listener socket hits an unexpected error (e.g. EMFILE), the worker now detects it and shuts down for the arbiter to restart, instead of silently losing that listener (bc3f998f3fda)
  • Cleaner graceful shutdown — uses asyncio.wait() with timeout and task cancellation, and cancels accept loops before closing listener sockets to avoid EBADF errors (bc3f998f3fda)

Upgrade instructions

  • No changes required.

0.114.1 (2026-03-04)

What's changed

  • Fixed server error responses being malformed on Python 3.14 due to a textwrap.dedent() behavior change that strips \r as whitespace, breaking the \r\n\r\n header-body separator (6e61cf5e39b3)

Upgrade instructions

  • No changes required.

0.114.0 (2026-03-04)

What's changed

  • Lock-free thread worker event loop — replaced RLock and futures.wait() with a pipe-based PollableMethodQueue that defers worker thread completions back to the main thread, eliminating all lock contention in the connection handling hot path (d0ecd12bbe)
  • Unified event loop — the main loop now uses a single poller.select() call for accepts, client data, and worker completions instead of splitting between poller.select() and futures.wait() (d0ecd12bbe)
  • Backpressure on accept — listener sockets are dynamically registered/unregistered from the poller when at connection capacity, preventing thread pool exhaustion under load (d0ecd12bbe)
  • Slow client timeout — new connections now get a read timeout during request parsing so slow or stalled clients can't hold a thread pool slot indefinitely (d0ecd12bbe)
  • Graceful shutdown improvement — shutdown now drains in-flight requests via the method queue instead of polling futures (d0ecd12bbe)
  • Added HTTPS_PROXY_HEADER upgrade warning to 0.113.0 changelog (b1d63fda04)

Upgrade instructions

  • No changes required.

0.113.0 (2026-03-04)

What's changed

  • Removed WSGI layer entirely — the server now creates Request objects directly and writes Response to sockets, eliminating the WSGI environ abstraction (163c31ba9f, 4a4fe406086a)
  • Request.__init__ now accepts method, path, headers, and connection params directly instead of a WSGI environ dict (f25f430f54b4, 1c9ab9e67611)
  • Test client creates Request directly without WSGI environ (bed765f3ff77)
  • Centralized header normalization in RequestHeaders class, simplifying Request attributes (acec7dfd89be)
  • Removed LimitedStream — body size limit is now enforced during reads (cb8ac54654f3)
  • Simplified Response by removing WSGI-era start_response method (7cea8c314449)
  • Server migrated from fork to spawn for process creation, with simplified process supervisor (a19c13255a4d)
  • Flattened Worker classes and moved runtime setup to worker entry point (ce1114615d86)
  • Removed server-level scheme detection, unified on HTTPS_PROXY_HEADER setting (05eea6446830)
  • Added Host header validation per RFC 9112 §3.2 (bd07db36aec2)
  • Structured access logging — server access log now uses structured context logging with configurable fields (72a905fbe1c3)
  • Removed standard log format — only keyvalue or json log formats are supported (96ad632e6f28)
  • Replaced Logger class with module-level functions and standard logging (24d9665818de)
  • Removed logging CLI options from server command (d00dc098b32d)
  • Added SERVER_* settings for server configuration (9fadf8bafec2)
  • Added per-response access log control and ASSETS_LOG_304 setting to suppress noisy asset 304s (4250db0ed02e)
  • Changed --access-log from file path to boolean flag (6924211917f6)
  • Preflight badge rendered inline in HTML instead of JavaScript fetch (2894abfc5d98)
  • Removed dead signal handlers (SIGUSR1, SIGUSR2, SIGWINCH, SIGHUP, SIGTTIN, SIGTTOU) (37f13c730b47)
  • Deleted Config dataclass — server params are passed directly (4989df5eb147)
  • Removed dead file-logging code from server (7f4f80fa0ff4)

Upgrade instructions

  • WSGI removed — The WSGI layer (plain.wsgi) has been completely removed. You can no longer use third-party WSGI servers (gunicorn, uvicorn, etc.) — use plain server directly. If you had a wsgi.py entry point, remove it.
  • Request() constructor changedRequest() now requires method and path keyword arguments. Code that constructed Request objects directly (e.g., in tests) must be updated: Request(method="GET", path="/").
  • request_started signal changed — The signal no longer sends an environ keyword argument. If you connected to request_started, update your receiver to not expect environ.
  • WEB_CONCURRENCYSERVER_WORKERS — The --workers CLI option now reads from the SERVER_WORKERS setting (default: 0 for auto/CPU count). WEB_CONCURRENCY env var is still read as a fallback in the default setting, but the canonical way is now PLAIN_SERVER_WORKERS env var or SERVER_WORKERS in settings.
  • New SERVER_* settings — Server configuration has moved from CLI-only options to settings: SERVER_WORKERS, SERVER_THREADS, SERVER_TIMEOUT, SERVER_MAX_REQUESTS, SERVER_ACCESS_LOG, SERVER_ACCESS_LOG_FIELDS, SERVER_GRACEFUL_TIMEOUT, SERVER_SENDFILE. CLI flags still work as overrides.
  • Log format standard removed — Only keyvalue or json are supported. Update LOG_FORMAT if you were using standard.
  • Server logging CLI options removed--log-level, --log-format, and --access-log-format have been removed. Configure via settings or environment variables instead.
  • --access-log is now a boolean — Use --access-log / --no-access-log instead of passing a file path. Access logs always go to stdout.
  • HTTPS_PROXY_HEADER now required behind reverse proxies — The server no longer auto-detects HTTPS from the connection. If your app runs behind an SSL-terminating proxy (Heroku, AWS ALB, nginx, etc.) and HTTPS_REDIRECT_ENABLED is True (the default), you must set HTTPS_PROXY_HEADER or you'll get an infinite 301 redirect loop. For example, on Heroku: HTTPS_PROXY_HEADER = "X-Forwarded-Proto: https" (or set PLAIN_HTTPS_PROXY_HEADER="X-Forwarded-Proto: https" as an env var).
  • Server process model changed from fork to spawn — This should be transparent, but if you relied on fork-inherited state in worker processes, it will no longer be available.
  • LimitedStream removed — Body size limits are now enforced automatically during reads.
  • HEADER_MAP config removed — Underscore-containing headers are always dropped (the previous default behavior). The refuse and dangerous options no longer exist.

0.112.1 (2026-03-03)

What's changed

  • settings.get_settings() now skips internal settings whose names start with _ (7bd0064bdc)

Upgrade instructions

  • No changes required.

0.112.0 (2026-02-28)

What's changed

  • Removed DEFAULT_CHARSET setting — the charset is now always utf-8, which was already the default. All references in QueryDict, ResponseBase, multipart parsing, and test utilities now use the hardcoded value directly (901e6b3c49)

Upgrade instructions

  • If you were customizing DEFAULT_CHARSET in your settings, remove it. UTF-8 is now always used.

0.111.0 (2026-02-26)

What's changed

  • Added built-in HEALTHCHECK_PATH setting — when set, requests to this exact path return a 200 response before any middleware runs, avoiding ALLOWED_HOSTS rejection and HTTPS redirect loops from health checkers (2c25ccbadd)
  • Fixed test client to handle responses from middleware that bypass URL routing (e.g. healthcheck) — previously missing sessions or unresolvable paths would raise exceptions (bcd8913f02)
  • Custom settings (prefixed with APP_) now resolve type annotations properly, enabling environment variable parsing for custom settings (d9fff3223e)
  • Secret settings now show collection size hints (e.g. {******** (3 items)}) instead of a flat ******** for dict, list, and tuple values (d9fff3223e)
  • plain docs --search now supports --api to also search public API symbols (e3ef3f3d84)
  • plain docs --section now matches ### subsections in addition to ## sections (9db0491a3f)

Upgrade instructions

  • No changes required.

0.110.1 (2026-02-26)

What's changed

  • Added type annotations to all settings in global_settings.py so they can be set via environment variables — previously 11 settings like HTTPS_REDIRECT_ENABLED, APPEND_SLASH, and FILE_UPLOAD_MAX_MEMORY_SIZE were missing annotations and would error when set via env vars (37e8a58ca9b5)

Upgrade instructions

  • No changes required.

0.110.0 (2026-02-26)

What's changed

  • Environment variables now take highest precedence, overriding values set in settings.py — previously explicit settings would win over env vars (0d40bcfcd539)
  • Moved SSL handshake from the main thread to the worker thread so handshake errors no longer crash the main loop, ported from gunicorn PR #3440 (6309ef82642e)
  • Switched keepalive timeouts to use time.monotonic() instead of time.time() for correctness during clock adjustments (e7ddd1a31cfe)
  • Extracted finish_body() method on the HTTP parser for explicit cleanup before returning keepalive connections to the poller (0cf51dd17c6f)

Upgrade instructions

  • If you rely on settings.py values taking precedence over PLAIN_-prefixed environment variables, be aware that env vars now win. Remove any env vars that conflict with values you want to set in code.

0.109.0 (2026-02-26)

What's changed

  • Added --outline flag to plain docs CLI to display section headings for quick navigation (153502ee90f5)
  • Added --search flag to plain docs CLI to find which modules and sections mention a term (153502ee90f5)
  • Enhanced plain docs --list to show core modules alongside packages, with color-coded output (3f34b5405ea3)
  • Updated shell banner to show app name and version in a styled box instead of generic welcome message (a7b152d0baf8)

Upgrade instructions

  • No changes required.

0.108.1 (2026-02-26)

What's changed

  • Fixed plain request to use localhost as SERVER_NAME and default the Accept header to text/html, matching typical browser behavior (01731c5485cf)
  • Updated plain-bug skill to create GitHub Issues via gh CLI instead of posting to the Plain API (ce7b95bd056d)

Upgrade instructions

  • No changes required.

0.108.0 (2026-02-24)

What's changed

  • Added absolute URL generation: new absolute_url() and reverse_absolute() functions that prepend the scheme and domain using a new BASE_URL setting (1e0d09f3ec70)
  • Added reverse_absolute and absolute_url as Jinja template globals for use in templates (1e0d09f3ec70)
  • Added reverse as an explicit template global (previously only available as url) (1e0d09f3ec70)

Upgrade instructions

  • No changes required. To use absolute URLs, set BASE_URL in your settings (e.g. BASE_URL = "https://example.com").

0.107.0 (2026-02-24)

What's changed

  • Added SettingOption — a custom Click option class that reads defaults from Plain settings, bridging CLI options to the settings system (cb5353b9d266)
  • Removed SyncWorker from the built-in HTTP server; ThreadWorker is now the only worker type (c38ee93de5b4)
  • Changed plain server defaults to --workers auto (one per CPU core) and --threads 4 for better out-of-the-box concurrency (c38ee93de5b4)

Upgrade instructions

  • If you relied on SyncWorker or single-threaded behavior, explicitly pass --threads 1 to plain server.
  • If you pinned --workers 1, note the default is now auto. Pass --workers 1 explicitly to keep the old behavior.

0.106.2 (2026-02-13)

What's changed

  • Added --version flag to the plain CLI to display the installed version (e76fd7070302)

Upgrade instructions

  • No changes required.

0.106.1 (2026-02-13)

What's changed

  • Added --section flag to plain docs for loading a specific ## section by name (e.g., plain docs models --section querying) (f2ce0243e6ea)
  • Simplified plain docs output by removing XML-style wrapper tags around documentation content (f2ce0243e6ea)
  • Added /plain-bug skill for submitting bug reports to plainframework.com (a9efc4383233)
  • Slimmed agent rules to concise bullet-point reminders and moved detailed code examples into README docs (f5d2731ebda0)
  • Added Forms section to templates README and View patterns section to views README (8c2189a896d2)
  • Fixed agents exclusion in LLM docs to only exclude .claude/ content, allowing agents/README.md to appear in docs output (f2ce0243e6ea)

Upgrade instructions

  • No changes required.

0.106.0 (2026-02-12)

What's changed

  • Added plain check command that runs core validation checks in sequence: custom commands, code linting, preflight, migration state, and tests (430268a12ae2)
  • Added assertion flags to plain request: --status, --contains, and --not-contains for automated response testing (6b66bfd05b9e)
  • Fixed plain request crash when no user model exists (8ef8a3813bff)
  • plain request now exits non-zero on server errors (5xx) and all failure paths (6b66bfd05b9e)
  • Updated agent rules with additional Django-to-Plain differences (model options, CSRF, forms, middleware) (9db8e0aa5d43)

Upgrade instructions

  • No changes required.

0.105.0 (2026-02-05)

What's changed

  • plain agent install now discovers and installs rules and skills from plainx.* namespace packages in addition to plain.* packages (bd568db924f7)
  • Orphan cleanup during plain agent install now correctly handles both plain and plainx prefixed items (bd568db924f7)

Upgrade instructions

  • No changes required.

0.104.1 (2026-02-04)

What's changed

  • Added __all__ exports to public modules for improved IDE autocompletion and explicit public API boundaries (e7164d3891b2, f26a63a5c941)
  • Removed @internalcode decorator from internal classes in favor of __all__ exports (e7164d3891b2)
  • Renamed plain docs --symbols option to --api for clarity (e7164d3891b2)

Upgrade instructions

  • If using plain docs --symbols, update to plain docs --api.

0.104.0 (2026-02-04)

What's changed

  • Refactored the assets manifest system with a clearer API: AssetsManifest replaces AssetsFingerprintsManifest, with explicit methods add_fingerprinted(), add_non_fingerprinted(), is_fingerprinted(), and resolve() (9cb84010b5fb)
  • Renamed ASSETS_BASE_URL setting to ASSETS_CDN_URL for clarity (9cb84010b5fb)
  • When ASSETS_CDN_URL is configured, AssetsRouter now redirects compiled assets to the CDN: original paths use 302 (temporary), fingerprinted paths use 301 (permanent) with immutable caching (9cb84010b5fb)
  • The is_immutable() check now uses the manifest to determine if a path is fingerprinted, rather than pattern-matching the filename (9cb84010b5fb)

Upgrade instructions

  • Rename ASSETS_BASE_URL to ASSETS_CDN_URL in your settings if you use a CDN for assets.
  • If you were importing from plain.assets.fingerprints, update imports to use plain.assets.manifest instead:
    • AssetsFingerprintsManifestAssetsManifest
    • get_fingerprinted_url_path()get_manifest().resolve()
    • _get_file_fingerprint()compute_fingerprint()

0.103.2 (2026-02-02)

What's changed

  • Compiled assets now use deterministic gzip output by setting mtime=0, ensuring consistent file hashes across builds (dc76e03879fc)
  • Agent rules now include a "Key Differences from Django" section to help Claude avoid common mistakes when working with Plain (02e11328dbf5)

Upgrade instructions

  • No changes required.

0.103.1 (2026-01-30)

What's changed

  • load_dotenv() now sets each environment variable immediately as it is parsed, so command substitutions like $(echo $TOKEN) can reference variables defined earlier in the same .env file (cecb71a016)

Upgrade instructions

  • No changes required.

0.103.0 (2026-01-30)

What's changed

  • plain docs now shows markdown documentation by default (previously required --source), with a new --symbols flag to show only the symbolicated API surface (b71dab9d5d)
  • plain docs --list now shows all official Plain packages (installed and uninstalled) with descriptions and install status (9cba705d62)
  • plain docs for uninstalled packages now shows the install command and an online docs URL instead of a generic error (9cba705d62)
  • Removed the plain agent context command and the SessionStart hook setup — agent rules now provide context directly without needing a startup hook (88d9424643)
  • plain agent install now cleans up old SessionStart hooks from .claude/settings.json (88d9424643)

Upgrade instructions

  • The --source flag for plain docs has been removed. Use --symbols instead to see the symbolicated API surface.
  • The --open flag for plain docs has been removed.
  • Run plain agent install to clean up the old SessionStart hook from your .claude/settings.json.

0.102.0 (2026-01-28)

What's changed

  • Refactored agent integration from skills-based to rules-based: packages now provide agents/.claude/rules/ files and agents/.claude/skills/ directories instead of skills/ directories (512040ac51)
  • The plain agent install command now copies both rules (.md files) and skills to the project's .claude/ directory, and cleans up orphaned plain* items (512040ac51)
  • Removed standalone skills (plain-docs, plain-shell, plain-request) that are now provided as passive rules instead (512040ac51)

Upgrade instructions

  • Run plain agent install to update your .claude/ directory with the new rules-based structure.

0.101.2 (2026-01-28)

What's changed

  • When load_dotenv() is called with override=False (the default), command substitution is now skipped for keys that already exist in os.environ. This prevents redundant command execution in child processes that re-load the .env file after inheriting resolved values from the parent, avoiding multiple auth prompts with tools like the 1Password CLI (2f6ff93499)
  • The _execute_command helper now uses stdout=subprocess.PIPE instead of capture_output=True, allowing stderr/tty to pass through for interactive prompts (2f6ff93499)
  • Updated templates README examples to use id instead of pk (837d345d23)
  • Added Settings section to README (803fee1ad5)

Upgrade instructions

  • No changes required.

0.101.1 (2026-01-17)

What's changed

  • Fixed a crash when running the development server with --reload when an app's assets directory doesn't exist (df33f93ece)
  • The plain agent install command now preserves user-created skills (those without the plain- prefix) instead of removing them as orphans (bbc87498ed)

Upgrade instructions

  • No changes required.

0.101.0 (2026-01-15)

What's changed

  • The plain server command now accepts --workers auto (or WEB_CONCURRENCY=auto) to automatically set worker count based on CPU count (02a1769948)
  • Response headers can now be set to None to opt out of default headers; None values are filtered out at the WSGI layer rather than being deleted by middleware (cbf27e728d)
  • Removed unused Response methods: serialize_headers, serialize, file-like interface stubs (write, flush, tell, readable, seekable, writable, writelines), text property, pickling support, and getvalue (cbf27e728d)

Upgrade instructions

  • No changes required

0.100.1 (2026-01-15)

What's changed

  • The plain agent install command now only sets up session hooks for Claude Code, not Codex, since the settings.json hook format is Claude Code-specific (a41e08bcd2)

Upgrade instructions

  • No changes required

0.100.0 (2026-01-15)

What's changed

  • The plain skills command has been renamed to plain agent with new subcommands: plain agent install (installs skills and sets up hooks), plain agent skills (lists available skills), and plain agent context (outputs framework context) (fac8673436)
  • Added SessionStart hook that automatically runs plain agent context at the start of every Claude Code or Codex session, providing framework context without needing a separate skill (fac8673436)
  • The plain-principles skill has been removed - its content is now provided by the plain agent context command via the SessionStart hook (fac8673436)
  • Added --no-headers and --no-body flags to plain request for limiting output (fac8673436)

Upgrade instructions

  • Replace plain skills --install with plain agent install
  • Replace plain skills (without flags) with plain agent skills
  • Run plain agent install to set up the new SessionStart hook in your project's .claude/ or .codex/ directory

0.99.0 (2026-01-15)

What's changed

  • Added plain.utils.dotenv module with load_dotenv() and parse_dotenv() functions for bash-compatible .env file parsing, supporting variable expansion, command substitution, multiline values, and escape sequences (a9b2dc3e16)

Upgrade instructions

  • No changes required

0.98.1 (2026-01-13)

What's changed

  • Fixed INSTALLED_PACKAGES not being optional in user settings, restoring the default empty list behavior (820773c473)

Upgrade instructions

  • No changes required

0.98.0 (2026-01-13)

What's changed

  • The plain skills --install command now removes orphaned skills from destination directories when skills are renamed or removed from packages (d51294ace1)
  • Added README documentation for plain.skills with available skills and installation instructions (7c90fc8595)

Upgrade instructions

  • No changes required

0.97.0 (2026-01-13)

What's changed

  • HTTP exceptions (NotFoundError404, ForbiddenError403, BadRequestError400, and SuspiciousOperationError400 variants) moved from plain.exceptions to plain.http.exceptions and are now exported from plain.http (b61f909e29)

Upgrade instructions

  • Update imports of HTTP exceptions from plain.exceptions to plain.http:

    # Before
    from plain.exceptions import NotFoundError404, ForbiddenError403, BadRequestError400
    
    # After
    from plain.http import NotFoundError404, ForbiddenError403, BadRequestError400
    

0.96.0 (2026-01-13)

What's changed

  • Response classes renamed for consistency: ResponseRedirectRedirectResponse, ResponseNotModifiedNotModifiedResponse, ResponseNotAllowedNotAllowedResponse (fad5bf28b0)
  • Redundant response classes removed: ResponseNotFound, ResponseForbidden, ResponseBadRequest, ResponseGone, ResponseServerError - use Response(status_code=X) instead (fad5bf28b0)
  • HTTP exceptions renamed to include status code suffix: Http404NotFoundError404, PermissionDeniedForbiddenError403, BadRequestBadRequestError400, SuspiciousOperationSuspiciousOperationError400 (5a1f020f52)
  • Added Secret[T] type annotation for masking sensitive settings like SECRET_KEY in CLI output (8713dc08b0)
  • Added ENV_SETTINGS_PREFIXES setting to configure which environment variable prefixes are checked for settings (defaults to ["PLAIN_"]) (8713dc08b0)
  • New plain settings list and plain settings get CLI commands for viewing settings with their sources (8713dc08b0)
  • Added preflight check for unused environment variables matching configured prefixes (8713dc08b0)
  • Renamed request.meta to request.environ for clarity (786b95bef8)
  • Added request.query_string and request.content_length properties (786b95bef8, 76dfd477d2)
  • Renamed X-Forwarded settings: USE_X_FORWARDED_HOSTHTTP_X_FORWARDED_HOST, USE_X_FORWARDED_PORTHTTP_X_FORWARDED_PORT, USE_X_FORWARDED_FORHTTP_X_FORWARDED_FOR (22f241a55c)
  • Changed HTTPS_PROXY_HEADER from a tuple to a string format (e.g., "X-Forwarded-Proto: https") (7ac2a431b6)

Upgrade instructions

  • Replace Response class imports and usages:

    • ResponseRedirectRedirectResponse
    • ResponseNotModifiedNotModifiedResponse
    • ResponseNotAllowedNotAllowedResponse
    • ResponseNotFoundResponse(status_code=404)
    • ResponseForbiddenResponse(status_code=403)
    • ResponseBadRequestResponse(status_code=400)
    • ResponseGoneResponse(status_code=410)
    • ResponseServerErrorResponse(status_code=500)
  • Replace exception imports and usages:

    • Http404NotFoundError404
    • PermissionDeniedForbiddenError403
    • BadRequestBadRequestError400
    • SuspiciousOperationSuspiciousOperationError400
    • SuspiciousMultipartFormSuspiciousMultipartFormError400
    • SuspiciousFileOperationSuspiciousFileOperationError400
    • TooManyFieldsSentTooManyFieldsSentError400
    • TooManyFilesSentTooManyFilesSentError400
    • RequestDataTooBigRequestDataTooBigError400
  • Replace request.meta with request.environ

  • Rename X-Forwarded settings in your configuration:

    • USE_X_FORWARDED_HOSTHTTP_X_FORWARDED_HOST
    • USE_X_FORWARDED_PORTHTTP_X_FORWARDED_PORT
    • USE_X_FORWARDED_FORHTTP_X_FORWARDED_FOR
  • Update HTTPS_PROXY_HEADER from tuple format to string format:

    # Before
    HTTPS_PROXY_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
    
    # After
    HTTPS_PROXY_HEADER = "X-Forwarded-Proto: https"
    
  • Replace plain setting <name> command with plain settings get <name>

0.95.0 (2025-12-22)

What's changed

  • Improved thread worker server shutdown behavior with cancel_futures=True for faster and cleaner process termination (72d0620)

Upgrade instructions

  • No changes required

0.94.0 (2025-12-12)

What's changed

  • FormFieldMissingError exceptions are now automatically converted to HTTP 400 Bad Request responses with a warning log instead of causing a 500 error (b38f6e5)

Upgrade instructions

  • No changes required

0.93.1 (2025-12-09)

What's changed

  • Added type annotation for request.unique_id attribute to improve IDE support and type checking (23af501)

Upgrade instructions

  • No changes required

0.93.0 (2025-12-04)

What's changed

  • Improved type annotations across forms, HTTP handling, logging, and other core modules for better IDE support and type checking (ac1eeb0)
  • Internal refactor of TimestampSigner to use composition instead of inheritance from Signer, maintaining the same public API (ac1eeb0)

Upgrade instructions

  • No changes required

0.92.0 (2025-12-01)

What's changed

  • Added request.client_ip property to get the client's IP address, with support for X-Forwarded-For header when behind a trusted proxy (cb0bc5d)
  • Added USE_X_FORWARDED_FOR setting to enable reading client IP from X-Forwarded-For header (cb0bc5d)
  • Improved print_event CLI output styling with dimmed text for less visual noise (b09edfd)

Upgrade instructions

  • No changes required

0.91.0 (2025-11-24)

What's changed

  • Request body parsing refactored: the request.data attribute has been replaced with request.json_data and request.form_data for explicit content-type handling (90332a9)
  • QueryDict now has proper type annotations for get(), pop(), getlist(), and __getitem__() methods that reflect string return types (90332a9)
  • Forms now automatically select between json_data and form_data based on request content-type (90332a9)
  • View mixins ObjectTemplateViewMixin removed in favor of class inheritance for better typing - UpdateView and DeleteView now inherit from DetailView (569afd6)
  • AppLogger context logging now uses a context dict parameter instead of **kwargs for better type checking (581b406)
  • Removed erroneous AuthViewMixin export from plain.views (334bbb6)

Upgrade instructions

  • Replace request.data with the appropriate method:

    • For JSON requests: use request.json_data (returns a dict, raises BadRequest for invalid JSON)
    • For form data: use request.form_data (returns a QueryDict)
  • Update app_logger calls that pass context as kwargs to use the context parameter:

    # Before
    app_logger.info("Message", user_id=123, action="login")
    
    # After
    app_logger.info("Message", context={"user_id": 123, "action": "login"})
    

0.90.0 (2025-11-20)

What's changed

  • Improved type annotations in timezone.py: is_aware() and is_naive() now accept both datetime and time objects for more flexible type checking (a43145e)
  • Enhanced type annotations in view classes: convert_value_to_response() and handler result variables now use more explicit type hints for better IDE support (dc4454e)
  • Fixed type errors in forms and server workers: URL field now handles bytes properly, and worker wait_fds has explicit type annotation (fc98d66)

Upgrade instructions

  • No changes required

0.89.0 (2025-11-14)

What's changed

  • Improved type annotations in view classes: url_args, url_kwargs, and various template/form context dictionaries now have more specific type hints for better IDE support and type checking (83bcb95)

Upgrade instructions

  • No changes required

0.88.0 (2025-11-13)

What's changed

  • The plain.forms module now uses explicit imports instead of wildcard imports, improving IDE autocomplete and type checking support (eff36f3)

Upgrade instructions

  • No changes required

0.87.0 (2025-11-12)

What's changed

  • Internal classes now use abstract base classes with @abstractmethod decorators instead of raising NotImplementedError, improving type checking and IDE support (91b329a, 81b5f88, d2e2423, 61e7b5a)
  • Updated to latest version of ty type checker and fixed type errors and warnings throughout the codebase (f4dbcef)

Upgrade instructions

  • No changes required

0.86.2 (2025-11-11)

What's changed

  • CLI color output is now enabled in CI environments by checking the CI environment variable, matching the behavior of modern tools like uv (a1500f15ed)

Upgrade instructions

  • No changes required

0.86.1 (2025-11-10)

What's changed

  • The plain preflight command now outputs to stderr only when using --format json, keeping stdout clean for JSON parsing while avoiding success messages appearing in error logs for text format (72ebee7729)
  • CLI color handling now follows the CLICOLOR standard with proper priority: NO_COLOR > CLICOLOR_FORCE/FORCE_COLOR > CLICOLOR > isatty (c7fea406c5)

Upgrade instructions

  • No changes required

0.86.0 (2025-11-10)

What's changed

  • Log output is now split by severity level: INFO and below go to stdout, WARNING and above go to stderr for proper cloud platform log classification (52403b15ba)
  • Added LOG_STREAM setting to customize log output behavior with options: "split" (default), "stdout", or "stderr" (52403b15ba)
  • Log configuration documentation expanded with detailed guidance on output streams and environment variable settings (52403b15ba)

Upgrade instructions

  • No changes required (default behavior splits logs to stdout/stderr automatically, but this can be customized via PLAIN_LOG_STREAM environment variable if needed)

0.85.0 (2025-11-03)

What's changed

  • CLI help output now organizes commands into "Common Commands", "Core Commands", and "Package Commands" sections for better discoverability (73d3a48)
  • CLI help output has been customized with improved formatting and shortcut indicators showing which commands are shortcuts (e.g., migrate → models migrate) (db882e6)
  • CSRF exception messages now include more detailed context about what was rejected and why (e.g., port mismatches, host mismatches) (9a8e09c)
  • The plain agent md command now saves a combined AGENTS.md file to .plain/ by default when using plain dev, making it easier to provide context to coding agents (786b7a0)
  • CLI help text styling has been refined with dimmed descriptions and usage prefixes for improved readability (d7f7053)

Upgrade instructions

  • No changes required

0.84.1 (2025-10-31)

What's changed

  • Added license = "BSD-3-Clause" to package metadata in pyproject.toml (8477355)

Upgrade instructions

  • No changes required

0.84.0 (2025-10-29)

What's changed

  • The DEFAULT_RESPONSE_HEADERS setting now supports format string placeholders (e.g., {request.csp_nonce}) for dynamic header values instead of requiring a callable function (5199383128)
  • Views can now set headers to None to explicitly remove default response headers (5199383128)
  • Added comprehensive documentation for customizing default response headers including override, remove, and extend patterns (5199383128)

Upgrade instructions

  • If you have DEFAULT_RESPONSE_HEADERS configured as a callable function, convert it to a dictionary with format string placeholders:

    # Before:
    def DEFAULT_RESPONSE_HEADERS(request):
        nonce = request.csp_nonce
        return {
            "Content-Security-Policy": f"script-src 'self' 'nonce-{nonce}'",
        }
    
    # After:
    DEFAULT_RESPONSE_HEADERS = {
        "Content-Security-Policy": "script-src 'self' 'nonce-{request.csp_nonce}'",
    }
    
  • If you were overriding default headers to empty strings ("") to remove them, change those to None instead

0.83.0 (2025-10-29)

What's changed

  • Added comprehensive Content Security Policy (CSP) documentation explaining how to use nonces with inline scripts and styles (784f3dd972)
  • The json_script utility function now accepts an optional nonce parameter for CSP-compliant inline JSON scripts (784f3dd972)

Upgrade instructions

  • Any |json_script usages need to make sure the second argument is a nonce, not a custom encoder (which is now third)

0.82.0 (2025-10-29)

What's changed

  • The DEFAULT_RESPONSE_HEADERS setting can now be a callable that accepts a request argument, enabling dynamic header generation per request (cb92905834)
  • Added request.csp_nonce cached property for generating Content Security Policy nonces (75071dcc70)
  • Simplified the preflight command by moving plain preflight check back to plain preflight (40c2c4560e)

Upgrade instructions

  • If you use plain preflight check, update to plain preflight (the check subcommand has been removed for simplicity)
  • If you use plain preflight check --deploy, update to plain preflight --deploy

0.81.0 (2025-10-22)

What's changed

  • Removed support for category-specific error template fallbacks like 4xx.html and 5xx.html (9513f7c4fa)

Upgrade instructions

  • If you have 4xx.html or 5xx.html error templates, rename them to specific status code templates (e.g., 404.html, 500.html) or remove them if you prefer the plain HTTP response fallback

0.80.0 (2025-10-22)

What's changed

  • CSRF failures now raise SuspiciousOperation (HTTP 400) instead of PermissionDenied (HTTP 403) (ad146bde3e)
  • Error templates can now use category-specific fallbacks like 4xx.html or 5xx.html instead of the generic error.html (716cfa3cfc)
  • Updated error template documentation with best practices for self-contained 500.html templates (55cea3b522)

Upgrade instructions

  • If you have a templates/error.html template, instead create specific error templates for each status code you want to customize (e.g., 400.html, 403.html, 404.html, 500.html). You can also create category-specific templates like 4xx.html or 5xx.html for broader coverage.

0.79.0 (2025-10-22)

What's changed

  • Response objects now have an exception attribute that stores the exception that caused 5xx errors (0a243ba89c)
  • Middleware classes now use an abstract base class HttpMiddleware with a process_request() method (b960eed6c6)
  • CSRF middleware now raises PermissionDenied instead of rendering a custom CsrfFailureView (d4b93e59b3)
  • The HTTP_ERROR_VIEWS setting has been removed (7a4e3a31f4)
  • Standalone plain-changelog and plain-upgrade executables have been removed in favor of the built-in commands (07c3a4c540)
  • Standalone plain-build executable has been removed (99301ea797)
  • Removed automatic logging of all HTTP 400+ status codes for cleaner logs (c2769d7281)

Upgrade instructions

  • If you have custom middleware, inherit from HttpMiddleware and rename your __call__() method to process_request():

    # Before:
    class MyMiddleware:
        def __init__(self, get_response):
            self.get_response = get_response
    
        def __call__(self, request):
            response = self.get_response(request)
            return response
    
    # After:
    from plain.http import HttpMiddleware
    
    class MyMiddleware(HttpMiddleware):
        def process_request(self, request):
            response = self.get_response(request)
            return response
    
  • Remove any custom HTTP_ERROR_VIEWS setting from your configuration - error views are now controlled entirely by exception handlers

  • If you were calling plain-changelog or plain-upgrade as standalone commands, use plain changelog or plain upgrade instead

  • If you were calling plain-build as a standalone command, use plain build instead

0.78.2 (2025-10-20)

What's changed

  • Updated package metadata to use [dependency-groups] instead of [tool.uv] for development dependencies, following PEP 735 standard (1b43a3a272)

Upgrade instructions

  • No changes required

0.78.1 (2025-10-17)

What's changed

  • Fixed job worker logging by using getLogger instead of directly instantiating Logger for the plain logger (dd675666b9)

Upgrade instructions

  • No changes required

0.78.0 (2025-10-17)

What's changed

  • Chores have been refactored to use abstract base classes instead of decorated functions (c4466d3c60)
  • Added SHELL_IMPORT setting to customize what gets automatically imported in plain shell (9055f59c08)
  • Views that return None now raise Http404 instead of returning ResponseNotFound (5bb60016eb)
  • The plain chores list command output formatting now matches the plain jobs list format (4b6881a49e)

Upgrade instructions

  • Update any chores from decorated functions to class-based chores:

    # Before:
    @register_chore("group")
    def chore_name():
        """Description"""
        return "Done!"
    
    # After:
    from plain.chores import Chore, register_chore
    
    @register_chore
    class ChoreName(Chore):
        """Description"""
    
        def run(self):
            return "Done!"
    
  • Import Chore base class from plain.chores when creating new chores

0.77.0 (2025-10-13)

What's changed

  • The plain server --reload now uses watchfiles for improved cross-platform file watching (92e95c5032)
  • Server reloader now watches .env* files for changes and triggers automatic reload (92e95c5032)
  • HTML template additions and deletions now trigger automatic server reload when using --reload (f2f31c288b)
  • Internal server worker type renamed from "gthread" to "thread" for clarity (6470748e91)

Upgrade instructions

  • No changes required

0.76.0 (2025-10-12)

What's changed

  • Added new plain server command with built-in WSGI server (vendored gunicorn) (f9dc2867c7)
  • The plain server command supports WEB_CONCURRENCY environment variable for worker processes (0c3e8c6f32)
  • Simplified server startup logging to use a single consolidated log line (b1405b71f0)
  • Removed gunicorn as an external dependency - server functionality is now built into plain core (cb6c2f484d)
  • Internal server environment variables renamed from GUNICORN_* to PLAIN_SERVER_* (745c073123)
  • Removed unused server features including hooks, syslog, proxy protocol, user/group dropping, and config file loading (be0f82d92b, 10c206875b, ecf327014c, fb5a10f50b)

Upgrade instructions

  • Replace any direct usage of gunicorn with the new plain server command (ex. gunicorn plain.wsgi:app --workers 4 becomes plain server --workers 4)
  • Update any deployment scripts or Procfiles that use gunicorn to use plain server instead
  • Remove gunicorn from your project dependencies if you added it separately (it's now built into plain)
  • For Heroku deployments, the $PORT is not automatically detected - update your Procfile to web: plain server --bind 0.0.0.0:$PORT
  • If you were using gunicorn configuration files, migrate the settings to plain server command-line options (run plain server --help to see available options)

0.75.0 (2025-10-10)

What's changed

  • Documentation references updated from plain-worker to plain-jobs following the package rename (24219856e0)

Upgrade instructions

  • No changes required

0.74.0 (2025-10-08)

What's changed

  • The plain agent request command now displays request ID in the response output (4a20cfa3fc)
  • Request headers are now included in OpenTelemetry tracing baggage for improved observability (08a3376d06)

Upgrade instructions

  • No changes required

0.73.0 (2025-10-07)

What's changed

  • Internal preflight result handling updated to use model_options instead of _meta for model label retrieval (73ba469)

Upgrade instructions

  • No changes required

0.72.2 (2025-10-06)

What's changed

  • Improved type annotations for test client responses with new ClientResponse wrapper class (369353f9d6)
  • Enhanced internal type checking for WSGI handler and request/response types (50463b00c3)

Upgrade instructions

  • No changes required

0.72.1 (2025-10-02)

What's changed

  • Fixed documentation examples to use the correct view attribute names (self.user instead of self.request.user) (f6278d9)

Upgrade instructions

  • No changes required

0.72.0 (2025-10-02)

What's changed

  • Request attributes user and session are no longer set directly on the request object (154ee10)
  • Test client now uses plain.auth.requests.get_request_user() to retrieve user for response object when available (154ee10)
  • Removed plain.auth.middleware.AuthenticationMiddleware from default middleware configuration (154ee10)

Upgrade instructions

  • No changes required

0.71.0 (2025-09-30)

What's changed

  • Renamed HttpRequest to Request throughout the codebase for consistency and simplicity (cd46ff20)
  • Renamed HttpHeaders to RequestHeaders for naming consistency (cd46ff20)
  • Renamed settings: APP_NAMENAME, APP_VERSIONVERSION, APP_LOG_LEVELLOG_LEVEL, APP_LOG_FORMATLOG_FORMAT, PLAIN_LOG_LEVELFRAMEWORK_LOG_LEVEL (4c5f2166)
  • Added request.get_preferred_type() method to select the most preferred media type from Accept header (b105ba4d)
  • Moved helper functions in http/request.py to be static methods of QueryDict (0e1b0133)

Upgrade instructions

  • Replace all imports and usage of HttpRequest with Request
  • Replace all imports and usage of HttpHeaders with RequestHeaders
  • Update any custom settings that reference APP_NAME to NAME, APP_VERSION to VERSION, APP_LOG_LEVEL to LOG_LEVEL, APP_LOG_FORMAT to LOG_FORMAT, and PLAIN_LOG_LEVEL to FRAMEWORK_LOG_LEVEL
  • Configuring these settings via the PLAIN_ prefixed environment variable will need to be updated accordingly

0.70.0 (2025-09-30)

What's changed

  • Added comprehensive type annotations throughout the codebase for improved IDE support and type checking (365414c)
  • The Asset class in plain.assets.finders is now a module-level public class instead of being defined inside iter_assets() (6321765)

Upgrade instructions

  • No changes required

0.69.0 (2025-09-29)

What's changed

  • Model-related exceptions (FieldDoesNotExist, FieldError, ObjectDoesNotExist, MultipleObjectsReturned, EmptyResultSet, FullResultSet) moved from plain.exceptions to plain.models.exceptions (1c02564)
  • Added plain dev alias prompt that suggests adding p as a shell alias for convenience (d913b44)

Upgrade instructions

  • Replace imports of FieldDoesNotExist, FieldError, ObjectDoesNotExist, MultipleObjectsReturned, EmptyResultSet, or FullResultSet from plain.exceptions to plain.models.exceptions
  • If you're using ObjectDoesNotExist in views, update your import from plain.exceptions.ObjectDoesNotExist to plain.models.exceptions.ObjectDoesNotExist

0.68.1 (2025-09-25)

What's changed

  • Preflight checks are now sorted by name for consistent ordering (cb8e160)

Upgrade instructions

  • No changes required

0.68.0 (2025-09-25)

What's changed

  • Major refactor of the preflight check system with new CLI commands and improved output (b0b610d461)
  • Preflight checks now use descriptive IDs instead of numeric codes (cd96c97b25)
  • Unified preflight error messages and hints into a single fix field (c7cde12149)
  • Added plain-upgrade as a standalone command for upgrading Plain packages (42f2eed80c)

Upgrade instructions

  • Update any uses of the plain preflight command to plain preflight check, and remove the --database and --fail-level options which no longer exist
  • Custom preflight checks should be class based, extending PreflightCheck and implementing the run() method
  • Preflight checks need to be registered with a custom name (ex. @register_check("app.my_custom_check")) and optionally with deploy=True if it should run in only in deploy mode
  • Preflight results should use PreflightResult (optionally with warning=True) instead of preflight.Warning or preflight.Error
  • Preflight result IDs should be descriptive strings (e.g., models.lazy_reference_resolution_failed) instead of numeric codes
  • PREFLIGHT_SILENCED_CHECKS setting has been replaced with PREFLIGHT_SILENCED_RESULTS which should contain a list of result IDs to silence. PREFLIGHT_SILENCED_CHECKS now silences entire checks by name.

0.67.0 (2025-09-22)

What's changed

  • ALLOWED_HOSTS now defaults to [] (empty list) which allows all hosts, making it easier for development setups (d3cb7712b9)
  • Empty ALLOWED_HOSTS in production now triggers a preflight error instead of a warning to ensure proper security configuration (d3cb7712b9)

Upgrade instructions

  • No changes required

0.66.0 (2025-09-22)

What's changed

  • Host validation moved to dedicated middleware and ALLOWED_HOSTS setting is now required (6a4b7be)
  • Changed request.get_port() method to request.port cached property (544f3e1)
  • Removed internal request._get_full_path() method (50cdb58)

Upgrade instructions

  • Add ALLOWED_HOSTS setting to your configuration if not already present (required for host validation)
  • Replace any usage of request.get_host() with request.host
  • Replace any usage of request.get_port() with request.port

0.65.1 (2025-09-22)

What's changed

  • Fixed DisallowedHost exception handling in request span attributes to prevent telemetry errors (bcc0005)
  • Removed cached property optimization for scheme/host to improve request processing reliability (3a52690)

Upgrade instructions

  • No changes required

0.65.0 (2025-09-22)

What's changed

  • Added CIDR notation support to ALLOWED_HOSTS for IP address range validation (c485d21)

Upgrade instructions

  • No changes required

0.64.0 (2025-09-19)

What's changed

  • Added plain-build command as a standalone executable (4b39ca4)
  • Removed constant_time_compare utility function in favor of hmac.compare_digest (55f3f55)
  • CLI now forces colors in CI environments (GitHub Actions, GitLab CI, etc.) for better output visibility (56f7d2b)

Upgrade instructions

  • Replace any usage of plain.utils.crypto.constant_time_compare with hmac.compare_digest or secrets.compare_digest

0.63.0 (2025-09-12)

What's changed

  • Model manager attribute renamed from objects to query throughout codebase (037a239)
  • Simplified HTTPS redirect middleware by removing HTTPS_REDIRECT_EXEMPT_PATHS and HTTPS_REDIRECT_HOST settings (d264cd3)
  • Database backups are now created automatically during migrations when DEBUG=True unless explicitly disabled (c802307)

Upgrade instructions

  • Remove any HTTPS_REDIRECT_EXEMPT_PATHS and HTTPS_REDIRECT_HOST settings from your configuration - the HTTPS redirect middleware now performs a blanket redirect. For advanced redirect logic, write custom middleware.

0.62.1 (2025-09-09)

What's changed

  • Added clarification about app_logger.kv removal to 0.62.0 changelog (106636f)

Upgrade instructions

  • No changes required

0.62.0 (2025-09-09)

What's changed

  • Complete rewrite of logging settings and AppLogger with improved formatters and debug capabilities (ea7c953)
  • Added app_logger.debug_mode() context manager to temporarily change log level (f535459)
  • Minimum Python version updated to 3.13 (d86e307)
  • Removed app_logger.kv in favor of context kwargs (ea7c953)

Upgrade instructions

  • Make sure you are using Python 3.13 or higher
  • Replace any app_logger.kv.info("message", key=value) calls with app_logger.info("message", key=value) or appropriate log level

0.61.0 (2025-09-03)

What's changed

  • Added new plain agent command with subcommands for coding agents including docs, md, and request (df3edbf)
  • Added -c option to plain shell to execute commands and exit, similar to python -c (5e67f0b)
  • The plain docs --llm functionality has been moved to plain agent docs command (df3edbf)
  • Removed the plain help command in favor of standard plain --help (df3edbf)

Upgrade instructions

  • Replace plain docs --llm usage with plain agent docs command
  • Use plain --help instead of plain help command

0.60.0 (2025-08-27)

What's changed

  • Added new APP_VERSION setting that defaults to the project version from pyproject.toml (57fb948d46)
  • Updated get_app_name_from_pyproject() to get_app_info_from_pyproject() to return both name and version (57fb948d46)

Upgrade instructions

  • No changes required

0.59.0 (2025-08-22)

What's changed

  • Added new APP_NAME setting that defaults to the project name from pyproject.toml (1a4d60e)
  • Template views now validate that get_template_names() returns a list instead of a string (428a64f)
  • Object views now use cached properties for .object and .objects to improve performance (bd0507a)
  • Improved plain upgrade command to suggest using subagents when there are more than 3 package updates (497c30d)

Upgrade instructions

  • In object views, self.load_object() is no longer necessary as self.object is now a cached property.

0.58.0 (2025-08-19)

What's changed

  • Complete rewrite of CSRF protection using modern Sec-Fetch-Site headers and origin validation (955150800c)
  • Replaced CSRF view mixin with path-based exemptions using CSRF_EXEMPT_PATHS setting (2a50a9154e)
  • Renamed HTTPS_REDIRECT_EXEMPT to HTTPS_REDIRECT_EXEMPT_PATHS with leading slash requirement (b53d3bb7a7)
  • Agent commands now print prompts directly when running in Claude Code or Codex Sandbox environments (6eaed8ae3b)

Upgrade instructions

  • Remove any usage of CsrfExemptViewMixin and request.csrf_exempt and add exempt paths to the CSRF_EXEMPT_PATHS setting instead (ex. CSRF_EXEMPT_PATHS = [r"^/api/", r"/webhooks/.*"] -- but consider first whether the view still needs CSRF exemption under the new implementation)
  • Replace HTTPS_REDIRECT_EXEMPT with HTTPS_REDIRECT_EXEMPT_PATHS and ensure patterns include leading slash (ex. [r"^/health$", r"/api/internal/.*"])
  • Remove all CSRF cookie and token related settings - the new implementation doesn't use cookies or tokens (ex. {{ csrf_input }} and {{ csrf_token }})

0.57.0 (2025-08-15)

What's changed

  • The ResponsePermanentRedirect class has been removed; use ResponseRedirect with status_code=301 instead (d5735ea)
  • The RedirectView.permanent attribute has been replaced with status_code for more flexible redirect status codes (12dda16)
  • Updated RedirectView initialization parameters: url_name replaces pattern_name, preserve_query_params replaces query_string, and removed 410 Gone functionality (3b9ca71)

Upgrade instructions

  • Replace ResponsePermanentRedirect imports with ResponseRedirect and pass status_code=301 to the constructor
  • Update RedirectView subclasses to use status_code=301 instead of permanent=True
  • Replace pattern_name with url_name in RedirectView usage
  • Replace query_string=True with preserve_query_params=True in RedirectView usage

0.56.1 (2025-07-30)

What's changed

  • Improved plain install command instructions to be more explicit about completing code modifications (83292225db)

Upgrade instructions

  • No changes required

0.56.0 (2025-07-25)

What's changed

  • Added plain install command to install Plain packages with agent-assisted setup (bf1873e)
  • Added --print option to agent commands (plain install and plain upgrade) to print prompts without running the agent (9721331)
  • The plain docs command now automatically converts hyphens to dots in package names (e.g., plain-modelsplain.models) (1e3edc1)
  • Moved plain-upgrade functionality into plain core, eliminating the need for a separate package (473f9bb)

Upgrade instructions

  • No changes required

0.55.0 (2025-07-22)

What's changed

  • Updated URL pattern documentation examples to use id instead of pk in URL kwargs (b656ee6)
  • Updated views documentation examples to use id instead of pk for DetailView, UpdateView, and DeleteView (b656ee6)

Upgrade instructions

  • Update your URL patterns from <int:pk> to <int:id> in your URLconf
  • Update view code that accesses self.url_kwargs["pk"] to use self.url_kwargs["id"] instead
  • Replace any QuerySet filters using pk with id (e.g., Model.query.get(pk=1) becomes Model.query.get(id=1))

0.54.1 (2025-07-20)

What's changed

  • Fixed OpenTelemetry route naming to include leading slash for consistency with HTTP paths (9d77268)

Upgrade instructions

  • No changes required

0.54.0 (2025-07-18)

What's changed

  • Added OpenTelemetry instrumentation for HTTP requests, views, and template rendering (b0224d0418)
  • Added plain-observer package reference to plain README (f29ff4dafe)

Upgrade instructions

  • No changes required

0.53.0 (2025-07-18)

What's changed

  • Added a pluralize filter for Jinja templates to handle singular/plural forms (4cef9829ed)
  • Added get_signed_cookie() method to HttpRequest for retrieving and verifying signed cookies (f8796c8786)
  • Improved CLI error handling by using click.UsageError instead of manual error printing (88f06c5184)
  • Simplified preflight check success message (adffc06152)

Upgrade instructions

  • No changes required

0.52.2 (2025-06-27)

What's changed

  • Improved documentation for the assets subsystem: the AssetsRouter reference in the Assets README now links directly to the source code for quicker navigation (65437e9)

Upgrade instructions

  • No changes required

0.52.1 (2025-06-27)

What's changed

  • Fixed plain help output on newer versions of Click by switching from MultiCommand to Group when determining sub-commands (9482e42)

Upgrade instructions

  • No changes required

0.52.0 (2025-06-26)

What's changed

  • Added plain-changelog as a standalone executable so you can view changelogs without importing the full framework (e4e7324)
  • Removed the runtime dependency on the packaging library by replacing it with an internal version-comparison helper (e4e7324)
  • Improved the error message when a package changelog cannot be found, now showing the path that was looked up (f3c82bb)
  • Fixed an f-string issue that broke plain.debug.dd on Python 3.11 (ed24276)

Upgrade instructions

  • No changes required

0.51.0 (2025-06-24)

What's changed

  • New plain changelog CLI sub-command to quickly view a package’s changelog from the terminal. Supports --from/--to flags to limit the version range (50f0de7).

Upgrade instructions

  • No changes required

0.50.0 (2025-06-23)

What's changed

  • The URL inspection command has moved; run plain urls list instead of the old plain urls command (6146fcb)
  • plain preflight gains a simpler --database flag that enables database checks for your default database. The previous behaviour that accepted one or more database aliases has been removed (d346d81)
  • Settings overhaul: use a single DATABASE setting instead of DATABASES/DATABASE_ROUTERS (d346d81)

Upgrade instructions

  • Update any scripts or documentation that call plain urls …:

    • Replace plain urls --flat with plain urls list --flat
  • If you invoke preflight checks in CI or locally:

    • Replace plain preflight --database <alias> (or multiple aliases) with the new boolean flag: plain preflight --database
  • In settings.py migrate to the new database configuration:

    # Before
    DATABASES = {
        "default": {
            "ENGINE": "plain.backends.sqlite3",
            "NAME": BASE_DIR / "db.sqlite3",
        }
    }
    
    # After
    DATABASE = {
        "ENGINE": "plain.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",
    }
    

    Remove any DATABASES and DATABASE_ROUTERS settings – they are no longer read.