mirror of https://github.com/zulip/zulip.git
tutorial documentation: Add documentation for typed_endpoint.
This commit is contained in:
parent
748a56aae9
commit
f27cee21e3
|
@ -254,13 +254,13 @@ above.
|
||||||
question works by reading the code! To understand how arguments
|
question works by reading the code! To understand how arguments
|
||||||
are specified in Zulip backend endpoints, read our [REST API
|
are specified in Zulip backend endpoints, read our [REST API
|
||||||
tutorial][rest-api-tutorial], paying special attention to the
|
tutorial][rest-api-tutorial], paying special attention to the
|
||||||
details of `REQ` and `has_request_variables`.
|
details of `typed_endpoint`.
|
||||||
|
|
||||||
Once you understand that, the best way to determine the supported
|
Once you understand that, the best way to determine the supported
|
||||||
arguments for an API endpoint is to find the corresponding URL
|
arguments for an API endpoint is to find the corresponding URL
|
||||||
pattern in `zprojects/urls.py`, look up the backend function for
|
pattern in `zprojects/urls.py`, look up the backend function for
|
||||||
that endpoint in `zerver/views/`, and inspect its arguments
|
that endpoint in `zerver/views/`, and inspect its keyword-only
|
||||||
declared using `REQ`.
|
arguments.
|
||||||
|
|
||||||
You can check your formatting using these helpful tools.
|
You can check your formatting using these helpful tools.
|
||||||
|
|
||||||
|
|
|
@ -433,12 +433,14 @@ annotation).
|
||||||
```diff
|
```diff
|
||||||
# zerver/views/realm.py
|
# zerver/views/realm.py
|
||||||
|
|
||||||
|
@typed_endpoint
|
||||||
def update_realm(
|
def update_realm(
|
||||||
request: HttpRequest,
|
request: HttpRequest,
|
||||||
user_profile: UserProfile,
|
user_profile: UserProfile,
|
||||||
name: Optional[str] = REQ(str_validator=check_string, default=None),
|
*,
|
||||||
|
name: str | None,
|
||||||
# ...
|
# ...
|
||||||
+ mandatory_topics: Optional[bool] = REQ(json_validator=check_bool, default=None),
|
+ mandatory_topics: Json[bool] | None = None,
|
||||||
# ...
|
# ...
|
||||||
):
|
):
|
||||||
# ...
|
# ...
|
||||||
|
|
|
@ -136,9 +136,11 @@ one of several bad outcomes:
|
||||||
validation that has the problems from the last bullet point.
|
validation that has the problems from the last bullet point.
|
||||||
|
|
||||||
In Zulip, we solve this problem with a special decorator called
|
In Zulip, we solve this problem with a special decorator called
|
||||||
`has_request_variables` which allows a developer to declare the
|
`typed_endpoint` which allows a developer to declare the
|
||||||
arguments a view function takes and validate their types all within
|
arguments a view function takes and validate their types all within
|
||||||
the `def` line of the function. We like this framework because we
|
the `def` line of the function. This framework uses
|
||||||
|
[Pydantic V2](https://docs.pydantic.dev/dev/) to perform data validation
|
||||||
|
and parsing for the view arguments. We like this framework because we
|
||||||
have found it makes the validation code compact, readable, and
|
have found it makes the validation code compact, readable, and
|
||||||
conveniently located in the same place as the method it is validating
|
conveniently located in the same place as the method it is validating
|
||||||
arguments for.
|
arguments for.
|
||||||
|
@ -147,20 +149,26 @@ Here's an example:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from zerver.decorator import require_realm_admin
|
from zerver.decorator import require_realm_admin
|
||||||
from zerver.lib.request import has_request_variables, REQ
|
from zerver.lib.typed_endpoint import typed_endpoint
|
||||||
|
|
||||||
@require_realm_admin
|
@require_realm_admin
|
||||||
@has_request_variables
|
@typed_endpoint
|
||||||
def create_user_backend(request, user_profile, email=REQ(), password=REQ(),
|
def create_user_backend(
|
||||||
full_name=REQ()):
|
request: HttpRequest,
|
||||||
|
user_profile: UserProfile,
|
||||||
|
*,
|
||||||
|
email: str,
|
||||||
|
password: str,
|
||||||
|
full_name: str,
|
||||||
|
):
|
||||||
# ... code here
|
# ... code here
|
||||||
```
|
```
|
||||||
|
|
||||||
You will notice the special `REQ()` in the keyword arguments to
|
The `typed_endpoint` decorator parses the declared
|
||||||
`create_user_backend`. `has_request_variables` parses the declared
|
[keyword-only arguments](https://docs.python.org/3/glossary.html#term-parameter)
|
||||||
keyword arguments of the decorated function, and for each that has an
|
of the decorated function, and for each argument that has been declared,
|
||||||
instance of `REQ` as the default value, it extracts the HTTP parameter
|
it extracts the HTTP parameter with that name from the request,
|
||||||
with that name from the request, parses it as JSON, and passes it to
|
parses it according to the type annotation, and then passes it to
|
||||||
the function. It will return an nicely JSON formatted HTTP 400 error
|
the function. It will return an nicely JSON formatted HTTP 400 error
|
||||||
in the event that an argument is missing, doesn't parse as JSON, or
|
in the event that an argument is missing, doesn't parse as JSON, or
|
||||||
otherwise is invalid.
|
otherwise is invalid.
|
||||||
|
@ -168,37 +176,85 @@ otherwise is invalid.
|
||||||
`require_realm_admin` is another decorator which checks the
|
`require_realm_admin` is another decorator which checks the
|
||||||
authorization of the given `user_profile` to make sure it belongs to a
|
authorization of the given `user_profile` to make sure it belongs to a
|
||||||
realm administrator (and thus has permission to create a user); we
|
realm administrator (and thus has permission to create a user); we
|
||||||
show it here primarily to show how `has_request_variables` should be
|
show it here primarily to show how `typed_endpoint` should be
|
||||||
the inner decorator.
|
the inner decorator.
|
||||||
|
|
||||||
The implementation of `has_request_variables` is documented in detail
|
The implementation of `typed_endpoint` is documented in detail
|
||||||
in
|
in
|
||||||
[zerver/lib/request.py](https://github.com/zulip/zulip/blob/main/zerver/lib/request.py))
|
[zerver/lib/typed_endpoint.py](https://github.com/zulip/zulip/blob/main/zerver/lib/typed_endpoint.py)
|
||||||
|
|
||||||
REQ also helps us with request variable validation. For example:
|
Pydantic also helps us with request variable validation. For example:
|
||||||
|
|
||||||
- `msg_ids = REQ(json_validator=check_list(check_int))` will check
|
- `msg_ids: Json[list[int]]` will check that the `msg_ids`
|
||||||
that the `msg_ids` HTTP parameter is a list of integers, marshalled
|
HTTP parameter is a list of integers, marshalled as JSON,
|
||||||
as JSON, and pass it into the function as the `msg_ids` Python
|
and pass it into the function as the `msg_ids` Python
|
||||||
keyword argument.
|
keyword argument.
|
||||||
|
|
||||||
- `streams_raw = REQ("subscriptions", json_validator=check_list(check_string))`
|
- `streams_raw: Annotated[Json[list[str]], ApiParamConfig("subscriptions")]`
|
||||||
will check that the "subscriptions" HTTP parameter is a list of
|
will check that the "subscriptions" HTTP parameter is a list of
|
||||||
strings, marshalled as JSON, and pass it into the function with the
|
strings, marshalled as JSON, and pass it into the function with the
|
||||||
Python keyword argument `streams_raw`.
|
Python keyword argument `streams_raw`.
|
||||||
|
|
||||||
- `message_id=REQ(converter=to_non_negative_int)` will check that the
|
- `message_id: Json[NonNegativeInt]` will check that the `message_id`
|
||||||
`message_id` HTTP parameter is a string containing a non-negative
|
HTTP parameter is a string containing a JSON encoded non-negative
|
||||||
integer (`converter` differs from `json_validator` in that it does
|
integer.
|
||||||
not automatically marshall the input from JSON).
|
|
||||||
|
|
||||||
- Since there is no need to JSON-encode strings, usually simply
|
[Annotated](https://docs.python.org/3/library/typing.html#typing.Annotated)
|
||||||
`my_string=REQ()` is correct. One can pass, for example,
|
can be used in combination with
|
||||||
`str_validator=check_string_in(...)` where one wants to run a
|
[Pydantic's validators](https://docs.pydantic.dev/latest/api/functional_validators/)
|
||||||
|
to provide additional validation for the arguments.
|
||||||
|
|
||||||
|
- `name: Annotated[str, StringConstraints(max_length=60)]` will check that the
|
||||||
|
`name` HTTP parameter is a string containing up to 60 characters.
|
||||||
|
|
||||||
|
- Since there is no need to JSON-encode strings
|
||||||
|
(lists, integers, bools and complex objects require JSON encoding), usually simply
|
||||||
|
`my_string: str` is correct. One can pass, for example,
|
||||||
|
`Annotated[str, check_string_in_validator(...)]` where one wants to run a
|
||||||
validator on the value of a string.
|
validator on the value of a string.
|
||||||
|
|
||||||
|
Default values can be specified for optional arguments similar to how we would specify
|
||||||
|
default values in regular python function.
|
||||||
|
|
||||||
|
- `is_default_stream: Json[bool] = False` will assign False to the `is_default_stream` argument
|
||||||
|
if no value is specified when making a request to the endpoint.
|
||||||
|
|
||||||
|
- We can use `None` as the default value for optional arguments when we don't
|
||||||
|
want to specify any specific default value, for example,
|
||||||
|
`narrow: Json[list[NarrowParameter]] | None = None`. This does not allow the
|
||||||
|
caller to pass `None` as the value, the only way `narrow` can be set to `None` is
|
||||||
|
by using the default value.
|
||||||
|
|
||||||
|
[Pydantic models](https://docs.pydantic.dev/latest/concepts/models/) can be used to
|
||||||
|
define the schema of complex objects that can be passed to the endpoint.
|
||||||
|
|
||||||
|
Here's an example:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from pydantic import BaseModel, StringConstraints, model_validator
|
||||||
|
|
||||||
|
class AddSubscriptionData(BaseModel):
|
||||||
|
name: str
|
||||||
|
color: str | None = None
|
||||||
|
description: (
|
||||||
|
Annotated[str, StringConstraints(max_length=Stream.MAX_DESCRIPTION_LENGTH)] | None
|
||||||
|
) = None
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def validate_terms(self) -> "AddSubscriptionData":
|
||||||
|
# ... Validation logic here
|
||||||
|
return self
|
||||||
|
```
|
||||||
|
|
||||||
|
- `add: Json[list[AddSubscriptionData]]` will require the `add` argument to be a list of objects
|
||||||
|
having the keys that are specified in the `AddSubscriptionData` model.
|
||||||
|
|
||||||
|
- `@model_validator` can be used to specify additional validation logic for the model.
|
||||||
|
|
||||||
See
|
See
|
||||||
[zerver/lib/validator.py](https://github.com/zulip/zulip/blob/main/zerver/lib/validator.py)
|
[zerver/lib/typed_endpoint_validators.py](https://github.com/zulip/zulip/blob/main/zerver/lib/typed_endpoint_validators.py)
|
||||||
for more validators and their documentation.
|
for more validators and their documentation.
|
||||||
|
|
||||||
### Deciding which HTTP verb to use
|
### Deciding which HTTP verb to use
|
||||||
|
@ -261,10 +317,12 @@ For example, in [zerver/views/realm.py](https://github.com/zulip/zulip/blob/main
|
||||||
|
|
||||||
```py
|
```py
|
||||||
@require_realm_admin
|
@require_realm_admin
|
||||||
@has_request_variables
|
@typed_endpoint
|
||||||
def update_realm(
|
def update_realm(
|
||||||
request: HttpRequest, user_profile: UserProfile,
|
request: HttpRequest,
|
||||||
name: Optional[str]=REQ(str_validator=check_string, default=None),
|
user_profile: UserProfile,
|
||||||
|
*,
|
||||||
|
name: Annotated[str | None, StringConstraints(max_length=Realm.MAX_REALM_NAME_LENGTH)] = None,
|
||||||
# ...
|
# ...
|
||||||
):
|
):
|
||||||
realm = user_profile.realm
|
realm = user_profile.realm
|
||||||
|
@ -343,12 +401,13 @@ If the webhook does not have an option to provide a bot email, use the
|
||||||
`request.client` fields of a request:
|
`request.client` fields of a request:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
@webhook_view('PagerDuty')
|
@webhook_view("PagerDuty", all_event_types=ALL_EVENT_TYPES)
|
||||||
@has_request_variables
|
@typed_endpoint
|
||||||
def api_pagerduty_webhook(request, user_profile,
|
def api_pagerduty_webhook(
|
||||||
payload=REQ(argument_type='body'),
|
request: HttpRequest,
|
||||||
stream=REQ(default='pagerduty'),
|
user_profile: UserProfile,
|
||||||
topic=REQ(default=None)):
|
*,
|
||||||
|
payload: JsonBodyPayload[WildValue],
|
||||||
```
|
```
|
||||||
|
|
||||||
`request.client` will be the result of `get_client("ZulipPagerDutyWebhook")`
|
`request.client` will be the result of `get_client("ZulipPagerDutyWebhook")`
|
||||||
|
|
Loading…
Reference in New Issue