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.
@@ -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