API reference

The public constants and classes are importable from the package root:

from action0.req import Header, Headers, Method, Request, Response, Status
from action0.req import BodyProducer, BytesBody

Headers and Header

The Headers mapping and the known header field names (Header).

action0.req.headers.HeaderValue = str | int | float | bool

A single header value; non-strings are coerced to strings on the way in (bools become the web-style "true" / "false").

action0.req.headers.HeaderTypes = typing.Iterable[tuple[str, str | int | float | bool | typing.Iterable[str | int | float | bool]]] | typing.Mapping[str, str | int | float | bool | typing.Iterable[str | int | float | bool]] | str

Everything that can initialize a Headers instance: a raw header block, a mapping, or an iterable of name/value(s) tuples.

class action0.req.headers.Headers(headers=None)[source]

An ordered, case-insensitive, multi-value aware mapping of HTTP header fields. Internally it is a list of (name, value) lines: the order and casing of the representation are preserved exactly, while every lookup matches field names case-insensitively (per RFC 9110headers["content-type"] and headers["Content-Type"] hit the same field). Names are never normalized beyond that; in particular an underscore is not treated as a dash.

Headers implements typing.MutableMapping: the mapping view (headers[name], get(), items(), …) works with a single value per name — the last one, like singles(). Multi-value access is available through get_all(), get_values(), add(), as_dict() and as_lines().

Example:

>>> headers = Headers({"Content-Type": "text/html"})
>>> headers.add("Set-Cookie", "a=1")
>>> headers.add("set-cookie", "b=2")
>>> headers["content-type"]
'text/html'
>>> headers.get_all("Set-Cookie")
['a=1', 'b=2']
>>> len(headers), "SET-COOKIE" in headers
(2, True)
>>> headers.as_str()
'Content-Type: text/html\r\nSet-Cookie: a=1\r\nset-cookie: b=2'

repr() (and thus str()) redacts the values of the secret_names fields; only as_str() renders them:

>>> headers["Authorization"] = "Bearer secret-token"
>>> headers
Headers(Content-Type: text/html, Set-Cookie: ***, set-cookie: ***, Authorization: ***)
Parameters:
  • headers (Iterable[tuple[str, str | int | float | bool | Iterable[str | int | float | bool]]] | Mapping[str, str | int | float | bool | Iterable[str | int | float | bool]] | str | None, default: None)

  • headers – the initial header lines, either as a raw header block (lines of "Name: value" separated by "\r\n" or "\n"; blank lines are skipped, obsolete line folding is not supported), another Headers instance whose lines are copied, or as a list of tuples or a dictionary. The values can be single values or lists of values (one line each); non-string values are coerced to strings (bools become “true” / “false”).

Raises:

ValueError – if a line of a raw header block has no ":"

secret_names: frozenset[str] = frozenset({'authorization', 'cookie', 'proxy-authorization', 'set-cookie', 'x-api-key', 'x-csrf-token'})

The (casefolded) names of the fields whose values carry secrets and are shown as *** by repr(). Override on a subclass or instance to redact more (or fewer) fields.

__getitem__(name)[source]

The single value of the field; if the field has multiple lines, the value of the last one — like singles(). Use get_all() for all values.

Parameters:

name (str) – the field name (case-insensitive)

Return type:

str

Returns:

the (last) value of the field

Raises:

KeyError – if the field does not exist

__setitem__(name, value)[source]

Replace all lines of the field, same as set().

Parameters:
Return type:

None

__delitem__(name)[source]

Remove the field with all its lines.

Parameters:

name (str) – the field name (case-insensitive)

Raises:

KeyError – if the field does not exist

Return type:

None

__iter__()[source]
Return type:

Iterator[str]

Returns:

an iterator over the distinct field names in the order of their first line, each in the casing of that first line

__len__()[source]
Return type:

int

Returns:

the number of distinct field names

__contains__(name)[source]
Parameters:

name (object) – the field name (case-insensitive)

Return type:

bool

Returns:

whether a field with this name exists

get_all(name)[source]

The values of all lines of the field, in representation order; use headers[name] or get() for the single-value view and get_values() for the comma-split element view.

Parameters:

name (str) – the field name (case-insensitive)

Return type:

list[str]

Returns:

the values as a list, an empty list if the field does not exist

get_values(name)[source]

The elements of all lines of the field: like get_all(), but each line is additionally split on "," (RFC 9110 list syntax) with whitespace stripped and empty elements dropped.

WARNING: Set-Cookie does not use the list syntax — its values may contain literal commas (e.g. in an Expires attribute), so use get_all() for it.

Parameters:

name (str) – the field name (case-insensitive)

Return type:

list[str]

Returns:

the elements as a list, an empty list if the field does not exist

add(name, value)[source]

Append a line for each given value at the end, keeping existing lines of the field.

Parameters:
Return type:

None

remove(name, value=None)[source]

If only a name is given all lines of this field are removed. If a value or a list of values is given only the matching lines are removed.

