SSE contract cross-checks.
contracts.rules_sse
| Name | Type | Default | Description |
|---|---|---|---|
type
|
|
— | |
qualified_name
|
|
— | |
element_type
|
|
— | |
description
|
|
— | |
source_file
|
|
— | |
line_number
|
|
— | |
is_autodoc
|
|
— | |
autodoc_element
|
|
— | |
_autodoc_template
|
|
— | |
_autodoc_url_path
|
|
— | |
_autodoc_page_type
|
|
— | |
title
|
|
— | |
doc_content_hash
|
|
— |
Symbols on this page
Remove Kida + HTML comments so element scans ignore illustrative markup.
Replace Kida expressions so route-pattern matching still works.
Extract all sse-swap event names from source (comments stripped).
Return(confident, value)for a string keyword argument.
Find user-accessor calls and long-lived loops inside a generator body.
found records whether get_user() / current_user()is called
anywhere in the walked tree;in_long_lived_loop…
Return the single positional argument of the firstEventStream(...)call.
EventStream(generate()) -> the generate()call node;
EventStream(gen) -> the gen Name node. Returns None…
Resolve the generator-producing name from anEventStream(...)arg.
generate() -> "generate"; gen -> "gen". Nonefor any other shape (e.g. an inline…
Find a nestedasync def/defnamed name inside tree.
Resolve a MODULE-LEVEL generator function by name via__globals__.
The handler-source AST walk only sees generators defined as NESTED functions inside the handler.…
Return a populated_UserReadFinderfor an EventStream route's generator.
ReturnsNone when the route is NOT an EventStreamroute, or its source/generator cannot be…
Flag an EventStream generator that reads the user with noAuthMiddleware.
AnEventStream route whose generator calls get_user()/
current_user()resolves the connect-time-pinned…
Nudge that SSE user identity is connect-time-pinned in a long-lived stream.
POST-FIX residual guard, NOT a broken-pattern flag: reading the user inside
anEventStream…
Infer literal SSE event names emitted by a route handler.
ReturnsNonewhen source is unavailable or a relevant event name is dynamic. Dynamic…
Error when sse-swap appears on same element as sse-connect.
Warn when sse-connect is inside broad hx-target scope without mitigation.
Cross-reference sse-swap values against declared and inferred events.
strip_template_comments
function
def strip_template_comments(source: str) -> str
Remove Kida + HTML comments so element scans ignore illustrative markup.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
source
|
str
|
— |
normalize_sse_url
function
def normalize_sse_url(url: str) -> str
Replace Kida expressions so route-pattern matching still works.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
url
|
str
|
— |
extract_sse_swap_values
function
def extract_sse_swap_values(source: str) -> set[str]
Extract all sse-swap event names from source (comments stripped).
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
source
|
str
|
— |
_string_kwarg
function
def _string_kwarg(node: ast.Call, name: str) -> tuple[bool, str | None]
Return(confident, value)for a string keyword argument.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
node
|
ast.Call
|
— | |
name
|
str
|
— |
_UserReadFinder
class
Find user-accessor calls and long-lived loops inside a generator body.
found records whether get_user() / current_user()is called
anywhere in the walked tree;in_long_lived_looprecords whether any such
call sits inside awhile / for / async for(the long-lived SSE
pump where connect-time identity pinning becomes a staleness caveat). A bare
user read at generator top-level (outside any loop) is treated as
short-lived — it resolves once and the stream ends.
_eventstream_generator_arg
function
def _eventstream_generator_arg(handler_tree: ast.AST) -> ast.expr | None
Return the single positional argument of the firstEventStream(...)call.
EventStream(generate()) -> the generate()call node;
EventStream(gen) -> the gen Name node. Returns Nonewhen no
EventStream(...)call with a first positional arg is found.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
handler_tree
|
ast.AST
|
— |
_generator_callable_name
function
def _generator_callable_name(gen_arg: ast.expr) -> str | None
Resolve the generator-producing name from anEventStream(...)arg.
generate() -> "generate"; gen -> "gen". Nonefor any
other shape (e.g. an inline comprehension or a method call) — the caller
falls back to walking the whole handler tree.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
gen_arg
|
ast.expr
|
— |
_find_nested_funcdef
function
def _find_nested_funcdef(tree: ast.AST, name: str) -> ast.AST | None
Find a nestedasync def/defnamed name inside tree.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
tree
|
ast.AST
|
— | |
name
|
str
|
— |
_resolve_module_level_generator
function
def _resolve_module_level_generator(handler: Any, name: str) -> ast.AST | None
Resolve a MODULE-LEVEL generator function by name via__globals__.
The handler-source AST walk only sees generators defined as NESTED
functions inside the handler. A module-levelasync def gen()passed as
EventStream(gen())is NOT in the handler source — so we resolve the
bare name against the handler's__globals__and parse THAT function's
source. ReturnsNonewhen the name is not a module-level function, or
its source is unavailable.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
handler
|
Any
|
— | |
name
|
str
|
— |
_analyze_eventstream_generator
function
def _analyze_eventstream_generator(handler: Any) -> _UserReadFinder | None
Return a populated_UserReadFinderfor an EventStream route's generator.
ReturnsNone when the route is NOT an EventStreamroute, or its
source/generator cannot be statically resolved (errs toward silence). The
generator is resolved in two scopes:
- NESTED — an inline
async def generate()inside the handler (the common case). Walked directly from the handler tree. - MODULE-LEVEL — a bare name passed as
EventStream(gen())resolved via the handler's__globals__and parsed separately (see_resolve_module_level_generator()).
Falls back to walking the WHOLE handler tree when the generator argument's shape is not a plain name/call (e.g. an inline comprehension) so an inline user read is still seen.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
handler
|
Any
|
— |
check_sse_auth_gate
function
def check_sse_auth_gate(router: Router, config: Any, middleware_list: list[Any]) -> list[ContractIssue]
Flag an EventStream generator that reads the user with noAuthMiddleware.
AnEventStream route whose generator calls get_user()/
current_user()resolves the connect-time-pinned user — but only when
AuthMiddleware is wired. Without it the captured user is AnonymousUser
for the entire stream, so an auth-sensitive SSE feed silently serves the
anonymous view to everyone. Parallelsauth_middleware: env-aware (ERROR
production / WARNING staging / silent development — the dev behavior surfaces
locally), detected by middleware class NAME.
SCOPE: the static AST walk resolves the generator in two scopes — an inline
nestedasync def inside the handler, AND a module-level async def
passed asEventStream(gen()) (resolved via the handler __globals__).
A generator built by any other indirection (a factory, a method, a value
threaded through another call) is not statically resolvable and is silently
skipped — errs toward silence, never a false ERROR.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
router
|
Router
|
— | |
config
|
Any
|
— | |
middleware_list
|
list[Any]
|
— |
check_sse_context
function
def check_sse_context(router: Router, config: Any) -> list[ContractIssue]
Nudge that SSE user identity is connect-time-pinned in a long-lived stream.
POST-FIX residual guard, NOT a broken-pattern flag: reading the user inside
anEventStreamgenerator now WORKS (connect-time capture). This only
surfaces the semantic caveat — when the user is read inside a LONG-LIVED
loop (while / async for / for), the identity is pinned at connect
and is NOT refreshed on a mid-stream logout / permission change for the life
of the connection. WARNING (env-aware: silent development, WARNING staging /
production); never ERROR — the pattern is correct, this is a known-semantic
nudge for auth-sensitive long-lived streams.
Same two-scope resolution (and the same single-indirection blind spot) as
check_sse_auth_gate(). A short-lived top-level user read (outside any
loop) is NOT nudged — it resolves once and the stream ends.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
router
|
Router
|
— | |
config
|
Any
|
— |
_infer_emitted_events
function
def _infer_emitted_events(handler: Any) -> set[str] | None
Infer literal SSE event names emitted by a route handler.
ReturnsNonewhen source is unavailable or a relevant event name is
dynamic. Dynamic cases are skipped by the cross-reference check so the
contract errs toward silence instead of false positives.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
handler
|
Any
|
— |
check_sse_self_swap
function
def check_sse_self_swap(template_sources: dict[str, str]) -> list[ContractIssue]
Error when sse-swap appears on same element as sse-connect.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
template_sources
|
dict[str, str]
|
— |
check_sse_connect_scope
function
def check_sse_connect_scope(template_sources: dict[str, str], broad_targets: set[str]) -> list[ContractIssue]
Warn when sse-connect is inside broad hx-target scope without mitigation.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
template_sources
|
dict[str, str]
|
— | |
broad_targets
|
set[str]
|
— |
check_sse_event_crossref
function
def check_sse_event_crossref(template_sources: dict[str, str], router: Router) -> list[ContractIssue]
Cross-reference sse-swap values against declared and inferred events.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
template_sources
|
dict[str, str]
|
— | |
router
|
Router
|
— |
View source · /home/runner/work/chirp/chirp/site/../src/chirp/contracts/rules_sse.py:1