ploneasgi.zcml#
A ZCML directive for registering native ASGI routes.
An add-on cannot edit the project's server.py, so there has to be some way
for it to contribute a websocket endpoint or a native HTTP route. In this
ecosystem that mechanism is ZCML, and this module supplies one directive:
<configure xmlns:ploneasgi="http://namespaces.plone.org/ploneasgi">
<ploneasgi:route
path="/notify"
handler=".endpoints.notify"
type="websocket"
permission="cmf.ManagePortal"
/>
</configure>
Paths are relative to a reserved prefix (/++asgi++ by default), so that one
lands at /++asgi++/notify. That is not cosmetic: it is what keeps a
deployment to a single proxy rule.
ploneasgi.vhm.VirtualHostMiddleware recognizes the prefix inside an
ordinary VirtualHostMonster rewrite, so registering a new route needs no
infrastructure change — and the Plone site comes out of the rewrite rather than
being restated in every path.
Two deliberate limits, both consequences of how ZCML actually works:
Registrations are process-wide, not per-site. ZCML executes once at startup into the global registry; local (persistent) registrations live in the ZODB and can change at runtime, so a startup snapshot of them would go stale the moment an add-on is installed. Which site a request enters is resolved per request from the rewrite, so one registration serves every site.
Discovery is explicit.
routes_from_registry()is called by the project, not byploneasgi.app.create_app(). "Adding a route means passing a route" still holds; this just lets an add-on put one in the pile.
The permission attribute is the reason this is worth having over a plain list
of routes. It is checked against the portal, inside
ploneasgi.context.run_authenticated(), before the handler is entered — so
an endpoint cannot forget to authorize itself.
The two route types run differently, because they last differently:
An http handler is a sync function, called in the worker thread inside the same context the permission was checked in:
def whoami(ctx: PloneContext, request: Request) -> Response: return JSONResponse({"user": ctx.user.getUserName()})
One thread hop, one ZODB connection, one authentication per request. A
StreamingResponseorFileResponsestill sends its body on the event loop, after the thread is released.A websocket handler is
async def handler(websocket). A session lasts as long as the client stays, and must not hold a worker thread; the check runs in a call of its own before the socket is accepted.
- ploneasgi.zcml.WS_POLICY_VIOLATION = 1008#
Websocket close code for a policy violation (RFC 6455). Used when a connection is refused for lack of a permission.
- class ploneasgi.zcml.ASGIRoute[source]#
One registered route, as declared in ZCML.
- Variables:
path -- Path pattern, relative to the reserved prefix.
handler -- The endpoint callable:
handler(ctx, request)returning aResponsefor http,async handler(websocket)for websocket.type --
httporwebsocket.methods -- HTTP methods; ignored for websocket routes.
permission -- Id of the permission required on the portal, or
None.site -- Fallback site id for requests arriving with no VHM rewrite.
name -- Route name.
- ploneasgi.zcml.build_route(route, prefix='/++asgi++')[source]#
Turn a registration into a Starlette route.
Declared paths are relative to the reserved prefix. Mounting every native route under one prefix is what lets a deployment keep a single proxy rule:
ploneasgi.vhm.VirtualHostMiddlewarerecognizes the prefix inside an ordinary VirtualHostMonster rewrite, so adding a route needs no proxy change at all.
- ploneasgi.zcml.routes_from_registry(registry=None, prefix='/++asgi++')[source]#
Collect every ZCML-registered route as a Starlette route.
Call this after Zope has started — ZCML has not executed before that — and pass the result to
ploneasgi.app.create_app():wsgi_app = build_wsgi_app(settings) app = create_app(settings, routes=routes_from_registry(), wsgi_app=wsgi_app)
Ordering is by type and path so the route table is deterministic across restarts; Starlette matches in list order, and a non-deterministic order would make an overlapping pattern win at random.
- ploneasgi.zcml.routeDirective(_context, path, handler, type='http', methods='GET', permission=None, site=None, name='', unguarded=False)[source]#
Register one ASGI route from ZCML.
- Parameters:
_context (Any) -- The ZCML configuration context.
path (str) -- Starlette path pattern.
handler (Callable[[...], Any]) -- The endpoint callable, already resolved by
GlobalObject.type (str) --
httporwebsocket.methods (str) -- Space-separated HTTP methods.
permission (str | None) -- Permission required on the portal.
site (str | None) -- Fallback site id for requests with no VHM rewrite.
name (str) -- Route name.
unguarded (bool) -- Required when
permissionis omitted.
- Raises:
ValueError -- On an unknown type, a missing site, or an unacknowledged missing permission.
- Return type:
None