Parameters:
  • name (str) – the name of the field to remove (or from which lines are to be removed), case-insensitive

  • value (str | int | float | bool | Iterable[str | int | float | bool] | None, default: None) – if given, only lines with matching value(s) are to be removed, not the entire field

Return type:

list[str]

Returns:

a list of removed values

set(name, value)[source]

Replace all lines of the field with the value(s) given, at the position of the field’s first line (new fields are appended at the end). Setting an empty list of values removes the field.

Parameters:
  • name (str) – the field name (matched case-insensitively, stored in the given casing)

  • value (str | int | float | bool | Iterable[str | int | float | bool]) – a single value or a list of values (one line each)

Return type:

None

update(headers=None)[source]

Merge the given headers into this instance: lines of fields that already exist are replaced (like dict.update, at the position of the field’s first line), other fields are appended. Accepts the same forms as the constructor (raw header block, mapping, iterable of tuples, Headers instance).

Parameters:

headers (Iterable[tuple[str, str | int | float | bool | Iterable[str | int | float | bool]]] | Mapping[str, str | int | float | bool | Iterable[str | int | float | bool]] | str | None, default: None) – the headers to merge in

Return type:

None

clear()[source]

Remove all fields, returns the removed lines (unlike MutableMapping.clear which returns None).

Return type:

list[tuple[str, str]]

Returns:

the removed lines as name/value tuples

copy()[source]
Return type:

Headers

Returns:

a new Headers instance with a copy of the lines

sort()[source]

Sort the lines in place by their (casefolded) field name. The sort is stable: lines of the same field keep their relative order — unlike sort(), values are never reordered because their order can be significant (e.g. Set-Cookie).

Return type:

None

as_str(separator='\\r\\n')[source]

The wire representation of the header lines, in order, with the original casing — secret values are NOT redacted here, unlike in repr().

Parameters:

separator (str, default: '\\r\\n') – the string joining the lines (no trailing one)

Return type:

str

Returns:

the header block, e.g. "Host: example.com\r\nAccept: */*"

as_lines()[source]
Return type:

list[tuple[str, str]]

Returns:

a copy of the representation: the header lines as name/value tuples, in order, with the original casing

as_dict()[source]
Return type:

dict[str, list[str]]

Returns:

the fields as a dictionary with the field names as keys (in the casing of each field’s first line) and the values as lists of strings

singles()[source]

For those who are really sure that each field has only one line and do not want to bother with the lists for the values, this method will return only the last value for each field.

WARNING: be aware, if the field has multiple lines, only one of those will be returned for the name!

Return type:

dict[str, str]

Returns:

a dictionary with a single value for each field name

__eq__(other)[source]

Headers are equal when they hold the same fields with the same values in the same per-field order; the order of different fields and the casing of the names don’t matter. Plain mappings are converted to Headers before comparing.

Parameters:

other (object) – the Headers instance or mapping to compare with

Return type:

bool

Returns:

whether the headers are equal

__repr__()[source]
Return type:

str

Returns:

the header lines in order, with the values of the secret_names fields redacted as ***. str() falls back to this representation, so only as_str() ever renders the secret values.

class action0.req.headers.Header(*values)[source]

The known HTTP header field names as string constants.

The members are the field names from the IANA HTTP field name registry — all permanent, deprecated and provisional registrations as of 2026-08-07 (obsoleted ones are left out) — plus a handful of widely used de-facto standard X-… names that were never registered.

Being a enum.StrEnum, every member is the header name string, so it can be used wherever a plain str is expected:

>>> from action0.req import Header
>>> print(Header.CONTENT_TYPE)
Content-Type
>>> Header.CONTENT_TYPE == "Content-Type"
True
>>> Header("ETag") is Header.ETAG
True

== on the enum is exact, case-sensitive string equality — case-insensitive header handling is the job of the header mapping, not of these constants.

