bareASGI-rest 5.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bareasgi_rest/__init__.py +14 -0
- bareasgi_rest/arg_builder.py +93 -0
- bareasgi_rest/constants.py +69 -0
- bareasgi_rest/py.typed +0 -0
- bareasgi_rest/rest_application.py +150 -0
- bareasgi_rest/rest_router.py +443 -0
- bareasgi_rest/serialization/__init__.py +1 -0
- bareasgi_rest/serialization/json.py +123 -0
- bareasgi_rest/serialization/xml.py +31 -0
- bareasgi_rest/swagger/__init__.py +14 -0
- bareasgi_rest/swagger/config.py +75 -0
- bareasgi_rest/swagger/controller.py +70 -0
- bareasgi_rest/swagger/entry.py +70 -0
- bareasgi_rest/swagger/errors.py +38 -0
- bareasgi_rest/swagger/helpers.py +23 -0
- bareasgi_rest/swagger/parameters.py +170 -0
- bareasgi_rest/swagger/paths.py +14 -0
- bareasgi_rest/swagger/properties.py +182 -0
- bareasgi_rest/swagger/repository.py +97 -0
- bareasgi_rest/swagger/responses.py +48 -0
- bareasgi_rest/swagger/types.py +80 -0
- bareasgi_rest/swagger/utils.py +22 -0
- bareasgi_rest/types.py +51 -0
- bareasgi_rest/utils.py +11 -0
- bareasgi_rest-5.0.0.dist-info/METADATA +287 -0
- bareasgi_rest-5.0.0.dist-info/RECORD +28 -0
- bareasgi_rest-5.0.0.dist-info/WHEEL +5 -0
- bareasgi_rest-5.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Enhanced REST support for bareASGI"""
|
|
2
|
+
|
|
3
|
+
from .rest_application import RestApplication
|
|
4
|
+
from .rest_router import RestHttpRouter
|
|
5
|
+
from .swagger.config import SwaggerConfig, SwaggerOauth2Config
|
|
6
|
+
from .types import RestError
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"RestApplication",
|
|
10
|
+
"RestHttpRouter",
|
|
11
|
+
"RestError",
|
|
12
|
+
"SwaggerConfig",
|
|
13
|
+
"SwaggerOauth2Config"
|
|
14
|
+
]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Argument builder"""
|
|
2
|
+
|
|
3
|
+
from inspect import Parameter, Signature
|
|
4
|
+
from typing import Any, Awaitable, Callable, get_args
|
|
5
|
+
|
|
6
|
+
from jetblack_serialization.custom_annotations import (
|
|
7
|
+
is_any_serialization_annotation
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
from jetblack_serialization import typing_ex
|
|
11
|
+
|
|
12
|
+
from .types import ArgDeserializer
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def is_optional_list(annotation: Any) -> bool:
|
|
16
|
+
if not typing_ex.is_optional(annotation):
|
|
17
|
+
return False
|
|
18
|
+
optional_types = typing_ex.get_optional_types(annotation)
|
|
19
|
+
if len(optional_types) != 1:
|
|
20
|
+
return False
|
|
21
|
+
return typing_ex.is_list(optional_types[0])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def make_args(
|
|
25
|
+
signature: Signature,
|
|
26
|
+
matches: dict[str, str],
|
|
27
|
+
query: dict[str, list[str]],
|
|
28
|
+
body: Callable[[Any], Awaitable[Any]],
|
|
29
|
+
arg_deserializer: ArgDeserializer
|
|
30
|
+
) -> tuple[tuple[Any, ...], dict[str, Any]]:
|
|
31
|
+
"""Make args and kwargs for the given signature from the route matches,
|
|
32
|
+
query args and body.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
signature (Signature): The function signature
|
|
36
|
+
matches (dict[str, str]): The route matches
|
|
37
|
+
query (dict[str, Any]): A dictionary built from the query string
|
|
38
|
+
body (Callable[[AsyncIterator[bytes], Any], Any]): Get the body
|
|
39
|
+
arg_deserializer (ArgDeserializer): A deserializer for args
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
KeyError: If a parameter was not found
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
tuple[tuple[Any, ...], dict[str, Any]]: A tuple for *args and **kwargs
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
kwargs: dict[str, Any] = {}
|
|
49
|
+
args: list[Any] = []
|
|
50
|
+
|
|
51
|
+
for parameter in signature.parameters.values():
|
|
52
|
+
if is_any_serialization_annotation(parameter.annotation):
|
|
53
|
+
value: Any = await body(parameter.annotation)
|
|
54
|
+
else:
|
|
55
|
+
if parameter.name in matches:
|
|
56
|
+
value = arg_deserializer(
|
|
57
|
+
matches[parameter.name],
|
|
58
|
+
parameter.annotation
|
|
59
|
+
)
|
|
60
|
+
elif parameter.name in query:
|
|
61
|
+
if typing_ex.is_list(
|
|
62
|
+
parameter.annotation
|
|
63
|
+
) or is_optional_list(
|
|
64
|
+
parameter.annotation
|
|
65
|
+
):
|
|
66
|
+
element_type, *_rest = get_args(parameter.annotation)
|
|
67
|
+
value = [
|
|
68
|
+
arg_deserializer(item, element_type)
|
|
69
|
+
for item in query[parameter.name]
|
|
70
|
+
]
|
|
71
|
+
else:
|
|
72
|
+
value = arg_deserializer(
|
|
73
|
+
query[parameter.name][0],
|
|
74
|
+
parameter.annotation
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
elif typing_ex.is_optional(parameter.annotation):
|
|
78
|
+
value = None
|
|
79
|
+
else:
|
|
80
|
+
raise KeyError(parameter.name)
|
|
81
|
+
|
|
82
|
+
if (
|
|
83
|
+
parameter.kind == Parameter.POSITIONAL_ONLY
|
|
84
|
+
or parameter.kind == Parameter.POSITIONAL_OR_KEYWORD
|
|
85
|
+
):
|
|
86
|
+
args.append(value)
|
|
87
|
+
else:
|
|
88
|
+
kwargs[parameter.name] = value
|
|
89
|
+
|
|
90
|
+
bound_args = signature.bind(*args, **kwargs)
|
|
91
|
+
bound_args.apply_defaults()
|
|
92
|
+
|
|
93
|
+
return bound_args.args, bound_args.kwargs
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Constants"""
|
|
2
|
+
|
|
3
|
+
from bareasgi import HttpResponse, text_writer
|
|
4
|
+
from stringcase import camelcase, snakecase, pascalcase
|
|
5
|
+
|
|
6
|
+
from jetblack_serialization import SerializerConfig
|
|
7
|
+
|
|
8
|
+
from .types import (
|
|
9
|
+
DictConsumes,
|
|
10
|
+
DictProduces,
|
|
11
|
+
DictSerializerConfig
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from .serialization.json import (
|
|
15
|
+
to_json,
|
|
16
|
+
from_json,
|
|
17
|
+
from_form_data,
|
|
18
|
+
from_query_string,
|
|
19
|
+
json_arg_deserializer_factory
|
|
20
|
+
)
|
|
21
|
+
from .serialization.xml import from_xml, to_xml
|
|
22
|
+
from .swagger import SwaggerConfig
|
|
23
|
+
|
|
24
|
+
DEFAULT_SWAGGER_BASE_URL = "https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.4.0"
|
|
25
|
+
DEFAULT_TYPEFACE_URL = "https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
|
|
26
|
+
|
|
27
|
+
DEFAULT_CONSUMES: DictConsumes = {
|
|
28
|
+
b'application/json': from_json,
|
|
29
|
+
b'application/x-www-form-urlencoded': from_query_string,
|
|
30
|
+
b'multipart/form-data': from_form_data,
|
|
31
|
+
b'application/xml': from_xml,
|
|
32
|
+
b'*/*': from_json,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
DEFAULT_PRODUCES: DictProduces = {
|
|
36
|
+
b'application/json': to_json,
|
|
37
|
+
b'application/xml': to_xml,
|
|
38
|
+
b'*/*': to_json,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
DEFAULT_COLLECTION_FORMAT = 'multi'
|
|
42
|
+
|
|
43
|
+
DEFAULT_NOT_FOUND_RESPONSE: HttpResponse = HttpResponse(
|
|
44
|
+
404,
|
|
45
|
+
[(b'content-type', b'text/plain')],
|
|
46
|
+
text_writer('Not Found')
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
DEFAULT_JSON_SERIALIZER_CONFIG = SerializerConfig(
|
|
50
|
+
key_serializer=camelcase,
|
|
51
|
+
key_deserializer=snakecase
|
|
52
|
+
)
|
|
53
|
+
DEFAULT_XML_SERIALIZER_CONFIG = SerializerConfig(
|
|
54
|
+
key_serializer=pascalcase,
|
|
55
|
+
key_deserializer=snakecase
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
DEFAULT_SERIALIZER_CONFIG: DictSerializerConfig = {
|
|
59
|
+
b'application/json': DEFAULT_JSON_SERIALIZER_CONFIG,
|
|
60
|
+
b'*/*': DEFAULT_JSON_SERIALIZER_CONFIG,
|
|
61
|
+
b'application/xml': DEFAULT_XML_SERIALIZER_CONFIG,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
DEFAULT_ARG_DESERIALIZER_FACTORY = json_arg_deserializer_factory
|
|
65
|
+
|
|
66
|
+
DEFAULT_SWAGGER_CONFIG = SwaggerConfig(
|
|
67
|
+
serialize_key=camelcase,
|
|
68
|
+
deserialize_key=snakecase
|
|
69
|
+
)
|
bareasgi_rest/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
from typing import Any, Callable, Final, Sequence, cast
|
|
2
|
+
|
|
3
|
+
from jetblack_serialization.config import SerializerConfig
|
|
4
|
+
|
|
5
|
+
from bareasgi import (
|
|
6
|
+
Application,
|
|
7
|
+
HttpMiddlewares,
|
|
8
|
+
HttpResponse,
|
|
9
|
+
LifespanRequestHandler,
|
|
10
|
+
WebSocketMiddlewares,
|
|
11
|
+
WebSocketRouter,
|
|
12
|
+
)
|
|
13
|
+
from bareutils import text_writer
|
|
14
|
+
from bareutils import response_code
|
|
15
|
+
|
|
16
|
+
from .constants import DEFAULT_COLLECTION_FORMAT
|
|
17
|
+
from .rest_router import RestHttpRouter
|
|
18
|
+
from .types import ArgDeserializerFactory, DictSerializerConfig, RestCallback
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
DEFAULT_NOT_FOUND_RESPONSE: Final[HttpResponse] = HttpResponse(
|
|
22
|
+
response_code.NOT_FOUND,
|
|
23
|
+
[(b'content-type', b'text/plain')],
|
|
24
|
+
text_writer('Not Found')
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RestApplication(Application):
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
*,
|
|
33
|
+
middlewares: HttpMiddlewares | None = None,
|
|
34
|
+
rest_router: RestHttpRouter | None = None,
|
|
35
|
+
ws_middlewares: WebSocketMiddlewares | None = None,
|
|
36
|
+
ws_router: WebSocketRouter | None = None,
|
|
37
|
+
startup_handlers: list[LifespanRequestHandler] | None = None,
|
|
38
|
+
shutdown_handlers: list[LifespanRequestHandler] | None = None,
|
|
39
|
+
not_found_response: HttpResponse = DEFAULT_NOT_FOUND_RESPONSE,
|
|
40
|
+
info: dict[str, Any] | None = None
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Construct the application
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from bareasgi import (
|
|
46
|
+
Application,
|
|
47
|
+
Scope,
|
|
48
|
+
HttpRequest,
|
|
49
|
+
HttpResponse,
|
|
50
|
+
text_reader,
|
|
51
|
+
text_writer
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
async def http_request_callback(request: HttpRequest) -> HttpResponse:
|
|
55
|
+
text = await text_reader(request.body)
|
|
56
|
+
return HttpResponse(
|
|
57
|
+
200,
|
|
58
|
+
[(b'content-type', b'text/plain')],
|
|
59
|
+
text_writer('This is not a test')
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
import uvicorn
|
|
63
|
+
|
|
64
|
+
app = Application()
|
|
65
|
+
app.http_router.add({'GET', 'POST', 'PUT', 'DELETE'}, '/{path}', http_request_callback)
|
|
66
|
+
|
|
67
|
+
uvicorn.run(app, port=9009)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
middlewares (HttpMiddlewares | None, optional): Optional
|
|
72
|
+
middleware callbacks. Defaults to None.
|
|
73
|
+
rest_router (RestHttpRouter | None, optional): Optional router to for
|
|
74
|
+
http routes. Defaults to None.
|
|
75
|
+
ws_middlewares (WebSocketMiddlewares | None, optional):
|
|
76
|
+
Optional middleware callbacks. Defaults to None.
|
|
77
|
+
ws_router (WebSocketRouter | None, optional): Optional
|
|
78
|
+
router for web routes. Defaults to None.
|
|
79
|
+
startup_handlers (list[LifespanHandler] | None, optional): Optional
|
|
80
|
+
handlers to run at startup. Defaults to None.
|
|
81
|
+
shutdown_handlers (list[LifespanHandler] | None, optional): Optional
|
|
82
|
+
handlers to run at shutdown. Defaults to None.
|
|
83
|
+
not_found_response (HttpResponse | None, optional): Optional not
|
|
84
|
+
found (404) response. Defaults to DEFAULT_NOT_FOUND_RESPONSE.
|
|
85
|
+
info (dict[str, Any] | None, optional): Optional
|
|
86
|
+
dictionary for user data. Defaults to None.
|
|
87
|
+
"""
|
|
88
|
+
super().__init__(
|
|
89
|
+
middlewares=middlewares,
|
|
90
|
+
http_router=rest_router,
|
|
91
|
+
ws_middlewares=ws_middlewares,
|
|
92
|
+
ws_router=ws_router,
|
|
93
|
+
startup_handlers=startup_handlers,
|
|
94
|
+
shutdown_handlers=shutdown_handlers,
|
|
95
|
+
not_found_response=not_found_response,
|
|
96
|
+
info=info
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def rest_router(self) -> RestHttpRouter:
|
|
101
|
+
"""Get the REST router
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
RestHttpRouter: The REST router
|
|
105
|
+
"""
|
|
106
|
+
return cast(RestHttpRouter, self.http_router)
|
|
107
|
+
|
|
108
|
+
def on_rest_request(
|
|
109
|
+
self,
|
|
110
|
+
methods: set[str],
|
|
111
|
+
path: str,
|
|
112
|
+
*,
|
|
113
|
+
consumes: Sequence[bytes] | None = None,
|
|
114
|
+
produces: Sequence[bytes] | None = None,
|
|
115
|
+
collection_format: str = DEFAULT_COLLECTION_FORMAT,
|
|
116
|
+
tags: list[str] | None = None,
|
|
117
|
+
status_code: int = response_code.OK,
|
|
118
|
+
status_description: str = 'OK',
|
|
119
|
+
serializer_config: DictSerializerConfig | None = None,
|
|
120
|
+
arg_serializer_config: SerializerConfig | None = None,
|
|
121
|
+
arg_deserializer_factory: ArgDeserializerFactory | None = None
|
|
122
|
+
) -> Callable[[RestCallback], RestCallback]:
|
|
123
|
+
"""A decorator to add an http rest route handler to the application
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
methods (AbstractSet[str]): The http methods, e.g. {{'POST', 'PUT'}
|
|
127
|
+
path (str): The path
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Callable[[HttpRequestCallback], HttpRequestCallback]: The decorated
|
|
131
|
+
request.
|
|
132
|
+
"""
|
|
133
|
+
def decorator(callback: RestCallback) -> Callable:
|
|
134
|
+
self.rest_router.add_rest(
|
|
135
|
+
methods,
|
|
136
|
+
path,
|
|
137
|
+
callback,
|
|
138
|
+
consumes=consumes,
|
|
139
|
+
produces=produces,
|
|
140
|
+
collection_format=collection_format,
|
|
141
|
+
tags=tags,
|
|
142
|
+
status_code=status_code,
|
|
143
|
+
status_description=status_description,
|
|
144
|
+
serializer_config=serializer_config,
|
|
145
|
+
arg_serializer_config=arg_serializer_config,
|
|
146
|
+
arg_deserializer_factory=arg_deserializer_factory
|
|
147
|
+
)
|
|
148
|
+
return callback
|
|
149
|
+
|
|
150
|
+
return decorator
|