Configuring request
If you want to enrich your HTTP request with additional arguments such as headers or query parameters, you can specify them in the HTTP request as dictionaries in headers and query fields.
Additionally, Arrest offers custom field types Header, Query and Body, which are inherited from pydantic's FieldInfo, that you can use in defining your pydantic request model.
Header¶
You can use the headers keyword-argument in the request method to directly pass a set of key-value pairs as headers.
using headers kwarg
If you want resource-wide shared header definition, you can set it via the config
argument. You can use both of these together as all the headers will be collected
and sent as a whole.
using config.headers
If you want to define your headers as part of your request model, use arrest.params.Header to specify the header fields in your request.
This is useful when you want to group together all the components of your request inside one data model.
using Header class
Warning
Arrest does NOT convert non-str values to str or convert snake_case to kebab-case before sending the fields as headers in the request.
If you want to send kebab-case headers you need to:
-
specify them as
kebab-casein the dictionary passed to theheaderskeyword or Resource class definition. -
Use
aliasin your pydantic field (if you are using pydantic@v2 then you need to useserialization_aliasinstead). -
Set the pydantic config to
allow_population_by_field_name(if pydantic@v2, useConfigDict.populate_by_name).
Query¶
Similar to headers, you can provide your request-specific query parameters as a dict to the query kwarg in the request method.
If you want to define your query parameters as part of your request model, use arrest.params.Query to specify the query fields in your request.
Whatever field is marked as Query will be attached as a query parameter in the request.
using Query class
Body¶
Request body is supplied with the keyword argument request in your call.
This can be a pydantic instance, a simple dictionary, a list or any object that can be jsonified.
However, if the handler for the path has a request type specified, the request body must match the type (or be convertible to it).
You can make use of arrest.params.Body when defining the body fields, although fields that don't have any defaults will be automatically parsed as body.
using request type
from arrest.params import Body
class BodyRequest(BaseModel):
name: str = Body(...)
email: str
password: str
role: Optional[str]
is_active: bool
# both of the following should work
await service.user.post("/", request=BodyRequest(name="abc", email="abc@email.com", password="123", role="ADMIN", is_active=False))
await service.user.post("/", request={"name": "abc", "email": "abc@email.com", "password": "123", "role": "ADMIN", "is_active": False})
If you do not have a request type specified to the handler, you can still pass a pydantic object but no model validation will take place and Arrest will extract the fields as per their defaults. You can also pass a plain dictionary or a list as request. They will be passed as a JSON payload.
regarding json payloads
Arrest uses orjson for serializing the request payload. This was chosen because the stdlib json does not parse datetime which orjson does.
Form¶
Use arrest.params.Form to send fields as application/x-www-form-urlencoded
instead of JSON. This is useful for traditional login forms, OAuth2 token
requests, and other form-encoded endpoints.
using Form class
from pydantic import BaseModel
from arrest.params import Form
class LoginRequest(BaseModel):
grant_type: str = Form(...)
username: str = Form(...)
password: str = Form(...)
scope: str = Form(default="read")
await service.auth.post("/token", request=LoginRequest(
grant_type="password",
username="alice",
password="s3cret",
))
# Content-Type: application/x-www-form-urlencoded
# Body: grant_type=password&username=alice&password=s3cret&scope=read
Cannot mix JSON and Form
A single request model cannot mix Body() (or unannotated fields) with
Form() / File() fields. Arrest will raise a ValueError:
class BadRequest(BaseModel):
name: str # unannotated → JSON body
token: str = Form(...) # Form field ❌ ValueError!
Choose one encoding per request model.
File¶
Use arrest.params.File along with arrest.types.UploadFile to send
multipart/form-data requests with file uploads.
uploading a file
from pydantic import BaseModel
from arrest.params import File, Form
from arrest.types import UploadFile
class ProfilePicture(BaseModel):
user_id: str = Form(...)
avatar: UploadFile = File(...)
# Upload from disk
with open("avatar.png", "rb") as f:
avatar = UploadFile(
filename="avatar.png",
content_type="image/png",
file=f,
)
await service.user.post(
"/avatar",
request=ProfilePicture(user_id="123", avatar=avatar),
)
uploading raw bytes
uploading a file path as string
The UploadFile type validates any file-like object with a read() method,
or raw bytes. When sent, Arrest constructs a proper multipart/form-data
body with the filename and content type.
UploadFile fields
| Field | Type | Description |
|---|---|---|
filename |
str \| None |
Name of the file |
content_type |
str |
MIME type (default: application/octet-stream) |
file |
IO[bytes] \| None |
File-like object with read() |
XML¶
Arrest supports XML request and response bodies via pydantic-xml's BaseXmlModel.
When your request type is a BaseXmlModel subclass, Arrest serializes it to XML
and sets Content-Type: application/xml. When your response type is a BaseXmlModel
subclass, Arrest parses the raw XML response into a model instance.
XML request and response
from pydantic_xml import BaseXmlModel, attr, element
from arrest import Resource
class UserRequest(BaseXmlModel, tag="user"):
name: str = element()
email: str = element()
class UserResponse(BaseXmlModel, tag="user"):
id: int = attr()
name: str = element()
email: str = element()
user = Resource(
route="/users",
handlers=[
("POST", "/xml", UserRequest, UserResponse),
],
)
# Request is serialized to XML with Content-Type: application/xml
resp = await svc.users.post(
"/xml",
request=UserRequest(name="Alice", email="alice@example.com"),
)
# resp.data is a UserResponse instance parsed from the XML response
Field annotations required
pydantic-xml requires explicit annotations on fields: use element() for
child elements, attr() for attributes. Plain str or int annotations
without these may not serialize correctly.
Additional Configuration¶
Arrest also allows providing other http parameters such as cookies, auth, transport, etc, or even your own instance of httpx.AsyncClient (or other classes subclassing it), if you choose to do so.
If you want to customize the httpx client and specify more parameters either at resource-level or at service-level, you can check out Resources & Services.
Path parameters¶
Path parameters are a bit tricky as they are not set as pydantic fields. To define a handler that takes a path parameter, you have to specify the path-params inside curlys with (optional) their types.
Resource(
route="/abc",
handlers=[
("GET", "/user/{user_id}"),
("GET", "/user/{user_id:uuid}"),
("GET", "/user/{user_id}/comments/{comment_id:int}")
]
)
Regarding multiple handlers
If you specify handlers with the same path parameter but different type hints, the most recent one will override all the others. So in the above example, we only have one handler GET /user/{user_id:uuid}
This is because we keep track of unique handlers with the pair <method, route> where route is the handler route with its type hint removed.
Subsequent handler definitions with varying path parameter types will thus override the entry.
There are many ways you can supply the path param. The most common way is to use a python f-string (considering the path-param is dynamic)
...
user_id = uuid.UUID("ca80a889-8811-4e65-86bb-5e7c0c6e07cf")
...
service.abc.get(f"/user/{user_id}")
Alternatively, you can pass it as a static string.
if f-strings are not cool enough for you, there is another alternative, albeit experimental, where you can pass the path-parameter(s) as kwargs in your request function.
service.abc.get("/user", user_id=user_id)
# or
service.abc.get(f"/user/{user_id}/comments", comment_id=comment_id)
Note
If the resource contains only one handler and that handler url contains multiple path params like this:
Resource(
route="/user",
handlers=[
(Methods.POST, "/profile/{id:int}/comments/{comment_id:int}"),
],
)
then you can pass all the path_params as kwargs in the request by specifying the url as /user
user.post("/profile", id=123, comment_id=456)
user.post("/profile/123", comment_id=456)
user.post("/profile/123/comments", comment_id=456)
user.post("/profile/123/comments/456") # all 4 would work
However if it contains specific paths from /profile, then you need to specify the sub-resource name of the specific profile
About url paths
If the endpoint you are trying to call ends with a trailing slash (/), you need to specify the relative path to the handler also with the trailing slash (/).
are considered two different handlers. This does not apply when you are passing kwargs as path-parameters and Arrest will construct and find a match for the full path.Using converters¶
Arrest uses converters for the following types to validate and stringify the passed kwarg path-params to construct the url.
- int
- float
- str
- UUID
If you want to run with a custom datatype as path-param, you can add a converter and regex for it by subclassing arrest.converters.Converter and add the converter to Arrest by add_converter(...)