ACCEPT = 'Accept'
ACCEPT_ADDITIONS = 'Accept-Additions'
ACCEPT_CH = 'Accept-CH'
ACCEPT_DATETIME = 'Accept-Datetime'
ACCEPT_ENCODING = 'Accept-Encoding'
ACCEPT_FEATURES = 'Accept-Features'
ACCEPT_LANGUAGE = 'Accept-Language'
ACCEPT_PATCH = 'Accept-Patch'
ACCEPT_POST = 'Accept-Post'
ACCEPT_QUERY = 'Accept-Query'
ACCEPT_RANGES = 'Accept-Ranges'
ACCEPT_SIGNATURE = 'Accept-Signature'
ACCESS_CONTROL_ALLOW_CREDENTIALS = 'Access-Control-Allow-Credentials'
ACCESS_CONTROL_ALLOW_HEADERS = 'Access-Control-Allow-Headers'
ACCESS_CONTROL_ALLOW_METHODS = 'Access-Control-Allow-Methods'
ACCESS_CONTROL_ALLOW_ORIGIN = 'Access-Control-Allow-Origin'
ACCESS_CONTROL_EXPOSE_HEADERS = 'Access-Control-Expose-Headers'
ACCESS_CONTROL_MAX_AGE = 'Access-Control-Max-Age'
ACCESS_CONTROL_REQUEST_HEADERS = 'Access-Control-Request-Headers'
ACCESS_CONTROL_REQUEST_METHOD = 'Access-Control-Request-Method'
AGE = 'Age'
ALLOW = 'Allow'
ALPN = 'ALPN'
ALTERNATES = 'Alternates'
ALT_SVC = 'Alt-Svc'
ALT_USED = 'Alt-Used'
APPLY_TO_REDIRECT_REF = 'Apply-To-Redirect-Ref'
AUTHENTICATION_CONTROL = 'Authentication-Control'
AUTHENTICATION_INFO = 'Authentication-Info'
AUTHORIZATION = 'Authorization'
AVAILABLE_DICTIONARY = 'Available-Dictionary'
A_IM = 'A-IM'
CACHE_CONTROL = 'Cache-Control'
CACHE_GROUPS = 'Cache-Groups'
CACHE_GROUP_INVALIDATION = 'Cache-Group-Invalidation'
CACHE_STATUS = 'Cache-Status'
CALDAV_TIMEZONES = 'CalDAV-Timezones'
CAL_MANAGED_ID = 'Cal-Managed-ID'
CAPSULE_PROTOCOL = 'Capsule-Protocol'
CDN_CACHE_CONTROL = 'CDN-Cache-Control'
CDN_LOOP = 'CDN-Loop'
CERT_NOT_AFTER = 'Cert-Not-After'
CERT_NOT_BEFORE = 'Cert-Not-Before'
CLEAR_SITE_DATA = 'Clear-Site-Data'
CLIENT_CERT = 'Client-Cert'
CLIENT_CERT_CHAIN = 'Client-Cert-Chain'
CLOSE = 'Close'
CONCEALED_AUTH_EXPORT = 'Concealed-Auth-Export'
CONNECTION = 'Connection'
CONTENT_DIGEST = 'Content-Digest'
CONTENT_DISPOSITION = 'Content-Disposition'
CONTENT_ENCODING = 'Content-Encoding'
CONTENT_LANGUAGE = 'Content-Language'
CONTENT_LENGTH = 'Content-Length'
CONTENT_LOCATION = 'Content-Location'
CONTENT_RANGE = 'Content-Range'
CONTENT_SECURITY_POLICY = 'Content-Security-Policy'
CONTENT_SECURITY_POLICY_REPORT_ONLY = 'Content-Security-Policy-Report-Only'
CONTENT_TYPE = 'Content-Type'
COOKIE = 'Cookie'
CROSS_ORIGIN_EMBEDDER_POLICY = 'Cross-Origin-Embedder-Policy'
CROSS_ORIGIN_EMBEDDER_POLICY_REPORT_ONLY = 'Cross-Origin-Embedder-Policy-Report-Only'
CROSS_ORIGIN_OPENER_POLICY = 'Cross-Origin-Opener-Policy'
CROSS_ORIGIN_OPENER_POLICY_REPORT_ONLY = 'Cross-Origin-Opener-Policy-Report-Only'
CROSS_ORIGIN_RESOURCE_POLICY = 'Cross-Origin-Resource-Policy'
DASL = 'DASL'
DATE = 'Date'
DAV = 'DAV'
DELTA_BASE = 'Delta-Base'
DEPRECATION = 'Deprecation'
DEPTH = 'Depth'
DESTINATION = 'Destination'
DETACHED_JWS = 'Detached-JWS'
DICTIONARY_ID = 'Dictionary-ID'
DPOP = 'DPoP'
DPOP_NONCE = 'DPoP-Nonce'
EARLY_DATA = 'Early-Data'
ETAG = 'ETag'
EXPECT = 'Expect'
EXPIRES = 'Expires'
FORWARDED = 'Forwarded'
FROM = 'From'
HOBAREG = 'Hobareg'
HOST = 'Host'
IF = 'If'
IF_MATCH = 'If-Match'
IF_MODIFIED_SINCE = 'If-Modified-Since'
IF_NONE_MATCH = 'If-None-Match'
IF_RANGE = 'If-Range'
IF_SCHEDULE_TAG_MATCH = 'If-Schedule-Tag-Match'
IF_UNMODIFIED_SINCE = 'If-Unmodified-Since'
IM = 'IM'
INCLUDE_REFERRED_TOKEN_BINDING_ID = 'Include-Referred-Token-Binding-ID'
INCREMENTAL = 'Incremental'
KEEP_ALIVE = 'Keep-Alive'
LABEL = 'Label'
LAST_EVENT_ID = 'Last-Event-ID'
LAST_MODIFIED = 'Last-Modified'
LOCATION = 'Location'
LOCK_TOKEN = 'Lock-Token'
MAX_FORWARDS = 'Max-Forwards'
MEMENTO_DATETIME = 'Memento-Datetime'
METER = 'Meter'
MIME_VERSION = 'MIME-Version'
NEGOTIATE = 'Negotiate'
NEL = 'NEL'
ODATA_ENTITYID = 'OData-EntityId'
ODATA_ISOLATION = 'OData-Isolation'
ODATA_MAXVERSION = 'OData-MaxVersion'
ODATA_VERSION = 'OData-Version'
OPTIONAL_WWW_AUTHENTICATE = 'Optional-WWW-Authenticate'
ORDERING_TYPE = 'Ordering-Type'
ORIGIN = 'Origin'
ORIGIN_AGENT_CLUSTER = 'Origin-Agent-Cluster'
OSCORE = 'OSCORE'
OSLC_CORE_VERSION = 'OSLC-Core-Version'
OVERWRITE = 'Overwrite'
PING_FROM = 'Ping-From'
PING_TO = 'Ping-To'
POSITION = 'Position'
PREFER = 'Prefer'
PREFERENCE_APPLIED = 'Preference-Applied'
PRIORITY = 'Priority'
PROXY_AUTHENTICATE = 'Proxy-Authenticate'
PROXY_AUTHENTICATION_INFO = 'Proxy-Authentication-Info'
PROXY_AUTHORIZATION = 'Proxy-Authorization'
PROXY_STATUS = 'Proxy-Status'
PUBLIC_KEY_PINS = 'Public-Key-Pins'
PUBLIC_KEY_PINS_REPORT_ONLY = 'Public-Key-Pins-Report-Only'
RANGE = 'Range'
REDIRECT_REF = 'Redirect-Ref'
REFERER = 'Referer'
REFERRER_POLICY = 'Referrer-Policy'
REFRESH = 'Refresh'
REPLAY_NONCE = 'Replay-Nonce'
REPR_DIGEST = 'Repr-Digest'
RETRY_AFTER = 'Retry-After'
SCHEDULE_REPLY = 'Schedule-Reply'
SCHEDULE_TAG = 'Schedule-Tag'
SEC_FETCH_DEST = 'Sec-Fetch-Dest'
SEC_FETCH_MODE = 'Sec-Fetch-Mode'
SEC_FETCH_SITE = 'Sec-Fetch-Site'
SEC_FETCH_USER = 'Sec-Fetch-User'
SEC_PURPOSE = 'Sec-Purpose'
SEC_TOKEN_BINDING = 'Sec-Token-Binding'
SEC_WEBSOCKET_ACCEPT = 'Sec-WebSocket-Accept'
SEC_WEBSOCKET_EXTENSIONS = 'Sec-WebSocket-Extensions'
SEC_WEBSOCKET_KEY = 'Sec-WebSocket-Key'
SEC_WEBSOCKET_PROTOCOL = 'Sec-WebSocket-Protocol'
SEC_WEBSOCKET_VERSION = 'Sec-WebSocket-Version'
SERVER = 'Server'
SERVER_TIMING = 'Server-Timing'
SET_TXN = 'Set-Txn'
SIGNATURE = 'Signature'
SIGNATURE_INPUT = 'Signature-Input'
SLUG = 'SLUG'
SOAPACTION = 'SoapAction'
STATUS_URI = 'Status-URI'
STRICT_TRANSPORT_SECURITY = 'Strict-Transport-Security'
SUNSET = 'Sunset'
TCN = 'TCN'
TE = 'TE'
TIMEOUT = 'Timeout'
TOPIC = 'Topic'
TRACEPARENT = 'Traceparent'
TRACESTATE = 'Tracestate'
TRAILER = 'Trailer'
TRANSFER_ENCODING = 'Transfer-Encoding'
TTL = 'TTL'
UNENCODED_DIGEST = 'Unencoded-Digest'
UPGRADE = 'Upgrade'
URGENCY = 'Urgency'
USER_AGENT = 'User-Agent'
USE_AS_DICTIONARY = 'Use-As-Dictionary'
VARIANT_VARY = 'Variant-Vary'
VARY = 'Vary'
VIA = 'Via'
WANT_CONTENT_DIGEST = 'Want-Content-Digest'
WANT_REPR_DIGEST = 'Want-Repr-Digest'
WANT_UNENCODED_DIGEST = 'Want-Unencoded-Digest'
WWW_AUTHENTICATE = 'WWW-Authenticate'
X_CONTENT_TYPE_OPTIONS = 'X-Content-Type-Options'
X_FRAME_OPTIONS = 'X-Frame-Options'
ACCEPT_CHARSET = 'Accept-Charset'
CONTENT_ID = 'Content-ID'
C_PEP_INFO = 'C-PEP-Info'
DIFFERENTIAL_ID = 'Differential-ID'
EXPECT_CT = 'Expect-CT'
PRAGMA = 'Pragma'
PROTOCOL_INFO = 'Protocol-Info'
PROTOCOL_QUERY = 'Protocol-Query'
ACTIVATE_STORAGE_ACCESS = 'Activate-Storage-Access'
AMP_CACHE_TRANSFORM = 'AMP-Cache-Transform'
CMCD_OBJECT = 'CMCD-Object'
CMCD_REQUEST = 'CMCD-Request'
CMCD_SESSION = 'CMCD-Session'
CMCD_STATUS = 'CMCD-Status'
CMSD_DYNAMIC = 'CMSD-Dynamic'
CMSD_STATIC = 'CMSD-Static'
CONFIGURATION_CONTEXT = 'Configuration-Context'
CTA_COMMON_ACCESS_TOKEN = 'CTA-Common-Access-Token'
EDIINT_FEATURES = 'EDIINT-Features'
ISOLATION = 'Isolation'
PERMISSIONS_POLICY = 'Permissions-Policy'
REPEATABILITY_CLIENT_ID = 'Repeatability-Client-ID'
REPEATABILITY_FIRST_SENT = 'Repeatability-First-Sent'
REPEATABILITY_REQUEST_ID = 'Repeatability-Request-ID'
REPEATABILITY_RESULT = 'Repeatability-Result'
REPORTING_ENDPOINTS = 'Reporting-Endpoints'
SEC_FETCH_STORAGE_ACCESS = 'Sec-Fetch-Storage-Access'
SEC_GPC = 'Sec-GPC'
SURROGATE_CAPABILITY = 'Surrogate-Capability'
SURROGATE_CONTROL = 'Surrogate-Control'
TIMING_ALLOW_ORIGIN = 'Timing-Allow-Origin'
X_API_KEY = 'X-API-Key'
X_CORRELATION_ID = 'X-Correlation-Id'
X_CSRF_TOKEN = 'X-CSRF-Token'
X_FORWARDED_FOR = 'X-Forwarded-For'
X_FORWARDED_HOST = 'X-Forwarded-Host'
X_FORWARDED_PROTO = 'X-Forwarded-Proto'
X_POWERED_BY = 'X-Powered-By'
X_REAL_IP = 'X-Real-IP'
X_REQUEST_ID = 'X-Request-Id'
X_ROBOTS_TAG = 'X-Robots-Tag'

