18.2.10. Describing API¶
NextGIS Web incorporates type-annotated views and routes to:
Ensure input data is valid
Convert data into Python objects
Create OpenAPI 3.1.0 schema
This approach heavily relies on Annotated type hints and the MsgSpec library. Currently, it supports a specific set of types, with plans to potentially add more in the future, without attempting to accommodate every possible use case.
Similar to the Pyramid framework, the handling of views is determined by their signatures. The established conventions for these view signatures are:
An argument named
request
is required at the first or second positionIf the
request
argument is the second, the first is a request contextA request context argument can use any name, like
resource
orobj
An argument named
body
orjson_body
represents decoded request bodyKeyword-only arguments (after
*
) represent query parametersThe remaining arguments are treated as path parameters
Path parameters¶
Supported types: string (str
) and integers (int
) only. Behind the
scenes, values are decoded using MsgSpec, thus msgspec.Meta
constraints
work.
from typing_extensions import Annotated
from msgspec import Meta
def path_param(request, param: Annotated[int, Meta(gt=0)]):
...
def setup_pyramid(comp, config):
config.add_route(
"path_param",
"/path/{param}",
types=dict(param=int),
get=path_param,
)
In the example above, both /path/1
and /path/0
will match the route, but
for /path/0
the 422 Unprocessable Entity
error will be returned as it
doesn’t fit param > 0
condition.
Query parameters¶
Supported types:
- Primitives:
Basic types:
str
,int
,bool
,float
enum.Enum
with string values onlytyping.Literal
with string and integer values
- Sequences of primitive types:
typing.List
for variable-length uniform liststyping.Tuple
for fixed-length non-uniform and uniform tuples
- Objects:
msgspec.Struct
typing.Dict
from typing import Dict, List
from msgspec import Struct
class SomeStruct(Struct, kw_only=True):
foo: str
bar: str = "qux"
def query_param(
request,
*,
txt: str,
num: int = 0,
flag: bool = False,
list_str: List[str],
list_int: List[int] = [1, 2, 3],
tuple_mixed: Tuple[str, int],
tuple_uniform: Tuple[float, ...],
struct: SomeStruct,
dict_str_int: Dict[str, int],
dict_list: Dict[str, List[int]],
):
...
Arguments of primitive types can accept a default value. For booleans true
and false
values should be used, but yes
and no
are also accepted.
In case of multiple values of the same parameter (...&num=1&num=2&...
), the
last value is decoded (num == 2
).
List values are decoded using the form
style array encoding with as
comma-separated values. Urlencoded commas are decoded as a part of values, plain
commas as list separators. An empty string value (...&arr_str=&...
) is
decoded as an empty list.
Structs are decoded using the form
style object encoding which means that
every struct field becomes an URL parameter (...&foo=some&bar=other&...
).
This fact can help to reuse a Struct for a group of parameters without
repeating. Default values aren’t allowed for structs, fields with no default
value are required parameters.
Dictionaries are decoded using the deepObject
style encoding as their
possible keys are unknown (...&obj_dict[a]=1&obj_dict[b]=2&...
). Default
values aren’t allowed for dictionaries.
Request body¶
For request bodies msgspec.Struct
types should be used in most cases. Refer
to MsgSpec documentation for details, here is the minimal example:
from msgspec import Struct
class SomeStruct(Struct, kw_only=True):
foo: str
bar: str = "qux"
def body(request, body: SomeStruct):
...
Response¶
View results are encoded using MsgSpec JSON encoder depending on return annotation in the following cases:
Declared as
msgspec.Struct
Wrapped into the
AsJSON
helper
These options support OpenAPI schema generation and static type checking, here is the examples:
from msgspec import Struct
from nextgisweb.lib.apitype import AsJSON
from nextgisweb.pyramid import viewargs
class SomeStruct(Struct, kw_only=True):
foo: str
bar: str = "qux"
def struct(request) -> SomeStruct:
return SomeStruct(foo="zoo")
def helper(request) -> AsJSON[int]:
return 1
The StatusCode
annotation can be used to declare non-200 status codes. It’s
important to note that this annotation only modifies the OpenAPI schema. To set
the actual response status code, you should use
request.response.status_code
:
from typing_extensions import Annotated
from msgspec import Struct
from nextgisweb.lib.apitype import StatusCode
class SomeStruct(Struct, kw_only=True):
foo: str
def create(request) -> Annotated[SomeStruct, StatusCode(201)]:
request.response.status_code = 201
return SomeStruct(foo="zoo")
If there is no idea which JSON value to return as nothing, like DELETE
methods, EmptyObject
can be used. It accepts None
and converts it to
{}
. An empty object is better than the null
value due to future
extensibility.
from nextgisweb.lib.apitype import EmptyObject
def void(request) -> EmptyObject:
pass