plain changelog
0.150.0 (2026-06-09)
What's changed
PREFLIGHT_SILENCED_RESULTSnow 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_silencescheck runs last on full preflight runs (plain preflight --deploy) and warns aboutPREFLIGHT_SILENCED_RESULTSentries that matched nothing — an unused entry is either a typo or stale (the issue it silenced has been fixed). It can be silenced viaPREFLIGHT_SILENCED_CHECKSlike any other check, and the matching logic is available asplain.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
ty0.0.45 upgrade.@deconstructiblegains explicit@overloadsignatures so it type-checks correctly whether applied bare (@deconstructible) or with arguments (@deconstructible(path=...)),Worker'ssocketsparameter widens fromlisttoSequence, and a fewty: ignorecomments 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
JsonResponseno longer accepts asafeargument. Previously it defaulted tosafe=Trueand raisedTypeErrorunlessdatawas adict; you passedsafe=Falseto serialize a list or any other non-dict value. The dict-only guard and the parameter are both gone —JsonResponsenow serializes any JSON-serializable value directly. (52338f58da)
Upgrade instructions
- Drop
safe=fromJsonResponse(...)calls. If you passedsafe=Falseto serialize a list (or any non-dict), just remove the argument — the value now serializes as-is.safe=Truewas the default and is likewise gone.
0.148.1 (2026-06-03)
What's changed
plain.observerhas been retired. Its references are removed from core: the package listing, theplain docsknown-packages list, and the internal request-span plumbing that passed cookies/headers to the observer sampler. (1bab9f784a)- The
plain-upgradeskill 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 toplain.connectfor production telemetry export andplain.pytest/plain.connectfor the OpenTelemetry SDK — observer is no longer published.
0.148.0 (2026-05-22)
What's changed
plain requestnow 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;--jsonreturns structured response metadata plus the same analysis under a top-leveltracekey (with both derivedanalysisand rawspans). Use--jsonfor context-frugal agent output — no response body, just metadata and trace data. The/plain-optimizeskill moves fromplain.observerto core now that trace analysis lives there. (afba897157)plain requestnow 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 aRedirected:line listing the hop chain (status → path) when redirects were followed, and always reports the resolved user (oranonymous) — in both text and--jsonoutput. (c4283e48a6)- Preflight queries are no longer counted in
plain requesttraces. The admin toolbar's preflight badge lazy-runs the full preflight suite on first render (pg_classintrospection, 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 /pathfor trace analysis, switch toplain request /path --json— same JSON shape philosophy, no observer install required. - No code changes required. The
/plain-optimizeskill now ships withplainitself —plain.observerno longer needs to be installed for the workflow.
0.147.0 (2026-05-21)
What's changed
- The
plain.utils.dotenvmodule has moved toplain.dev.dotenv. Production deployments (which don't installplain.dev) no longer ship dotenv parsing code at all — load environment variables via your deployment platform instead. (9932738450) - The CLI dispatcher now sets
PLAIN_ENVautomatically based on the active command:plain dev→dev,plain test→test. ExportPLAIN_ENVyourself to override. This letsplain.dev's new.envprecedence loader pick up the right env-specific files without users having to setPLAIN_ENVmanually. (9932738450) SuspiciousOperationError400exceptions are now logged atWARNINGwithout an attachedexc_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_dotenvorparse_dotenvfromplain.utils.dotenv, installplain.devand import fromplain.dev.dotenvinstead. - If you were filtering
SuspiciousOperationError400events out of your error tracker by exception type, switch to filtering by log level (warnings vs. errors) or by theplain.security.*logger namespace.
0.146.0 (2026-05-20)
What's changed
plain docs --searchnow substring-matches by default; pass--regexto 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
requestobject is no longer attached as a log extra on the 400 host-validation warning, the 405 method-not-allowed warning, orlog_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 --searchinvocation relied on regex behavior, add--regexto it. - If your logging configuration consumed
record.requestfrom these entries, switch to another extra field —request.pathis still attached where it was before.
0.145.3 (2026-05-20)
What's changed
- The
plain-supportpackage has been removed and no longer appears in the package docs list or theplain docsCLI. Customer support forms now live inplain.connectvia the{% connect_support_fields %}template tag. (a1a1da39c5)
Upgrade instructions
- No changes required.
0.145.2 (2026-05-19)
What's changed
- The
plain-pageviewspackage has been removed and no longer appears in the package docs list or theplain docsCLI. First-party pageview tracking now lives inplain.connectvia the{% connect_pageviews %}template tag. (8fa042fc19) plain docs --apinow 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 toplain.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/loginchanges whereaction="."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_SLASHsetting (defaultFalse) decides the canonical form for every route in the project.path("about")andpath("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 siblingSlashMismatchso a specific route's slash redirect isn't shadowed, and propagates throughinclude()boundaries viaResolverMatch.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}.htmlrendering lives onTemplateView.handle_exceptioninplain.templates0.2.0. View-level 5xx attribution still happens in core via_respond_to_exception. (90d8fd983b) - URL routing internals refactor. Dropped the
Routedataclass —URLPatternandURLResolverholdsegments: tuple[Segment, ...]directly, withURLPattern.convertersas acached_propertyfor 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 onURLResolver. Removed_effective_trailing_slashand theprefix_trailing_slashparameter that threaded throughresolve/reverse. (48ca69bafa) include()slashes are irrelevant.include("admin")andinclude("admin/")are identical mounts. The separator between an include's prefix and its children is enforced structurally by segment matching —/adminhomecollisions are impossible regardless of how the route string is spelled. (48ca69bafa)plain urls listrenders canonical slashes from segment tuples. Both--flatand the tree view now show the slash formresolve()/reverse()actually produce —URLS_TRAILING_SLASH=Truemakes endpoints render asadmin/home/rather thanadmin/home. (48ca69bafa)
Upgrade instructions
- Add
URLS_TRAILING_SLASH = Truetoapp/settings.pyto 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/→/loginmakesaction="."POST to/instead of/login. Either keepURLS_TRAILING_SLASH = True, or grep templates foraction="\."/href="\."and replace withaction=""(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("…/")+ matchingpath("…")for the same view with a singlepath("…")plus aforce_trailing_slashdecision. The slash form is no longer the discriminator. url_pattern.routeis gone. Reach forurl_pattern.segmentsorurl_pattern.converters(the latter replacesurl_pattern.route.converters).- Catchall routes (
path("<path:_>")) drop the slash check at registration. If you previously hadpath("<path:_>/", ...)to mean "slash-only catchall," it now behaves the same aspath("<path:_>", ...).
0.144.0 (2026-05-13)
What's changed
- URL routing rewritten as a segment-based resolver.
plain.urlssplits into focused modules:segments.pyparses route strings intoLiteral/Capture/Patterntuples,patterns.pymatches endpoints,resolvers.pywalks a prefix tree and builds reverse/namespace lookup tables eagerly at__init__,paths.pynormalizes request paths (RFC 3986 dot/slash collapse → 308/400), andreverse.pyexposesreverse()/_lazy/_absolute. The regex-decompiler inregex_helper.pyis gone; only_lazy_re_compileremains for the rest of the codebase. (5025de26be) path()andinclude()accept string routes only — rawre.compile(...)is no longer public API. The<converter:name>syntax (<int:>,<str:>,<uuid:>,<path:>,<slug:>) plusregister_converter()covers anything the raw-regex form did.RegexPatternand the converter classes (IntConverter,UUIDConverter, etc.) are now internal.URLPattern.patternnarrows toRoutePattern; the_check_pattern_startswith_slashand_check_include_trailing_dollarpreflight checks are gone. (28dba1d2ed)- Bidirectional 308 trailing-slash redirects;
APPEND_SLASHsetting 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).RedirectSlashMiddlewareis gone.Request.get_full_path()loses itsforce_append_slashparameter. (db4ac49f72) path()andinclude()normalize slash boundaries.include("admin"),include("admin/"), andinclude("/admin/")all resolve to"admin/".path("/users/")is equivalent topath("users/"). Fixes silent corruption whereinclude("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. withdata=Noneno 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 customregister_converter()covers anything raw regex did. The/plain-upgradeskill rewrites the common cases. - Drop
APPEND_SLASHfromapp/settings.py— it has no effect. Trailing-slash behavior is now decided per-route by whetherpath("…/")orpath("…")is registered. - Remove
RedirectSlashMiddlewarefromMIDDLEWAREif you had it. - Update
request.get_full_path()callers to dropforce_append_slash=. There was only one such caller in the framework itself (the deleted middleware). - Inline
IntConverter/UUIDConverter/_get_convertersimports fromplain.urls.converterswere never public; if you reached into them, switch toregister_converter()and checkconverter.keywordorurl_pattern.raw_route/url_pattern.route.converterson the public side.
0.143.0 (2026-05-12)
What's changed
plain.templatesextracted to a siblingplain.templatespackage. The Jinja engine,Template/TemplateFileMissing,register_template_global/filter/extension,DefaultEnvironment, theTEMPLATES_JINJA_ENVIRONMENTsetting, and the template-touching view classes (TemplateView,FormView,DetailView,CreateView,UpdateView,DeleteView,ListView) all moved out ofplaincore.plain.viewsslims toView,RedirectView,ServerSentEventsView;jinja2is no longer aplaindependency. (19b622a7ca)- Plain core's exception handler degrades to plain text when
plain.templatesisn't installed or registered. The framework still tries to render{status}.htmlwhenplain.templatesis inINSTALLED_PACKAGES; otherwise it returns a plain-text body. (1d293be8fd) request.path_inforemoved; userequest.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 OTelurl.pathall userequest.pathconsistently 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 aCONSUMERspan (plain/cli/chores.py), witherror.type+record_exceptionstamped on failure. (144c3b4822, 05ea71d6d7) View._respond_to_exceptionis now the single 5xx logging/observability point. Centralized solog_exception+response.exceptionattachment happens in one place per request. (2634fd1d1c)- Stopped emitting
exception.escapedon 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.templatesif 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(orFormView,DetailView,CreateView,UpdateView,DeleteView,ListView) is nowfrom plain.templates.views import .... The/plain-upgradeskill rewrites these automatically. - Rewrite
from plain.templates import Template, register_template_*— the import path is unchanged, but you now need theplain.templatespackage installed for those imports to resolve at all. - Drop any direct use of
request.path_info— replace withrequest.path. They've been equal in practice; there's no behavior change beyond the name. - Custom subclasses of
Requestthat accepted apath_info=constructor kwarg must drop it.
0.142.0 (2026-05-12)
What's changed
plain.assetsextracted to a siblingplain.assetspackage. The asset finder, manifest, compile pipeline, URL routing, view, settings (ASSETS_REDIRECT_ORIGINAL,ASSETS_CDN_URL,ASSETS_LOG_304), and theasset()template global all moved out ofplaincore into a dedicatedplain.assetspackage. Core no longer carries any HTML-asset-serving code or settings. This fixes a layering inversion (core templates previously imported fromplain.assets.urls) and lets REST-only services skip installing it entirely. (844f46e428)plain buildremoved; useplain assets buildinstead. The build orchestrator (user[tool.plain.build.run]commands +plain.buildentry points + asset compile) lives inplain.assetsnow.plain.tailwindandplain.esbuildkeep registeringplain.buildentry 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 buildwithplain assets buildin deploy scripts, Procfiles, and CI. from plain.assets import ...continues to work as a namespace import, but you must haveplain.assetsinstalled for the package'sINSTALLED_PACKAGESregistration (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) inapp/settings.pypreviously dropped the setting on the floor —dir()doesn't include unassigned names, soPLAIN_APP_FOOenv vars were ignored andsettings.APP_FOOraisedAttributeError. They now register as required settings, withfrom __future__ import annotationsstring annotations resolved viatyping.get_type_hints()so env parsing receivesintand 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
UnsupportedMediaTypeError415exception inplain.http.request.json_dataandrequest.form_datapreviously raised plainValueErrorwhen the Content-Type didn't match what they parse, which fell through to a generic 500. They now raiseUnsupportedMediaTypeError415(anHTTPExceptionsubclass withstatus_code=415) soAPIViewand downstream handlers can render it as a clean 4xx without per-endpoint try/except. (f30d2ef3f6cd)
Upgrade instructions
- If you were catching
ValueErroraroundrequest.json_data/request.form_datato handle bad Content-Type, you can drop the try/except — the new 415 will be rendered automatically by any view that handlesHTTPException(includingAPIView).
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 stampsescaped=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 explicitescaped=Truepath so downstream tooling can filter on it. (e9c35b855f)
Upgrade instructions
- No changes required.
0.140.0 (2026-05-05)
What's changed
plain shellnow runs piped/heredoc stdin throughplain.runtime.setup()instead of relying onPYTHONSTARTUP. Previouslyecho "User.query.count()" | plain shellraisedPackageRegistryNotReadybecausePYTHONSTARTUPis only honored for interactive sessions. Non-TTY stdin is now detected and exec'd after setup, matching the-candplain runpaths. 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 asextrafields. Mutations are now scoped to theformat()call. (c793ed71) - Tightened type annotations across
plain.http.request,plain.server.http.request,plain.test.client, andplain.utils.inspectfor ty 0.0.33 — introduces aRequestStreamProtocol forRequest._stream. (4b9d1db1)
Upgrade instructions
- No changes required.
0.139.0 (2026-04-30)
What's changed
T | Nonesettings now unwrap from env vars without JSON quoting. A setting annotatedstr | None(or any single-arm union withNone) acceptsPLAIN_X=valuedirectly — empty string maps toNone, anything else parses asT. 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
--sectionand--outlinefromplain docs. Both flags are removed from the CLI surface.--searchcovers the same ground (it returns full matching sections, scoped to a module if you give one), and a plainplain docs <name>is short enough to read top to bottom. The internal_extract_sectionhelper 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 unusedCheckRegistry.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 withplain docs <module> --search <term>(which prints all sections matching<term>), or justplain docs <module>and read the section directly. - Replace any
plain docs --outline/plain docs <module> --outlineinvocations withplain docs --search <term>to find headings, orplain docs <module>for the full doc. - If you import
get_checksfromplain.preflight.registry, switch to iteratingrun_checks(...)directly —get_checks()was unused and has been removed.
0.137.1 (2026-04-28)
What's changed
plain docs --searchnow matches inside fenced code blocks. API names likeCheckConstraintmostly 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.otelfor capturing OpenTelemetry signals in tests. Newinstall_test_tracer()andinstall_test_meter()helpers install in-memory providers and return anInMemorySpanExporter/InMemoryMetricReaderso 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 usingplain.pytest, prefer the newotel_spans/otel_metricsfixtures. (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.routeattribute and span name. Homepage routes (URL pattern"") were being treated like unmatched 404s because the truthy-check onresolver_match.routeskipped them. Switched tois not Noneso matched routes with empty patterns gethttp.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
Viewis now generic over its handler return type. The base class isView[HandlerResult = Response], andget/post/put/patch/delete/headare typed to returnHandlerResultinstead ofResponse. Subclasses that accept richer return types (e.g.APIViewin plain-api) can parameterize the base asView[APIResult]and have their handler signatures reflect that in type checkers. (9c0c12df13fd)- Renamed
View.convert_value_to_response→View.convert_result_to_response. The parameter is nowresult: HandlerResultinstead ofvalue: Any, which keeps the type flowing through the generic parameter. (11c8fe16b544)
Upgrade instructions
- If you overrode
convert_value_to_responseon aViewsubclass — rename it toconvert_result_to_responseand update the parameter name fromvaluetoresult. The signature is otherwise unchanged.
0.135.0 (2026-04-23)
What's changed
- Merged
ResponseBaseintoResponseas the common supertype.ResponseBaseis no longer exported fromplain.http—Responseis now the base class forStreamingResponseandAsyncStreamingResponse. Type hints across the framework (View.after_response,View.handle_exception,View.convert_value_to_response, etc.) useResponse. (f5007281d7fa)
Upgrade instructions
- If you imported
ResponseBase— replace it withResponse.from plain.http import Responsecovers bytes-body, streaming, and async-streaming responses.
0.134.0 (2026-04-22)
What's changed
- Removed
plain.signals. Therequest_started/request_finishedbuilt-in signals and the entireplain.signalsmodule (includingplain.signals.dispatch.Signal,receiver, and the dispatcher internals) are gone. Plain no longer emitsrequest_started/request_finishedaround request handling — pool-backed connection lifecycle inplain-postgresreplaced the one consumer. (2a51b25) - Narrowed
Viewhandler return types toResponse.get,post,put,patch,delete, andheadmust now return aResponse(or subclass likeJsonResponse,RedirectResponse). The shorthand coercion that acceptedstr,int,dict,list,tuple, andNonehas moved toAPIViewinplain-api— base views raiseTypeErrorif you return something that isn't aResponseBase.TemplateView.getand theform_validhooks onCreateView/UpdateView/DeleteVieware typed to returnResponsetoo. (1935f3f) - Per-request
contextvars.Contextbridges async view pipelines. Each request now runs through a singlecontextvars.Context— sync executor hops go throughctx.run(...)and async view coroutines run on a task bound to that context.before_request, the view, andafter_responsesee a consistent view of request-scopedContextVars (e.g. theplain-postgresconnection 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_finishedlisteners need to move off signals; hook into the request lifecycle with middleware orView.before_request/View.after_responseinstead. CustomSignal()usage has no replacement in Plain — roll your own event dispatch or use an external library if you need it. - If a
Viewhandler returned a dict, list, str, int, tuple, orNone— wrap it in aResponse(orJsonResponse(...),Response(status_code=...), etc.). For JSON APIs that want the shorthand back, switch toplain.api.APIView— it accepts dict/list/int/tuple/None returns via itsconvert_value_to_responseoverride. - If you subclassed
Viewand overrodeconvert_value_to_response— it now receives anAny-typed value and must raiseTypeErrorfor non-Responseinputs unless your override coerces them. Re-check your logic against the trimmed base implementation.
0.133.0 (2026-04-21)
What's changed
- Added
Viewlifecycle hooks:before_request,after_response, andhandle_exception.before_requestruns before the method handler (raise to reject the request).after_responseruns after the response is built — including error responses — and can mutate or replace it.handle_exceptionconverts 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 wrappingget_response(). (c1234c14be1d, 0da5639d17e2, 48effac976a9) - Added
HTTPExceptionbase class inplain.http. All HTTP exceptions (BadRequestError400,ForbiddenError403,NotFoundError404, etc.) now inherit from it and carry astatus_codeattribute. Subclass it to define your own status-mapped exceptions (e.g.class PaymentRequiredError402(HTTPException): status_code = 402).FormFieldMissingErrorandMultiPartParserErrornow subclassBadRequestError400. (48effac976a9) - Restricted
Viewdispatch to IANA HTTP methods. Handlers forget,post,put,patch,delete,head, andoptionsdispatch;TRACEandCONNECTare no longer dispatched to. Non-HTTP-method attribute names on a view class can no longer collide with the dispatcher. (5da708a057db) Viewnow tracks implemented methods via__init_subclass__. AView.implemented_methodsfrozenset is computed once per subclass, replacing the runtimehasattr()scan. This is what OpenAPI generation andAllow:header construction should read. (23baeea0653a)- HEAD now falls back to
getat dispatch time without mutatingself.head. Previously__init__aliasedself.head = self.get, which polluted the instance. (5848c010d66c) - Error rendering simplified. The internal
ErrorView/TemplateView-based path is gone —response_for_exceptionnow renders{status}.htmldirectly viaTemplate(...).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 bothView.get_responseand 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 — insideView.get_responsewhen an exception reaches the handler.ResponseExceptionis unwrapped beforehandle_exceptionruns, so overrides focus purely on response shape. Returning a response fromhandle_exceptionsuppresses 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 ofView, migrate to the new hooks —before_requestfor 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
HTTPExceptionwith astatus_codeattribute instead of catching and re-raising. - If you import
ErrorViewfromplain.views, it has been removed. Use the default framework renderer (which reads{status}.html) or overrideView.handle_exceptionfor custom formats. - If you read
hasattr(view, "get")/hasattr(view, "post")to detect implemented methods, switch toview.implemented_methods(a frozenset of lowercase method names) — baseViewnow provides stub handlers for every verb.
0.132.1 (2026-04-14)
What's changed
- Updated the
choresREADME example to reflect the newQuerySet.delete()return type (intinstead of a(count, by_label)tuple). (29e10dba51d9)
Upgrade instructions
- No changes required.
0.132.0 (2026-04-13)
What's changed
- Removed
AUTH_USER_MODELsetting. The User model is now fixed atapp.users.models.User— a required convention, not a configurable setting. Code that usedSettingsReference("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 ofAUTH_USER_MODELin migrations, which no longer exists. (0861c9915cb6) - Made
FormViewgeneric over its form type.FormView[MyForm]now typesself.formand 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 --userto resolve users viaapp.users.models.Userinstead ofget_user_model(). (0861c9915cb6) - Migrated type suppression comments to
ty: ignoreand upgraded the ty checker to 0.0.29. (4ec631a7ef51)
Upgrade instructions
- Move your User model to
app/users/models.py(package labelusers, class nameUser) if it isn't already there. RemoveAUTH_USER_MODELfromsettings.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 toGET /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.fullfrom HTTP server spans. The HTTP semconv doesn't defineurl.fullfor server spans —url.pathandurl.queryare already set separately. (b56a9edc9c7d) - Added recommended HTTP server span attributes.
server.address,server.port,client.address, anduser_agent.originalare now set on HTTP server spans. (b56a9edc9c7d) - Unknown HTTP methods normalized to
_OTHER. Per the HTTP semconv, unrecognized methods are now set to_OTHERwith the original value inhttp.request.method_original. (b56a9edc9c7d) - Added
error.typeto 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.nametohttp.server.request.durationmetric. (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
plaintoplain.templates. (b56a9edc9c7d) - Template
code.namespacereplaced with fully-qualifiedcode.function.name. Uses the stable semconv attribute. (b56a9edc9c7d) - Added
plain.utils.otelmodule withformat_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.typemetric now only flag 5xx responses as errors. Previously, 4xx responses were marked asStatusCode.ERRORon server spans and includederror.typein 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. Theget()method was unnecessarilyasync— it only constructs anAsyncStreamingResponsewithout awaiting anything. Making it sync fixes compatibility withAuthViewand other view mixins that overrideget_response(), which previously received a coroutine instead of a response object. (890d0d6ddb5a)
Upgrade instructions
- If you override
get()on aServerSentEventsViewsubclass withasync def get(self), change it todef get(self). Thestream()method remains async — no changes needed there.
0.131.0 (2026-04-01)
What's changed
- Added
http.server.request.durationOTel 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 --datanow auto-detects content type. If--content-typeis not specified, JSON data (starting with{or[) is sent asapplication/json, otherwise asapplication/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 checknow usespostgres sync --checkinstead of separatemigrate --checkandmakemigrations --checkcalls. This validates migrations, pending model changes, and convergence in a single step. (13bd4b963394)- Removed the top-level
makemigrationsandmigrateshortcut commands. Usemigrations createandmigrations apply(underplain postgres), orplain postgres syncfor the combined workflow. (adf021688bf3, b026895edc4c)
Upgrade instructions
- Replace
plain migratewithplain postgres syncandplain makemigrationswithplain migrations createin scripts and CI. Requiresplain-postgres>=0.91.0.
0.129.0 (2026-03-27)
What's changed
- Renamed
forms.CharFieldtoforms.TextField— all subclasses (EmailField,URLField,UUIDField,JSONField,RegexField) now extendTextField(4e29f5d6cade) - **Replaced
**kwargswith explicit parameters in Response classes** —Response,StreamingResponse,AsyncStreamingResponse,FileResponse,RedirectResponse,NotModifiedResponse,NotAllowedResponse, andJsonResponsenow declare all parameters explicitly (7d1cb9af3a06) - Removed
signing.dumps()andsigning.loads()— useTimestampSigner(salt=...).sign_object()/.unsign_object()orSignerdirectly (99b0e57bc175) - Disabled worker recycling in reload mode — file-change restarts already recycle workers, so
max_requestsretirement was causing unnecessary extra restart cycles during development (8eb9c6da485e) - Added
TYPE_CHECKINGstubs for View handler methods (get,post,put,patch,delete,head,trace) to improve IDE autocomplete and type checking (ca7ee03424d1)
Upgrade instructions
- Rename
forms.CharFieldtoforms.TextFieldin all form definitions. - Replace
signing.dumps(obj, salt=..., ...)withTimestampSigner(salt=...).sign_object(obj, ...)andsigning.loads(s, salt=..., ...)withTimestampSigner(salt=...).unsign_object(s, ...). - If you pass
**kwargsto 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 (RequestFactoryandClient) to acceptAnyinstead ofdict[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-portalto workspace, package list, andplain docsregistry (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— addedget_rss_bytes(),get_memory_usage(), andget_process_cpu_percent()helpers that work inside cgroups v1/v2 containers. Also added cgroup v1 CPU quota support toget_cpu_count()(40482feb2b) - Test client now creates OTel spans —
plain.test.Clientwraps each request in a server span, so observer traces are captured during tests (aa54f27d95) plain request --useraccepts email addresses — in addition to user IDs, with a clearer error whenplain-authis 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) andSERVER_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 asapp_loggerinstead of bare[LEVEL] messageformat. Log messages use stable, greppable sentence fragments with variable data passed as structuredextra={}fields rather than%sinterpolation (75a8b60c91) get_framework_logger()factory — new public function inplain.logsthat auto-derives logger names from the caller's module (e.g.plain.server.workers.entrybecomesplain.server), replacing scatteredlogging.getLogger("plain.xxx")calls across the codebase (2e25cae784)AppLoggerrenamed toPlainLogger— the logger class inplain.logshas been renamed and moved fromapp.pytologger.py. Theapp_loggerinstance and its API are unchanged. A newexception()method was added to supportcontext={}on exception logs (b79829ddbb)- Flat extra for structured formatters —
PlainLogger._log()now merges persistent context,extra, and per-callcontextinto flat top-levelLogRecordattributes instead of nesting underextra["context"]. TheKeyValueFormatterandJSONFormatterextract context by diffing against standardLogRecordattributes, so bothapp_loggerand standardplain.*loggers produce identical structured output (5148b2bc31) - Response body size in traces — the
http.response.body.sizeOpenTelemetry 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— theget_cpu_count()helper was moved from the server CLI toplain.utils.osso the jobs worker can also use it for container-aware process counts (aa0e57b7eb)
Upgrade instructions
- If you subclassed or imported
AppLoggerdirectly, update toPlainLogger(import path changed fromplain.logs.apptoplain.logs.logger). - If you relied on
extra["context"]being a nested dict on log records fromapp_logger, note that context keys are now flat top-level attributes on theLogRecord. Standardextra={}usage andcontext={}onapp_loggercalls are unaffected. - Review any custom log parsing that expected
[LEVEL] messageformat fromplain.*loggers — they now use the same formatter asapp_logger(key-value or JSON depending onAPP_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
SIGUSR1to forward memory recording signals to all workers (cdd7b2def319)
Upgrade instructions
- No changes required.
0.124.1 (2026-03-16)
What's changed
- Added
/plain-guideskill for researching framework questions usingplain docsand source code (16597aa560af) plain docs --searchnow 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.modelstoplain.postgresacross views, CLI docs, registry docstrings, README doc links, and agent rules.
Upgrade instructions
- Update imports:
from plain.modelstofrom plain.postgres,from plain import modelstofrom plain import postgres.
0.123.4 (2026-03-12)
What's changed
- Read cgroup v2 CPU quota for accurate container worker count —
os.process_cpu_count()only checkssched_getaffinity, not cgroup CPU quotas, so containers still saw the host CPU count. Now reads/proc/self/cgroupto resolve the process's cgroup path, then parsescpu.maxfor 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()withos.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=Nso you can confirm the server configuration at a glance (e63d9f90520c) - H2 max concurrent streams setting registered with default of 100 —
SERVER_H2_MAX_CONCURRENT_STREAMSis now a proper setting with a default value instead of usinggetattrwith 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_PATHendpoint now responds directly on the async event loop with a raw200 OKbefore 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. Passallow_external=Trueto 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_externalattribute — class-based redirect views now supportallow_external = Truefor 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), addallow_external=True:# Before RedirectResponse("https://example.com/callback") # After RedirectResponse("https://example.com/callback", allow_external=True)For
RedirectViewsubclasses that redirect externally, setallow_external = Trueon 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_boundfor forms with no fields (e.g., delete confirmation forms) — empty POST requests produced a falsyQueryDict, causing the form to never validate.is_boundnow checks the request method instead of data truthiness (f630b3b5fb22)
Upgrade instructions
- No changes required.
0.122.0 (2026-03-10)
What's changed
Secretis now type-transparent —Secret[str]is a type alias forAnnotated[str, _SecretMarker()], so type checkers see the underlying type directly. No moretype: ignorecomments needed on default values (a90197b95315, 997afd9a558f)TYPE_CHECKINGignored in settings modules — settings loading now skipsTYPE_CHECKING(and other uppercase non-setting names) so you can usefrom __future__ import annotationsandif TYPE_CHECKING:blocks in settings files without triggering duplicate-setting errors (f20869e0bd2b)- Adopted PEP 695 type parameter syntax (
def foo[T]()instead ofTypeVar) across the codebase (aa5b2db6e8ed)
Upgrade instructions
- If you previously had
type: ignore[assignment]comments onSecretdefault 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 checkandplain requestguidance (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.filesto 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
infolevel instead oferror, 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 logging —
PermissionDenied,MultiPartParserError, andBadRequestError400exceptions no longer includeexc_infoin their log entries, reducing noise in error tracking (c395232acdb9) - Handle asyncio
ConnectionResetErrorwithout errno — asyncio's_drain_helperraisesConnectionResetError('Connection lost')without an errno, which bypassed the existing errno-based check. Now caught explicitly before theOSErrorerrno check (b623d4f78667) - Removed
type: ignorecomments 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()withContextVarfor async compatibility — timezone activation (plain.utils.timezone) and URL resolver population tracking now useContextVarinstead ofthreading.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
OSErrorduring keepalive waits (e.g. client TCP reset) andConnectionErrorduring connection handling, preventing noisy tracebacks from abrupt client disconnects (e3aee49b32ac) - Updated
BaseHandler._run_in_executorto propagate only the OpenTelemetry span context into executor threads, intentionally leaving DB connection ContextVars on their native thread context so connections persist across requests honoringCONN_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_handshakemethod, thehanded_offflag, and all raw socket I/O fallback paths.Connectionnow always receivesasyncio.StreamReader/StreamWriterinstead of a raw socket, removing dual-path code inrecv,sendall,wait_readable,close, andResponse._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
Responsewrite methods (send_headers,write,sendfile,write_file,write_response,close) and theSERVER_SENDFILEsetting — all response writing now uses the async path (c1e172eec835) - Connection backpressure now uses
asyncio.Semaphoreinstead of a manualasyncio.Eventwith 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_argsand positional URL arguments — views no longer receive positional URL arguments viaself.url_args. All URL parameters are now keyword-only viaself.url_kwargs.reverse()andreverse_absolute()no longer accept*args— use keyword arguments instead.RegexPatternnow 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_CONNECTIONSsetting — controls the maximum number of concurrent connections per worker (default: 1000). Replaces implicit connection limits (322fd51cf206) - New
SERVER_H2_MAX_CONCURRENT_STREAMSsetting — 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_argswithself.url_kwargsin 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 withreverse(name, param1=arg1, param2=arg2)using keyword arguments. - Replace any
reverse_absolute(name, arg1)calls similarly. - In templates, replace
url("name", arg1)withurl("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()andsetup()from View — views are now passed as classes directly to URL patterns (e.g.,path("foo/", MyView)instead ofpath("foo/", MyView.as_view())). Thesetup()method is removed; request, URL args, and URL kwargs are set directly on the view instance.View.__init__no longer accepts arguments. Theview_classattribute moved fromURLPattern.viewtoURLPattern.view_classdirectly (0d0c8a64cb45) - Removed
--pidfileoption from server — the--pidfileCLI option andSERVER_PIDFILEsetting have been removed (3ac519e691b2) - Removed
--max-requestsoption from server — the--max-requestsCLI option andSERVER_MAX_REQUESTSsetting 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 justMyView. - Remove any
**kwargspassed toas_view()— set attributes on the class or override methods instead. - If you override
setup()in a view, move that logic toget(),post(), or another view method. - Remove any
--pidfileor--max-requestsflags from server invocations and theSERVER_PIDFILE/SERVER_MAX_REQUESTSsettings.
0.117.1 (2026-03-06)
What's changed
- Fixed 500 error handling to pass the actual exception to
ErrorViewinstead ofNone, 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 anasyncgenerator without occupying a thread pool slot. Available asfrom plain.http import AsyncStreamingResponse(b62c283ecd3d)ServerSentEventsView— new view class for Server-Sent Events. Subclass it and implement an asyncevents()generator that yieldsSentEventobjects. Handles SSE framing,Cache-Control, andX-Accel-Bufferingheaders 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
h2library, 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 phases —
HttpMiddlewareno longer uses the onion/wrapper model withprocess_request()andself.get_response(). Instead, middleware now implementsbefore_request(request) -> Response | None(return a response to short-circuit, orNoneto continue) andafter_response(request, response) -> Response(modify and return the response). The middleware pipeline runsbefore_requestforward through the chain, thenafter_responsein reverse. Middleware__init__no longer receivesget_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)tobefore_request(self, request)in custom middleware. The method should returnNoneto continue to the next middleware/view, or return aResponseto short-circuit. - Move any post-response logic (code after
self.get_response(request)) into a newafter_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 receivesget_response. Changedef __init__(self, get_response)todef __init__(self)and removesuper().__init__(get_response).
0.115.0 (2026-03-05)
What's changed
- Asyncio worker event loop — replaced the hand-rolled
selectorsevent loop andPollableMethodQueuepipe with Python'sasyncioas 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 viarun_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\ras whitespace, breaking the\r\n\r\nheader-body separator (6e61cf5e39b3)
Upgrade instructions
- No changes required.
0.114.0 (2026-03-04)
What's changed
- Lock-free thread worker event loop — replaced
RLockandfutures.wait()with a pipe-basedPollableMethodQueuethat 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 betweenpoller.select()andfutures.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_HEADERupgrade 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
Requestobjects directly and writesResponseto sockets, eliminating the WSGI environ abstraction (163c31ba9f, 4a4fe406086a) Request.__init__now acceptsmethod,path,headers, and connection params directly instead of a WSGI environ dict (f25f430f54b4, 1c9ab9e67611)- Test client creates
Requestdirectly without WSGI environ (bed765f3ff77) - Centralized header normalization in
RequestHeadersclass, simplifyingRequestattributes (acec7dfd89be) - Removed
LimitedStream— body size limit is now enforced during reads (cb8ac54654f3) - Simplified
Responseby removing WSGI-erastart_responsemethod (7cea8c314449) - Server migrated from fork to spawn for process creation, with simplified process supervisor (a19c13255a4d)
- Flattened
Workerclasses and moved runtime setup to worker entry point (ce1114615d86) - Removed server-level scheme detection, unified on
HTTPS_PROXY_HEADERsetting (05eea6446830) - Added
Hostheader 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
keyvalueorjsonlog formats are supported (96ad632e6f28) - Replaced
Loggerclass 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_304setting to suppress noisy asset 304s (4250db0ed02e) - Changed
--access-logfrom 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
Configdataclass — 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.) — useplain serverdirectly. If you had awsgi.pyentry point, remove it. Request()constructor changed —Request()now requiresmethodandpathkeyword arguments. Code that constructedRequestobjects directly (e.g., in tests) must be updated:Request(method="GET", path="/").request_startedsignal changed — The signal no longer sends anenvironkeyword argument. If you connected torequest_started, update your receiver to not expectenviron.WEB_CONCURRENCY→SERVER_WORKERS— The--workersCLI option now reads from theSERVER_WORKERSsetting (default:0for auto/CPU count).WEB_CONCURRENCYenv var is still read as a fallback in the default setting, but the canonical way is nowPLAIN_SERVER_WORKERSenv var orSERVER_WORKERSin 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
standardremoved — Onlykeyvalueorjsonare supported. UpdateLOG_FORMATif you were usingstandard. - Server logging CLI options removed —
--log-level,--log-format, and--access-log-formathave been removed. Configure via settings or environment variables instead. --access-logis now a boolean — Use--access-log/--no-access-loginstead of passing a file path. Access logs always go to stdout.HTTPS_PROXY_HEADERnow 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.) andHTTPS_REDIRECT_ENABLEDisTrue(the default), you must setHTTPS_PROXY_HEADERor you'll get an infinite 301 redirect loop. For example, on Heroku:HTTPS_PROXY_HEADER = "X-Forwarded-Proto: https"(or setPLAIN_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.
LimitedStreamremoved — Body size limits are now enforced automatically during reads.HEADER_MAPconfig removed — Underscore-containing headers are always dropped (the previous default behavior). Therefuseanddangerousoptions 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_CHARSETsetting — the charset is now alwaysutf-8, which was already the default. All references inQueryDict,ResponseBase, multipart parsing, and test utilities now use the hardcoded value directly (901e6b3c49)
Upgrade instructions
- If you were customizing
DEFAULT_CHARSETin your settings, remove it. UTF-8 is now always used.
0.111.0 (2026-02-26)
What's changed
- Added built-in
HEALTHCHECK_PATHsetting — when set, requests to this exact path return a200response 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 --searchnow supports--apito also search public API symbols (e3ef3f3d84)plain docs --sectionnow 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.pyso they can be set via environment variables — previously 11 settings likeHTTPS_REDIRECT_ENABLED,APPEND_SLASH, andFILE_UPLOAD_MAX_MEMORY_SIZEwere 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 oftime.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.pyvalues taking precedence overPLAIN_-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
--outlineflag toplain docsCLI to display section headings for quick navigation (153502ee90f5) - Added
--searchflag toplain docsCLI to find which modules and sections mention a term (153502ee90f5) - Enhanced
plain docs --listto 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 requestto uselocalhostas SERVER_NAME and default the Accept header totext/html, matching typical browser behavior (01731c5485cf) - Updated
plain-bugskill to create GitHub Issues viaghCLI 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()andreverse_absolute()functions that prepend the scheme and domain using a newBASE_URLsetting (1e0d09f3ec70) - Added
reverse_absoluteandabsolute_urlas Jinja template globals for use in templates (1e0d09f3ec70) - Added
reverseas an explicit template global (previously only available asurl) (1e0d09f3ec70)
Upgrade instructions
- No changes required. To use absolute URLs, set
BASE_URLin 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
SyncWorkerfrom the built-in HTTP server;ThreadWorkeris now the only worker type (c38ee93de5b4) - Changed
plain serverdefaults to--workers auto(one per CPU core) and--threads 4for better out-of-the-box concurrency (c38ee93de5b4)
Upgrade instructions
- If you relied on
SyncWorkeror single-threaded behavior, explicitly pass--threads 1toplain server. - If you pinned
--workers 1, note the default is nowauto. Pass--workers 1explicitly to keep the old behavior.
0.106.2 (2026-02-13)
What's changed
- Added
--versionflag to theplainCLI to display the installed version (e76fd7070302)
Upgrade instructions
- No changes required.
0.106.1 (2026-02-13)
What's changed
- Added
--sectionflag toplain docsfor loading a specific##section by name (e.g.,plain docs models --section querying) (f2ce0243e6ea) - Simplified
plain docsoutput by removing XML-style wrapper tags around documentation content (f2ce0243e6ea) - Added
/plain-bugskill 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, allowingagents/README.mdto appear in docs output (f2ce0243e6ea)
Upgrade instructions
- No changes required.
0.106.0 (2026-02-12)
What's changed
- Added
plain checkcommand 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-containsfor automated response testing (6b66bfd05b9e) - Fixed
plain requestcrash when no user model exists (8ef8a3813bff) plain requestnow 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 installnow discovers and installs rules and skills fromplainx.*namespace packages in addition toplain.*packages (bd568db924f7)- Orphan cleanup during
plain agent installnow correctly handles bothplainandplainxprefixed 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
@internalcodedecorator from internal classes in favor of__all__exports (e7164d3891b2) - Renamed
plain docs --symbolsoption to--apifor clarity (e7164d3891b2)
Upgrade instructions
- If using
plain docs --symbols, update toplain docs --api.
0.104.0 (2026-02-04)
What's changed
- Refactored the assets manifest system with a clearer API:
AssetsManifestreplacesAssetsFingerprintsManifest, with explicit methodsadd_fingerprinted(),add_non_fingerprinted(),is_fingerprinted(), andresolve()(9cb84010b5fb) - Renamed
ASSETS_BASE_URLsetting toASSETS_CDN_URLfor clarity (9cb84010b5fb) - When
ASSETS_CDN_URLis configured,AssetsRouternow 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_URLtoASSETS_CDN_URLin your settings if you use a CDN for assets. - If you were importing from
plain.assets.fingerprints, update imports to useplain.assets.manifestinstead:AssetsFingerprintsManifest→AssetsManifestget_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.envfile (cecb71a016)
Upgrade instructions
- No changes required.
0.103.0 (2026-01-30)
What's changed
plain docsnow shows markdown documentation by default (previously required--source), with a new--symbolsflag to show only the symbolicated API surface (b71dab9d5d)plain docs --listnow shows all official Plain packages (installed and uninstalled) with descriptions and install status (9cba705d62)plain docsfor uninstalled packages now shows the install command and an online docs URL instead of a generic error (9cba705d62)- Removed the
plain agent contextcommand and theSessionStarthook setup — agent rules now provide context directly without needing a startup hook (88d9424643) plain agent installnow cleans up old SessionStart hooks from.claude/settings.json(88d9424643)
Upgrade instructions
- The
--sourceflag forplain docshas been removed. Use--symbolsinstead to see the symbolicated API surface. - The
--openflag forplain docshas been removed. - Run
plain agent installto 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 andagents/.claude/skills/directories instead ofskills/directories (512040ac51) - The
plain agent installcommand now copies both rules (.mdfiles) and skills to the project's.claude/directory, and cleans up orphanedplain*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 installto update your.claude/directory with the new rules-based structure.
0.101.2 (2026-01-28)
What's changed
- When
load_dotenv()is called withoverride=False(the default), command substitution is now skipped for keys that already exist inos.environ. This prevents redundant command execution in child processes that re-load the.envfile after inheriting resolved values from the parent, avoiding multiple auth prompts with tools like the 1Password CLI (2f6ff93499) - The
_execute_commandhelper now usesstdout=subprocess.PIPEinstead ofcapture_output=True, allowing stderr/tty to pass through for interactive prompts (2f6ff93499) - Updated templates README examples to use
idinstead ofpk(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
--reloadwhen an app'sassetsdirectory doesn't exist (df33f93ece) - The
plain agent installcommand now preserves user-created skills (those without theplain-prefix) instead of removing them as orphans (bbc87498ed)
Upgrade instructions
- No changes required.
0.101.0 (2026-01-15)
What's changed
- The
plain servercommand now accepts--workers auto(orWEB_CONCURRENCY=auto) to automatically set worker count based on CPU count (02a1769948) - Response headers can now be set to
Noneto opt out of default headers;Nonevalues are filtered out at the WSGI layer rather than being deleted by middleware (cbf27e728d) - Removed unused
Responsemethods:serialize_headers,serialize, file-like interface stubs (write,flush,tell,readable,seekable,writable,writelines),textproperty, pickling support, andgetvalue(cbf27e728d)
Upgrade instructions
- No changes required
0.100.1 (2026-01-15)
What's changed
- The
plain agent installcommand now only sets up session hooks for Claude Code, not Codex, since thesettings.jsonhook format is Claude Code-specific (a41e08bcd2)
Upgrade instructions
- No changes required
0.100.0 (2026-01-15)
What's changed
- The
plain skillscommand has been renamed toplain agentwith new subcommands:plain agent install(installs skills and sets up hooks),plain agent skills(lists available skills), andplain agent context(outputs framework context) (fac8673436) - Added
SessionStarthook that automatically runsplain agent contextat the start of every Claude Code or Codex session, providing framework context without needing a separate skill (fac8673436) - The
plain-principlesskill has been removed - its content is now provided by theplain agent contextcommand via the SessionStart hook (fac8673436) - Added
--no-headersand--no-bodyflags toplain requestfor limiting output (fac8673436)
Upgrade instructions
- Replace
plain skills --installwithplain agent install - Replace
plain skills(without flags) withplain agent skills - Run
plain agent installto 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.dotenvmodule withload_dotenv()andparse_dotenv()functions for bash-compatible.envfile 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_PACKAGESnot 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 --installcommand now removes orphaned skills from destination directories when skills are renamed or removed from packages (d51294ace1) - Added README documentation for
plain.skillswith 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, andSuspiciousOperationError400variants) moved fromplain.exceptionstoplain.http.exceptionsand are now exported fromplain.http(b61f909e29)
Upgrade instructions
Update imports of HTTP exceptions from
plain.exceptionstoplain.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:
ResponseRedirect→RedirectResponse,ResponseNotModified→NotModifiedResponse,ResponseNotAllowed→NotAllowedResponse(fad5bf28b0) - Redundant response classes removed:
ResponseNotFound,ResponseForbidden,ResponseBadRequest,ResponseGone,ResponseServerError- useResponse(status_code=X)instead (fad5bf28b0) - HTTP exceptions renamed to include status code suffix:
Http404→NotFoundError404,PermissionDenied→ForbiddenError403,BadRequest→BadRequestError400,SuspiciousOperation→SuspiciousOperationError400(5a1f020f52) - Added
Secret[T]type annotation for masking sensitive settings likeSECRET_KEYin CLI output (8713dc08b0) - Added
ENV_SETTINGS_PREFIXESsetting to configure which environment variable prefixes are checked for settings (defaults to["PLAIN_"]) (8713dc08b0) - New
plain settings listandplain settings getCLI commands for viewing settings with their sources (8713dc08b0) - Added preflight check for unused environment variables matching configured prefixes (8713dc08b0)
- Renamed
request.metatorequest.environfor clarity (786b95bef8) - Added
request.query_stringandrequest.content_lengthproperties (786b95bef8, 76dfd477d2) - Renamed X-Forwarded settings:
USE_X_FORWARDED_HOST→HTTP_X_FORWARDED_HOST,USE_X_FORWARDED_PORT→HTTP_X_FORWARDED_PORT,USE_X_FORWARDED_FOR→HTTP_X_FORWARDED_FOR(22f241a55c) - Changed
HTTPS_PROXY_HEADERfrom a tuple to a string format (e.g.,"X-Forwarded-Proto: https") (7ac2a431b6)
Upgrade instructions
Replace Response class imports and usages:
ResponseRedirect→RedirectResponseResponseNotModified→NotModifiedResponseResponseNotAllowed→NotAllowedResponseResponseNotFound→Response(status_code=404)ResponseForbidden→Response(status_code=403)ResponseBadRequest→Response(status_code=400)ResponseGone→Response(status_code=410)ResponseServerError→Response(status_code=500)
Replace exception imports and usages:
Http404→NotFoundError404PermissionDenied→ForbiddenError403BadRequest→BadRequestError400SuspiciousOperation→SuspiciousOperationError400SuspiciousMultipartForm→SuspiciousMultipartFormError400SuspiciousFileOperation→SuspiciousFileOperationError400TooManyFieldsSent→TooManyFieldsSentError400TooManyFilesSent→TooManyFilesSentError400RequestDataTooBig→RequestDataTooBigError400
Replace
request.metawithrequest.environRename X-Forwarded settings in your configuration:
USE_X_FORWARDED_HOST→HTTP_X_FORWARDED_HOSTUSE_X_FORWARDED_PORT→HTTP_X_FORWARDED_PORTUSE_X_FORWARDED_FOR→HTTP_X_FORWARDED_FOR
Update
HTTPS_PROXY_HEADERfrom 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 withplain settings get <name>
0.95.0 (2025-12-22)
What's changed
- Improved thread worker server shutdown behavior with
cancel_futures=Truefor faster and cleaner process termination (72d0620)
Upgrade instructions
- No changes required
0.94.0 (2025-12-12)
What's changed
FormFieldMissingErrorexceptions 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_idattribute 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
TimestampSignerto use composition instead of inheritance fromSigner, maintaining the same public API (ac1eeb0)
Upgrade instructions
- No changes required
0.92.0 (2025-12-01)
What's changed
- Added
request.client_ipproperty to get the client's IP address, with support forX-Forwarded-Forheader when behind a trusted proxy (cb0bc5d) - Added
USE_X_FORWARDED_FORsetting to enable reading client IP fromX-Forwarded-Forheader (cb0bc5d) - Improved
print_eventCLI 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.dataattribute has been replaced withrequest.json_dataandrequest.form_datafor explicit content-type handling (90332a9) QueryDictnow has proper type annotations forget(),pop(),getlist(), and__getitem__()methods that reflect string return types (90332a9)- Forms now automatically select between
json_dataandform_databased on request content-type (90332a9) - View mixins
ObjectTemplateViewMixinremoved in favor of class inheritance for better typing -UpdateViewandDeleteViewnow inherit fromDetailView(569afd6) AppLoggercontext logging now uses acontextdict parameter instead of**kwargsfor better type checking (581b406)- Removed erroneous
AuthViewMixinexport fromplain.views(334bbb6)
Upgrade instructions
Replace
request.datawith the appropriate method:- For JSON requests: use
request.json_data(returns a dict, raisesBadRequestfor invalid JSON) - For form data: use
request.form_data(returns aQueryDict)
- For JSON requests: use
Update
app_loggercalls that pass context as kwargs to use thecontextparameter:# 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()andis_naive()now accept bothdatetimeandtimeobjects 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.formsmodule 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
@abstractmethoddecorators instead of raisingNotImplementedError, improving type checking and IDE support (91b329a, 81b5f88, d2e2423, 61e7b5a) - Updated to latest version of
tytype 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
CIenvironment 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 preflightcommand 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_STREAMsetting 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_STREAMenvironment 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 mdcommand now saves a combinedAGENTS.mdfile to.plain/by default when usingplain 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 inpyproject.toml(8477355)
Upgrade instructions
- No changes required
0.84.0 (2025-10-29)
What's changed
- The
DEFAULT_RESPONSE_HEADERSsetting 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
Noneto 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_HEADERSconfigured 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 toNoneinstead
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_scriptutility function now accepts an optionalnonceparameter for CSP-compliant inline JSON scripts (784f3dd972)
Upgrade instructions
- Any
|json_scriptusages 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_HEADERSsetting can now be a callable that accepts a request argument, enabling dynamic header generation per request (cb92905834) - Added
request.csp_noncecached property for generating Content Security Policy nonces (75071dcc70) - Simplified the preflight command by moving
plain preflight checkback toplain preflight(40c2c4560e)
Upgrade instructions
- If you use
plain preflight check, update toplain preflight(thechecksubcommand has been removed for simplicity) - If you use
plain preflight check --deploy, update toplain preflight --deploy
0.81.0 (2025-10-22)
What's changed
- Removed support for category-specific error template fallbacks like
4xx.htmland5xx.html(9513f7c4fa)
Upgrade instructions
- If you have
4xx.htmlor5xx.htmlerror 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 ofPermissionDenied(HTTP 403) (ad146bde3e) - Error templates can now use category-specific fallbacks like
4xx.htmlor5xx.htmlinstead of the genericerror.html(716cfa3cfc) - Updated error template documentation with best practices for self-contained
500.htmltemplates (55cea3b522)
Upgrade instructions
- If you have a
templates/error.htmltemplate, 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 like4xx.htmlor5xx.htmlfor broader coverage.
0.79.0 (2025-10-22)
What's changed
- Response objects now have an
exceptionattribute that stores the exception that caused 5xx errors (0a243ba89c) - Middleware classes now use an abstract base class
HttpMiddlewarewith aprocess_request()method (b960eed6c6) - CSRF middleware now raises
PermissionDeniedinstead of rendering a customCsrfFailureView(d4b93e59b3) - The
HTTP_ERROR_VIEWSsetting has been removed (7a4e3a31f4) - Standalone
plain-changelogandplain-upgradeexecutables have been removed in favor of the built-in commands (07c3a4c540) - Standalone
plain-buildexecutable 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
HttpMiddlewareand rename your__call__()method toprocess_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 responseRemove any custom
HTTP_ERROR_VIEWSsetting from your configuration - error views are now controlled entirely by exception handlersIf you were calling
plain-changelogorplain-upgradeas standalone commands, useplain changelogorplain upgradeinsteadIf you were calling
plain-buildas a standalone command, useplain buildinstead
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
getLoggerinstead of directly instantiatingLoggerfor 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_IMPORTsetting to customize what gets automatically imported inplain shell(9055f59c08) - Views that return
Nonenow raiseHttp404instead of returningResponseNotFound(5bb60016eb) - The
plain chores listcommand output formatting now matches theplain jobs listformat (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
Chorebase class fromplain.choreswhen creating new chores
0.77.0 (2025-10-13)
What's changed
- The
plain server --reloadnow useswatchfilesfor 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 servercommand with built-in WSGI server (vendored gunicorn) (f9dc2867c7) - The
plain servercommand supportsWEB_CONCURRENCYenvironment variable for worker processes (0c3e8c6f32) - Simplified server startup logging to use a single consolidated log line (b1405b71f0)
- Removed
gunicornas an external dependency - server functionality is now built into plain core (cb6c2f484d) - Internal server environment variables renamed from
GUNICORN_*toPLAIN_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
gunicornwith the newplain servercommand (ex.gunicorn plain.wsgi:app --workers 4becomesplain server --workers 4) - Update any deployment scripts or Procfiles that use
gunicornto useplain serverinstead - Remove
gunicornfrom your project dependencies if you added it separately (it's now built into plain) - For Heroku deployments, the
$PORTis not automatically detected - update your Procfile toweb: plain server --bind 0.0.0.0:$PORT - If you were using gunicorn configuration files, migrate the settings to
plain servercommand-line options (runplain server --helpto see available options)
0.75.0 (2025-10-10)
What's changed
- Documentation references updated from
plain-workertoplain-jobsfollowing the package rename (24219856e0)
Upgrade instructions
- No changes required
0.74.0 (2025-10-08)
What's changed
- The
plain agent requestcommand 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_optionsinstead of_metafor 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
ClientResponsewrapper 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.userinstead ofself.request.user) (f6278d9)
Upgrade instructions
- No changes required
0.72.0 (2025-10-02)
What's changed
- Request attributes
userandsessionare 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.AuthenticationMiddlewarefrom default middleware configuration (154ee10)
Upgrade instructions
- No changes required
0.71.0 (2025-09-30)
What's changed
- Renamed
HttpRequesttoRequestthroughout the codebase for consistency and simplicity (cd46ff20) - Renamed
HttpHeaderstoRequestHeadersfor naming consistency (cd46ff20) - Renamed settings:
APP_NAME→NAME,APP_VERSION→VERSION,APP_LOG_LEVEL→LOG_LEVEL,APP_LOG_FORMAT→LOG_FORMAT,PLAIN_LOG_LEVEL→FRAMEWORK_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.pyto be static methods ofQueryDict(0e1b0133)
Upgrade instructions
- Replace all imports and usage of
HttpRequestwithRequest - Replace all imports and usage of
HttpHeaderswithRequestHeaders - Update any custom settings that reference
APP_NAMEtoNAME,APP_VERSIONtoVERSION,APP_LOG_LEVELtoLOG_LEVEL,APP_LOG_FORMATtoLOG_FORMAT, andPLAIN_LOG_LEVELtoFRAMEWORK_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
Assetclass inplain.assets.findersis now a module-level public class instead of being defined insideiter_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 fromplain.exceptionstoplain.models.exceptions(1c02564) - Added
plain devalias prompt that suggests addingpas a shell alias for convenience (d913b44)
Upgrade instructions
- Replace imports of
FieldDoesNotExist,FieldError,ObjectDoesNotExist,MultipleObjectsReturned,EmptyResultSet, orFullResultSetfromplain.exceptionstoplain.models.exceptions - If you're using
ObjectDoesNotExistin views, update your import fromplain.exceptions.ObjectDoesNotExisttoplain.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
fixfield (c7cde12149) - Added
plain-upgradeas a standalone command for upgrading Plain packages (42f2eed80c)
Upgrade instructions
- Update any uses of the
plain preflightcommand toplain preflight check, and remove the--databaseand--fail-leveloptions which no longer exist - Custom preflight checks should be class based, extending
PreflightCheckand implementing therun()method - Preflight checks need to be registered with a custom name (ex.
@register_check("app.my_custom_check")) and optionally withdeploy=Trueif it should run in only in deploy mode - Preflight results should use
PreflightResult(optionally withwarning=True) instead ofpreflight.Warningorpreflight.Error - Preflight result IDs should be descriptive strings (e.g.,
models.lazy_reference_resolution_failed) instead of numeric codes PREFLIGHT_SILENCED_CHECKSsetting has been replaced withPREFLIGHT_SILENCED_RESULTSwhich should contain a list of result IDs to silence.PREFLIGHT_SILENCED_CHECKSnow silences entire checks by name.
0.67.0 (2025-09-22)
What's changed
ALLOWED_HOSTSnow defaults to[](empty list) which allows all hosts, making it easier for development setups (d3cb7712b9)- Empty
ALLOWED_HOSTSin 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_HOSTSsetting is now required (6a4b7be) - Changed
request.get_port()method torequest.portcached property (544f3e1) - Removed internal
request._get_full_path()method (50cdb58)
Upgrade instructions
- Add
ALLOWED_HOSTSsetting to your configuration if not already present (required for host validation) - Replace any usage of
request.get_host()withrequest.host - Replace any usage of
request.get_port()withrequest.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_HOSTSfor IP address range validation (c485d21)
Upgrade instructions
- No changes required
0.64.0 (2025-09-19)
What's changed
- Added
plain-buildcommand as a standalone executable (4b39ca4) - Removed
constant_time_compareutility function in favor ofhmac.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_comparewithhmac.compare_digestorsecrets.compare_digest
0.63.0 (2025-09-12)
What's changed
- Model manager attribute renamed from
objectstoquerythroughout codebase (037a239) - Simplified HTTPS redirect middleware by removing
HTTPS_REDIRECT_EXEMPT_PATHSandHTTPS_REDIRECT_HOSTsettings (d264cd3) - Database backups are now created automatically during migrations when
DEBUG=Trueunless explicitly disabled (c802307)
Upgrade instructions
- Remove any
HTTPS_REDIRECT_EXEMPT_PATHSandHTTPS_REDIRECT_HOSTsettings 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.kvremoval 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.kvin 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 withapp_logger.info("message", key=value)or appropriate log level
0.61.0 (2025-09-03)
What's changed
- Added new
plain agentcommand with subcommands for coding agents includingdocs,md, andrequest(df3edbf) - Added
-coption toplain shellto execute commands and exit, similar topython -c(5e67f0b) - The
plain docs --llmfunctionality has been moved toplain agent docscommand (df3edbf) - Removed the
plain helpcommand in favor of standardplain --help(df3edbf)
Upgrade instructions
- Replace
plain docs --llmusage withplain agent docscommand - Use
plain --helpinstead ofplain helpcommand
0.60.0 (2025-08-27)
What's changed
- Added new
APP_VERSIONsetting that defaults to the project version frompyproject.toml(57fb948d46) - Updated
get_app_name_from_pyproject()toget_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_NAMEsetting that defaults to the project name frompyproject.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
.objectand.objectsto improve performance (bd0507a) - Improved
plain upgradecommand 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 asself.objectis 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_PATHSsetting (2a50a9154e) - Renamed
HTTPS_REDIRECT_EXEMPTtoHTTPS_REDIRECT_EXEMPT_PATHSwith 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
CsrfExemptViewMixinandrequest.csrf_exemptand add exempt paths to theCSRF_EXEMPT_PATHSsetting 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_EXEMPTwithHTTPS_REDIRECT_EXEMPT_PATHSand 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
ResponsePermanentRedirectclass has been removed; useResponseRedirectwithstatus_code=301instead (d5735ea) - The
RedirectView.permanentattribute has been replaced withstatus_codefor more flexible redirect status codes (12dda16) - Updated
RedirectViewinitialization parameters:url_namereplacespattern_name,preserve_query_paramsreplacesquery_string, and removed 410 Gone functionality (3b9ca71)
Upgrade instructions
- Replace
ResponsePermanentRedirectimports withResponseRedirectand passstatus_code=301to the constructor - Update
RedirectViewsubclasses to usestatus_code=301instead ofpermanent=True - Replace
pattern_namewithurl_namein RedirectView usage - Replace
query_string=Truewithpreserve_query_params=Truein RedirectView usage
0.56.1 (2025-07-30)
What's changed
- Improved
plain installcommand 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 installcommand to install Plain packages with agent-assisted setup (bf1873e) - Added
--printoption to agent commands (plain installandplain upgrade) to print prompts without running the agent (9721331) - The
plain docscommand now automatically converts hyphens to dots in package names (e.g.,plain-models→plain.models) (1e3edc1) - Moved
plain-upgradefunctionality 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
idinstead ofpkin URL kwargs (b656ee6) - Updated views documentation examples to use
idinstead ofpkfor 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 useself.url_kwargs["id"]instead - Replace any QuerySet filters using
pkwithid(e.g.,Model.query.get(pk=1)becomesModel.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-observerpackage reference to plain README (f29ff4dafe)
Upgrade instructions
- No changes required
0.53.0 (2025-07-18)
What's changed
- Added a
pluralizefilter for Jinja templates to handle singular/plural forms (4cef9829ed) - Added
get_signed_cookie()method toHttpRequestfor retrieving and verifying signed cookies (f8796c8786) - Improved CLI error handling by using
click.UsageErrorinstead 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
AssetsRouterreference 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 helpoutput on newer versions of Click by switching fromMultiCommandtoGroupwhen determining sub-commands (9482e42)
Upgrade instructions
- No changes required
0.52.0 (2025-06-26)
What's changed
- Added
plain-changelogas a standalone executable so you can view changelogs without importing the full framework (e4e7324) - Removed the runtime dependency on the
packaginglibrary 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.ddon Python 3.11 (ed24276)
Upgrade instructions
- No changes required
0.51.0 (2025-06-24)
What's changed
- New
plain changelogCLI sub-command to quickly view a package’s changelog from the terminal. Supports--from/--toflags 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 listinstead of the oldplain urlscommand (6146fcb) plain preflightgains a simpler--databaseflag 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
DATABASEsetting instead ofDATABASES/DATABASE_ROUTERS(d346d81)
Upgrade instructions
Update any scripts or documentation that call
plain urls …:- Replace
plain urls --flatwithplain urls list --flat
- Replace
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
- Replace
In
settings.pymigrate 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
DATABASESandDATABASE_ROUTERSsettings – they are no longer read.