Request and Method

The HTTP request representation (Request) and the method constants.

class action0.req.request.Method(*values)[source]

The HTTP request methods (“verbs”) as string constants.

The members are the methods defined by RFC 9110, plus PATCH (RFC 5789) and QUERY (RFC 10008). Being a enum.StrEnum, every member is the method string:

>>> from action0.req import Method
>>> print(Method.GET)
GET
>>> Method.GET == "GET"
True
>>> Method("POST") is Method.POST
True
CONNECT = 'CONNECT'
DELETE = 'DELETE'
GET = 'GET'
HEAD = 'HEAD'
OPTIONS = 'OPTIONS'
PATCH = 'PATCH'
POST = 'POST'
PUT = 'PUT'
QUERY = 'QUERY'
TRACE = 'TRACE'
class action0.req.request.Request(url=None, method=Method.GET, *, query=None, headers=None, body=None, http_version='HTTP/1.1', meta=None)[source]

Python representation of an HTTP request: method, URL, headers, body and HTTP version — every part a plain mutable attribute (Url for the URL, Headers for the headers).

Example:

>>> req = Request("https://api.example.com/items", query={"page": 2})
>>> req.method
'GET'
>>> req.url.as_str()
'https://api.example.com/items?page=2'
>>> req.headers["Accept"] = "application/json"
>>> print(req.as_str(separator="\n"))
GET /items?page=2 HTTP/1.1
Host: api.example.com
Accept: application/json
>>> req
Request(GET https://api.example.com/items?page=2)

The body can be set as bytes, str or a streaming BodyProducer and retrieved in any of the three forms via body_bytes(), body_str() and body_producer(), regardless of how it was set.

Parameters:
method: str
body: bytes | str | BodyProducer | None
meta: dict[str, Any]

Application metadata riding along with the request — correlation ids, tracing context, per-request knobs for custom backends, … — never sent on the wire and not part of __eq__(). The dict is the request’s own (the constructor copies the given mapping); libraries should namespace their keys (e.g. "my-lib.correlation-id").

body_bytes()[source]

The body as bytes, regardless of how it was set: bytes are returned as-is, a str is encoded with the Content-Type charset (default utf-8), a BodyProducer is read in full.

Return type:

bytes | None

Returns:

the body bytes, None if there is no body

body_str()[source]

The body as text, regardless of how it was set: a str is returned as-is, bytes are decoded with the Content-Type charset (default utf-8), a BodyProducer is read in full and decoded.

Return type:

str | None

Returns:

the body text, None if there is no body

body_producer()[source]

The body as a streaming producer, regardless of how it was set: a BodyProducer is returned as-is, bytes and str are wrapped in an in-memory BytesBody (str encoded with the Content-Type charset first).

Return type:

BodyProducer | None

Returns:

the body producer, None if there is no body

copy(**overrides)[source]

An independent copy of this request (with its own Url, Headers and meta dict), optionally with attributes replaced. The body is carried over as-is — in particular a BodyProducer is shared, not copied; the meta values are shared too (shallow copy).

Example:

>>> req = Request("https://api.example.com/items")
>>> req.copy(method="POST", body="{}")
Request(POST https://api.example.com/items)
Parameters:

overrides (Any) – any attribute accepted by the constructor

Return type:

Request

Returns:

a new Request, this instance is not modified

as_str(include_body=False, separator='\\r\\n')[source]

The wire representation: the request line with the origin-form target (path and query), the header lines — with a Host header derived from the URL if none is set — and optionally the body. Secret header values are NOT redacted here, unlike in repr().

Parameters:
  • include_body (bool, default: False) – append a blank line and the body (as text, via body_str()); a BodyProducer is not consumed — a placeholder is shown instead

  • separator (str, default: '\\r\\n') – the string joining the lines (no trailing one)

Return type:

str

Returns:

e.g. "GET /items?page=2 HTTP/1.1\r\nHost: api.example.com"

__eq__(other)[source]

Requests are equal when their method, URL, headers, body and HTTP version are equal, each with the part’s own equality semantics (e.g. header name casing doesn’t matter). The body is compared as set: b"x" and "x" are different bodies. The meta dict is metadata and not compared.

Parameters:

other (object) – the Request to compare with

Return type:

bool

Returns:

whether the requests are equal

__repr__()[source]
Return type:

str

Returns:

the method and the URL, with the password redacted like repr(Url) does; headers and body are left out, so no secret header values can leak. str() falls back to this representation — the wire rendering is only available explicitly via as_str().

Response

The HTTP response representation (Response).

class action0.req.response.Response(status=Status.OK, *, headers=None, body=None, reason=None, http_version='HTTP/1.1', request=None, meta=None)[source]

Python representation of an HTTP response: status, headers, body and HTTP version — every part a plain mutable attribute (the headers a Headers).

Example:

>>> resp = Response(404, headers={"Content-Type": "text/plain"}, body="not here")
>>> resp.status
404
>>> resp.phrase
'Not Found'
>>> resp.is_client_error
True
>>> print(resp.as_str(include_body=True, separator="\n"))
HTTP/1.1 404 Not Found
Content-Type: text/plain

not here
>>> resp
Response(404 Not Found)

The body works exactly like the request body: set as bytes, str or a streaming BodyProducer, retrieved in any of the three forms via body_bytes(), body_str() and body_producer().

Parameters:
  • status (int, default: <Status.OK: 200>)

  • headers (Iterable[tuple[str, str | int | float | bool | Iterable[str | int | float | bool]]] | Mapping[str, str | int | float | bool | Iterable[str | int | float | bool]] | str | None, default: None)

  • body (bytes | str | BodyProducer | None, default: None)

  • reason (str | None, default: None)

  • http_version (str, default: 'HTTP/1.1')

  • request (Request | None, default: None)

  • meta (Mapping[str, Any] | None, default: None)

  • status – the status code, a Status member or any int — unregistered codes are fine

  • headers – the initial header lines, in any form accepted by Headers

  • body – the response body as bytes, str or a streaming BodyProducer

  • reason – the reason phrase as sent by the server; None falls back to the registry phrase of the status code (see phrase)

  • http_version – the protocol version rendered in the status line

  • request – the request that produced this response — metadata: shared, not copied, and not part of equality

  • meta – initial application metadata riding along with the response (see meta)

meta: dict[str, Any]

Application metadata riding along with the response — e.g. a backend’s native response object, cache markers, timings — never sent on the wire and not part of __eq__(). The dict is the response’s own (the constructor copies the given mapping); libraries should namespace their keys.

property phrase: str

The reason phrase: the explicitly set reason if there is one, otherwise the registry phrase of the status code, otherwise (for unregistered codes) an empty string.

property is_informational: bool

True for 1xx statuses (works for any int status).

property is_success: bool

True for 2xx statuses (works for any int status).

property is_redirection: bool

True for 3xx statuses (works for any int status).

property is_client_error: bool

True for 4xx statuses (works for any int status).

property is_server_error: bool

True for 5xx statuses (works for any int status).

body_bytes()[source]

The body as bytes, regardless of how it was set: bytes are returned as-is, a str is encoded with the Content-Type charset (default utf-8), a BodyProducer is read in full.

Return type:

bytes | None

Returns:

the body bytes, None if there is no body

body_str()[source]

The body as text, regardless of how it was set: a str is returned as-is, bytes are decoded with the Content-Type charset (default utf-8), a BodyProducer is read in full and decoded.

Return type:

str | None

Returns:

the body text, None if there is no body

body_producer()[source]

The body as a streaming producer, regardless of how it was set: a BodyProducer is returned as-is, bytes and str are wrapped in an in-memory BytesBody (str encoded with the Content-Type charset first).

Return type:

BodyProducer | None

Returns:

the body producer, None if there is no body

copy(**overrides)[source]

An independent copy of this response (with its own Headers instance and meta dict), optionally with attributes replaced. The body and the request reference are carried over as-is — in particular a BodyProducer is shared, not copied; the meta values are shared too (shallow copy).

Example:

>>> resp = Response(200, body="ok")
>>> resp.copy(status=404, reason="Nope")
Response(404 Nope)
Parameters:

overrides (Any) – any attribute accepted by the constructor

Return type:

Response

Returns:

a new Response, this instance is not modified

as_str(include_body=False, separator='\\r\\n')[source]

The wire representation: the status line (with the phrase), the header lines and optionally the body. Secret header values are NOT redacted here, unlike in repr().

Parameters:
  • include_body (bool, default: False) – append a blank line and the body (as text, via body_str()); a BodyProducer is not consumed — a placeholder is shown instead

  • separator (str, default: '\\r\\n') – the string joining the lines (no trailing one)

Return type:

str

Returns:

e.g. "HTTP/1.1 404 Not Found\r\nContent-Length: 0"

__eq__(other)[source]

Responses are equal when their status, reason, headers, body and HTTP version are equal, each with the part’s own equality semantics (e.g. header name casing doesn’t matter). The body is compared as set: b"x" and "x" are different bodies. The request reference and the meta dict are metadata and not compared.

Parameters:

other (object) – the Response to compare with

Return type:

bool

Returns:

whether the responses are equal

__repr__()[source]
Return type:

str

Returns:

the status code and the phrase; headers and body are left out, so no secret header values can leak. str() falls back to this representation — the wire rendering is only available explicitly via as_str().

Body producers

Streaming abstractions for request and response bodies.

class action0.req.body.BodyProducer(*args, **kwargs)[source]

The interface for streaming the bytes of a request or response body.

Implementations produce the body as a sequence of chunks — synchronously via chunks() or asynchronously via achunks() — and as a whole via as_bytes(). BytesBody is the simplest implementation; file- and iterable-backed producers are planned.

The protocol is runtime_checkable, so isinstance(obj, BodyProducer) checks that the four methods exist.

content_length()[source]
Return type:

int | None

Returns:

the total number of body bytes, or None if not known in advance (such a body would be sent chunked)

chunks()[source]
Return type:

Iterator[bytes]

Returns:

the body as an iterator of byte chunks

achunks()[source]
Return type:

AsyncIterator[bytes]

Returns:

the body as an asynchronous iterator of byte chunks

as_bytes()[source]
Return type:

bytes

Returns:

the whole body as a single bytes object

class action0.req.body.BytesBody(data)[source]

The simplest BodyProducer: an in-memory bytes value, produced as a single chunk.

Example:

>>> body = BytesBody(b"hello")
>>> body.content_length()
5
>>> list(body.chunks())
[b'hello']
>>> body.as_bytes()
b'hello'
Parameters:
  • data (bytes)

  • data – the body bytes

content_length()[source]
Return type:

int | None

Returns:

the number of body bytes

chunks()[source]
Return type:

Iterator[bytes]

Returns:

the body as an iterator with a single chunk

async achunks()[source]
Return type:

AsyncIterator[bytes]

Returns:

the body as an asynchronous iterator with a single chunk

as_bytes()[source]
Return type:

bytes

Returns:

the body bytes

class action0.req.body.FileBody(source, chunk_size=65536)[source]

A BodyProducer streaming a file in chunks.

The source can be a path or an already-open binary file object:

  • A path is opened freshly for every iteration, so the body is re-iterable (e.g. for retries) and no file descriptor is held between uses.

  • A file object is used as-is and never closed by this class. If it is seekable it is rewound to the start for every iteration (making it re-iterable too); if not, reading starts at the current position and the body is consumable only once.

achunks() performs every blocking file operation in the default thread pool via asyncio.to_thread(), so the event loop is never stalled — without any extra dependency.

Parameters:
  • source (str | PathLike[str] | IO[bytes])

  • chunk_size (int, default: 65536)

  • source – the path of the file to stream, or an open binary file object

  • chunk_size – the number of bytes per chunk

Raises:

ValueError – if the chunk size is not positive

content_length()[source]
Return type:

int | None

Returns:

the size of the file; for a non-seekable file object None (unknown)

chunks()[source]
Return type:

Iterator[bytes]

Returns:

the file contents as an iterator of chunks of (up to) the configured chunk size

async achunks()[source]
Return type:

AsyncIterator[bytes]

Returns:

the file contents as an asynchronous iterator of chunks; all file operations run in the default thread pool

as_bytes()[source]
Return type:

bytes

Returns:

the whole file contents

class action0.req.body.IterableBody(iterable)[source]

A BodyProducer wrapping an iterable of byte chunks, e.g. a generator. The total length is unknown (content_length() is None — such a body would be sent chunked).

WARNING: if the iterable is a generator (or any other single-use iterable), the body can be consumed only once — also by as_bytes(). Pass a list of chunks for a re-iterable body.

Parameters:
  • iterable (Iterable[bytes])

  • iterable – the byte chunks of the body

content_length()[source]
Return type:

int | None

Returns:

always None, the length is unknown in advance

chunks()[source]
Return type:

Iterator[bytes]

Returns:

the chunks as given by the wrapped iterable

async achunks()[source]
Return type:

AsyncIterator[bytes]

Returns:

the chunks as given by the wrapped (synchronous) iterable

as_bytes()[source]
Return type:

bytes

Returns:

all chunks joined (consumes a single-use iterable)

class action0.req.body.AsyncIterableBody(aiterable)[source]

A BodyProducer wrapping an asynchronous iterable of byte chunks, e.g. an async generator proxying another stream. The total length is unknown (content_length() is None).

An asynchronous source is async-only: the synchronous accessors chunks() and as_bytes() raise a RuntimeError — consume the body with achunks(). Like a generator, an async generator source is consumable only once.

Parameters:
content_length()[source]
Return type:

int | None

Returns:

always None, the length is unknown in advance

chunks()[source]
Raises:

RuntimeError – always — an async body has no synchronous chunks; use achunks()

Return type:

Iterator[bytes]

async achunks()[source]
Return type:

AsyncIterator[bytes]

Returns:

the chunks as given by the wrapped asynchronous iterable

as_bytes()[source]
Raises:

RuntimeError – always — an async body has no synchronous bytes view; use achunks()

Return type:

bytes

action0.req.body.BodyTypes = bytes | str | action0.req.body.BodyProducer

Everything a request or response accepts as body: raw bytes, text (encoded with the Content-Type charset when accessed as bytes), or a streaming BodyProducer.

Status

Constants for the known HTTP status codes.

class action0.req.status.Status(*values)[source]

The known HTTP status codes as integer constants.

The members are the codes from the IANA HTTP status code registry as of 2026-08-07 with their registered reason phrases, plus the well-known 418 I'm a Teapot from RFC 2324. Being an enum.IntEnum, every member is its numeric code:

>>> from action0.req import Status
>>> print(Status.NOT_FOUND)
404
>>> Status.NOT_FOUND == 404
True
>>> Status.NOT_FOUND.phrase
'Not Found'
>>> Status(503).phrase
'Service Unavailable'
>>> Status.NOT_FOUND.is_client_error
True

RFC 9110 renamed two reason phrases; the pre-9110 names remain available as aliases:

>>> Status.PAYLOAD_TOO_LARGE is Status.CONTENT_TOO_LARGE
True
>>> Status.UNPROCESSABLE_ENTITY is Status.UNPROCESSABLE_CONTENT
True
phrase: str

The registered reason phrase, e.g. "Not Found" for 404.

property is_informational: bool

True for the 1xx codes.

property is_success: bool

True for the 2xx codes.

property is_redirection: bool

True for the 3xx codes.

property is_client_error: bool

True for the 4xx codes.

property is_server_error: bool

True for the 5xx codes.

CONTINUE = 100
SWITCHING_PROTOCOLS = 101
PROCESSING = 102
EARLY_HINTS = 103
OK = 200
CREATED = 201
ACCEPTED = 202
NON_AUTHORITATIVE_INFORMATION = 203
NO_CONTENT = 204
RESET_CONTENT = 205
PARTIAL_CONTENT = 206
MULTI_STATUS = 207
ALREADY_REPORTED = 208
IM_USED = 226
MULTIPLE_CHOICES = 300
MOVED_PERMANENTLY = 301
FOUND = 302
SEE_OTHER = 303
NOT_MODIFIED = 304
USE_PROXY = 305
TEMPORARY_REDIRECT = 307
PERMANENT_REDIRECT = 308
BAD_REQUEST = 400
UNAUTHORIZED = 401
PAYMENT_REQUIRED = 402
FORBIDDEN = 403
NOT_FOUND = 404
METHOD_NOT_ALLOWED = 405
NOT_ACCEPTABLE = 406
PROXY_AUTHENTICATION_REQUIRED = 407
REQUEST_TIMEOUT = 408
CONFLICT = 409
GONE = 410
LENGTH_REQUIRED = 411
PRECONDITION_FAILED = 412
CONTENT_TOO_LARGE = 413
URI_TOO_LONG = 414
UNSUPPORTED_MEDIA_TYPE = 415
RANGE_NOT_SATISFIABLE = 416
EXPECTATION_FAILED = 417
IM_A_TEAPOT = 418
MISDIRECTED_REQUEST = 421
UNPROCESSABLE_CONTENT = 422
LOCKED = 423
FAILED_DEPENDENCY = 424
TOO_EARLY = 425
UPGRADE_REQUIRED = 426
PRECONDITION_REQUIRED = 428
TOO_MANY_REQUESTS = 429
REQUEST_HEADER_FIELDS_TOO_LARGE = 431
INTERNAL_SERVER_ERROR = 500
NOT_IMPLEMENTED = 501
BAD_GATEWAY = 502
SERVICE_UNAVAILABLE = 503
GATEWAY_TIMEOUT = 504
HTTP_VERSION_NOT_SUPPORTED = 505
VARIANT_ALSO_NEGOTIATES = 506
INSUFFICIENT_STORAGE = 507
LOOP_DETECTED = 508
NOT_EXTENDED = 510
NETWORK_AUTHENTICATION_REQUIRED = 511
PAYLOAD_TOO_LARGE = 413
UNPROCESSABLE_ENTITY = 422