clear-skies 2.0.5__py3-none-any.whl → 2.0.6__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.
Potentially problematic release.
This version of clear-skies might be problematic. Click here for more details.
- {clear_skies-2.0.5.dist-info → clear_skies-2.0.6.dist-info}/METADATA +1 -1
- clear_skies-2.0.6.dist-info/RECORD +251 -0
- clearskies/__init__.py +61 -0
- clearskies/action.py +7 -0
- clearskies/authentication/__init__.py +15 -0
- clearskies/authentication/authentication.py +46 -0
- clearskies/authentication/authorization.py +16 -0
- clearskies/authentication/authorization_pass_through.py +20 -0
- clearskies/authentication/jwks.py +163 -0
- clearskies/authentication/public.py +5 -0
- clearskies/authentication/secret_bearer.py +553 -0
- clearskies/autodoc/__init__.py +8 -0
- clearskies/autodoc/formats/__init__.py +5 -0
- clearskies/autodoc/formats/oai3_json/__init__.py +7 -0
- clearskies/autodoc/formats/oai3_json/oai3_json.py +87 -0
- clearskies/autodoc/formats/oai3_json/oai3_schema_resolver.py +15 -0
- clearskies/autodoc/formats/oai3_json/parameter.py +35 -0
- clearskies/autodoc/formats/oai3_json/request.py +68 -0
- clearskies/autodoc/formats/oai3_json/response.py +28 -0
- clearskies/autodoc/formats/oai3_json/schema/__init__.py +11 -0
- clearskies/autodoc/formats/oai3_json/schema/array.py +9 -0
- clearskies/autodoc/formats/oai3_json/schema/default.py +13 -0
- clearskies/autodoc/formats/oai3_json/schema/enum.py +7 -0
- clearskies/autodoc/formats/oai3_json/schema/object.py +35 -0
- clearskies/autodoc/formats/oai3_json/test.json +1985 -0
- clearskies/autodoc/py.typed +0 -0
- clearskies/autodoc/request/__init__.py +15 -0
- clearskies/autodoc/request/header.py +6 -0
- clearskies/autodoc/request/json_body.py +6 -0
- clearskies/autodoc/request/parameter.py +8 -0
- clearskies/autodoc/request/request.py +47 -0
- clearskies/autodoc/request/url_parameter.py +6 -0
- clearskies/autodoc/request/url_path.py +6 -0
- clearskies/autodoc/response/__init__.py +5 -0
- clearskies/autodoc/response/response.py +9 -0
- clearskies/autodoc/schema/__init__.py +31 -0
- clearskies/autodoc/schema/array.py +10 -0
- clearskies/autodoc/schema/base64.py +8 -0
- clearskies/autodoc/schema/boolean.py +5 -0
- clearskies/autodoc/schema/date.py +5 -0
- clearskies/autodoc/schema/datetime.py +5 -0
- clearskies/autodoc/schema/double.py +5 -0
- clearskies/autodoc/schema/enum.py +17 -0
- clearskies/autodoc/schema/integer.py +6 -0
- clearskies/autodoc/schema/long.py +5 -0
- clearskies/autodoc/schema/number.py +6 -0
- clearskies/autodoc/schema/object.py +13 -0
- clearskies/autodoc/schema/password.py +5 -0
- clearskies/autodoc/schema/schema.py +11 -0
- clearskies/autodoc/schema/string.py +5 -0
- clearskies/backends/__init__.py +65 -0
- clearskies/backends/api_backend.py +1178 -0
- clearskies/backends/backend.py +136 -0
- clearskies/backends/cursor_backend.py +335 -0
- clearskies/backends/memory_backend.py +797 -0
- clearskies/backends/secrets_backend.py +106 -0
- clearskies/column.py +1233 -0
- clearskies/columns/__init__.py +71 -0
- clearskies/columns/audit.py +206 -0
- clearskies/columns/belongs_to_id.py +483 -0
- clearskies/columns/belongs_to_model.py +132 -0
- clearskies/columns/belongs_to_self.py +105 -0
- clearskies/columns/boolean.py +113 -0
- clearskies/columns/category_tree.py +275 -0
- clearskies/columns/category_tree_ancestors.py +51 -0
- clearskies/columns/category_tree_children.py +127 -0
- clearskies/columns/category_tree_descendants.py +48 -0
- clearskies/columns/created.py +95 -0
- clearskies/columns/created_by_authorization_data.py +116 -0
- clearskies/columns/created_by_header.py +99 -0
- clearskies/columns/created_by_ip.py +92 -0
- clearskies/columns/created_by_routing_data.py +97 -0
- clearskies/columns/created_by_user_agent.py +92 -0
- clearskies/columns/date.py +234 -0
- clearskies/columns/datetime.py +282 -0
- clearskies/columns/email.py +76 -0
- clearskies/columns/float.py +153 -0
- clearskies/columns/has_many.py +505 -0
- clearskies/columns/has_many_self.py +56 -0
- clearskies/columns/has_one.py +14 -0
- clearskies/columns/integer.py +160 -0
- clearskies/columns/json.py +128 -0
- clearskies/columns/many_to_many_ids.py +337 -0
- clearskies/columns/many_to_many_ids_with_data.py +274 -0
- clearskies/columns/many_to_many_models.py +158 -0
- clearskies/columns/many_to_many_pivots.py +134 -0
- clearskies/columns/phone.py +159 -0
- clearskies/columns/select.py +92 -0
- clearskies/columns/string.py +102 -0
- clearskies/columns/timestamp.py +164 -0
- clearskies/columns/updated.py +110 -0
- clearskies/columns/uuid.py +86 -0
- clearskies/configs/README.md +105 -0
- clearskies/configs/__init__.py +162 -0
- clearskies/configs/actions.py +43 -0
- clearskies/configs/any.py +13 -0
- clearskies/configs/any_dict.py +22 -0
- clearskies/configs/any_dict_or_callable.py +23 -0
- clearskies/configs/authentication.py +23 -0
- clearskies/configs/authorization.py +23 -0
- clearskies/configs/boolean.py +16 -0
- clearskies/configs/boolean_or_callable.py +18 -0
- clearskies/configs/callable_config.py +18 -0
- clearskies/configs/columns.py +34 -0
- clearskies/configs/conditions.py +30 -0
- clearskies/configs/config.py +24 -0
- clearskies/configs/datetime.py +18 -0
- clearskies/configs/datetime_or_callable.py +19 -0
- clearskies/configs/endpoint.py +23 -0
- clearskies/configs/endpoint_list.py +29 -0
- clearskies/configs/float.py +16 -0
- clearskies/configs/float_or_callable.py +18 -0
- clearskies/configs/integer.py +16 -0
- clearskies/configs/integer_or_callable.py +18 -0
- clearskies/configs/joins.py +30 -0
- clearskies/configs/list_any_dict.py +30 -0
- clearskies/configs/list_any_dict_or_callable.py +31 -0
- clearskies/configs/model_class.py +35 -0
- clearskies/configs/model_column.py +65 -0
- clearskies/configs/model_columns.py +56 -0
- clearskies/configs/model_destination_name.py +25 -0
- clearskies/configs/model_to_id_column.py +43 -0
- clearskies/configs/readable_model_column.py +9 -0
- clearskies/configs/readable_model_columns.py +9 -0
- clearskies/configs/schema.py +23 -0
- clearskies/configs/searchable_model_columns.py +9 -0
- clearskies/configs/security_headers.py +39 -0
- clearskies/configs/select.py +26 -0
- clearskies/configs/select_list.py +47 -0
- clearskies/configs/string.py +29 -0
- clearskies/configs/string_dict.py +32 -0
- clearskies/configs/string_list.py +32 -0
- clearskies/configs/string_list_or_callable.py +35 -0
- clearskies/configs/string_or_callable.py +18 -0
- clearskies/configs/timedelta.py +18 -0
- clearskies/configs/timezone.py +18 -0
- clearskies/configs/url.py +23 -0
- clearskies/configs/validators.py +45 -0
- clearskies/configs/writeable_model_column.py +9 -0
- clearskies/configs/writeable_model_columns.py +9 -0
- clearskies/configurable.py +76 -0
- clearskies/contexts/__init__.py +11 -0
- clearskies/contexts/cli.py +117 -0
- clearskies/contexts/context.py +98 -0
- clearskies/contexts/wsgi.py +76 -0
- clearskies/contexts/wsgi_ref.py +82 -0
- clearskies/decorators.py +33 -0
- clearskies/di/__init__.py +14 -0
- clearskies/di/additional_config.py +130 -0
- clearskies/di/additional_config_auto_import.py +17 -0
- clearskies/di/di.py +973 -0
- clearskies/di/inject/__init__.py +23 -0
- clearskies/di/inject/by_class.py +21 -0
- clearskies/di/inject/by_name.py +18 -0
- clearskies/di/inject/di.py +13 -0
- clearskies/di/inject/environment.py +14 -0
- clearskies/di/inject/input_output.py +20 -0
- clearskies/di/inject/now.py +13 -0
- clearskies/di/inject/requests.py +13 -0
- clearskies/di/inject/secrets.py +14 -0
- clearskies/di/inject/utcnow.py +13 -0
- clearskies/di/inject/uuid.py +15 -0
- clearskies/di/injectable.py +29 -0
- clearskies/di/injectable_properties.py +131 -0
- clearskies/di/test_module/__init__.py +6 -0
- clearskies/di/test_module/another_module/__init__.py +2 -0
- clearskies/di/test_module/module_class.py +5 -0
- clearskies/end.py +183 -0
- clearskies/endpoint.py +1314 -0
- clearskies/endpoint_group.py +336 -0
- clearskies/endpoints/__init__.py +25 -0
- clearskies/endpoints/advanced_search.py +526 -0
- clearskies/endpoints/callable.py +388 -0
- clearskies/endpoints/create.py +205 -0
- clearskies/endpoints/delete.py +139 -0
- clearskies/endpoints/get.py +271 -0
- clearskies/endpoints/health_check.py +183 -0
- clearskies/endpoints/list.py +574 -0
- clearskies/endpoints/restful_api.py +427 -0
- clearskies/endpoints/schema.py +189 -0
- clearskies/endpoints/simple_search.py +286 -0
- clearskies/endpoints/update.py +193 -0
- clearskies/environment.py +104 -0
- clearskies/exceptions/__init__.py +19 -0
- clearskies/exceptions/authentication.py +2 -0
- clearskies/exceptions/authorization.py +2 -0
- clearskies/exceptions/client_error.py +2 -0
- clearskies/exceptions/input_errors.py +4 -0
- clearskies/exceptions/missing_dependency.py +2 -0
- clearskies/exceptions/moved_permanently.py +3 -0
- clearskies/exceptions/moved_temporarily.py +3 -0
- clearskies/exceptions/not_found.py +2 -0
- clearskies/functional/__init__.py +7 -0
- clearskies/functional/routing.py +92 -0
- clearskies/functional/string.py +112 -0
- clearskies/functional/validations.py +76 -0
- clearskies/input_outputs/__init__.py +13 -0
- clearskies/input_outputs/cli.py +171 -0
- clearskies/input_outputs/exceptions/__init__.py +2 -0
- clearskies/input_outputs/exceptions/cli_input_error.py +2 -0
- clearskies/input_outputs/exceptions/cli_not_found.py +2 -0
- clearskies/input_outputs/headers.py +45 -0
- clearskies/input_outputs/input_output.py +138 -0
- clearskies/input_outputs/programmatic.py +69 -0
- clearskies/input_outputs/py.typed +0 -0
- clearskies/input_outputs/wsgi.py +77 -0
- clearskies/model.py +1922 -0
- clearskies/py.typed +0 -0
- clearskies/query/__init__.py +12 -0
- clearskies/query/condition.py +223 -0
- clearskies/query/join.py +136 -0
- clearskies/query/query.py +196 -0
- clearskies/query/sort.py +27 -0
- clearskies/schema.py +82 -0
- clearskies/secrets/__init__.py +6 -0
- clearskies/secrets/additional_configs/__init__.py +32 -0
- clearskies/secrets/additional_configs/mysql_connection_dynamic_producer.py +61 -0
- clearskies/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +160 -0
- clearskies/secrets/akeyless.py +182 -0
- clearskies/secrets/exceptions/__init__.py +1 -0
- clearskies/secrets/exceptions/not_found.py +2 -0
- clearskies/secrets/secrets.py +38 -0
- clearskies/security_header.py +15 -0
- clearskies/security_headers/__init__.py +11 -0
- clearskies/security_headers/cache_control.py +67 -0
- clearskies/security_headers/cors.py +50 -0
- clearskies/security_headers/csp.py +94 -0
- clearskies/security_headers/hsts.py +22 -0
- clearskies/security_headers/x_content_type_options.py +0 -0
- clearskies/security_headers/x_frame_options.py +0 -0
- clearskies/test_base.py +8 -0
- clearskies/typing.py +11 -0
- clearskies/validator.py +37 -0
- clearskies/validators/__init__.py +33 -0
- clearskies/validators/after_column.py +62 -0
- clearskies/validators/before_column.py +13 -0
- clearskies/validators/in_the_future.py +32 -0
- clearskies/validators/in_the_future_at_least.py +11 -0
- clearskies/validators/in_the_future_at_most.py +10 -0
- clearskies/validators/in_the_past.py +32 -0
- clearskies/validators/in_the_past_at_least.py +10 -0
- clearskies/validators/in_the_past_at_most.py +10 -0
- clearskies/validators/maximum_length.py +26 -0
- clearskies/validators/maximum_value.py +29 -0
- clearskies/validators/minimum_length.py +26 -0
- clearskies/validators/minimum_value.py +29 -0
- clearskies/validators/required.py +34 -0
- clearskies/validators/timedelta.py +59 -0
- clearskies/validators/unique.py +30 -0
- clear_skies-2.0.5.dist-info/RECORD +0 -4
- {clear_skies-2.0.5.dist-info → clear_skies-2.0.6.dist-info}/WHEEL +0 -0
- {clear_skies-2.0.5.dist-info → clear_skies-2.0.6.dist-info}/licenses/LICENSE +0 -0
clearskies/endpoint.py
ADDED
|
@@ -0,0 +1,1314 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import urllib.parse
|
|
5
|
+
from collections import OrderedDict
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
7
|
+
|
|
8
|
+
import clearskies.column
|
|
9
|
+
import clearskies.configs
|
|
10
|
+
import clearskies.configurable
|
|
11
|
+
import clearskies.decorators
|
|
12
|
+
import clearskies.di
|
|
13
|
+
import clearskies.end
|
|
14
|
+
import clearskies.typing
|
|
15
|
+
from clearskies import autodoc, exceptions
|
|
16
|
+
from clearskies.authentication import Authentication, Authorization, Public
|
|
17
|
+
from clearskies.autodoc import schema
|
|
18
|
+
from clearskies.autodoc.request import Parameter, Request
|
|
19
|
+
from clearskies.autodoc.response import Response
|
|
20
|
+
from clearskies.functional import routing, string, validations
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from clearskies import Column, Model, SecurityHeader
|
|
24
|
+
from clearskies.input_outputs import InputOutput
|
|
25
|
+
from clearskies.schema import Schema
|
|
26
|
+
from clearskies.security_headers import Cors
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Endpoint(
|
|
30
|
+
clearskies.end.End, # type: ignore
|
|
31
|
+
clearskies.configurable.Configurable,
|
|
32
|
+
clearskies.di.InjectableProperties,
|
|
33
|
+
):
|
|
34
|
+
"""
|
|
35
|
+
Automating drudgery!
|
|
36
|
+
|
|
37
|
+
With clearskies, endpoints exist to offload some drudgery and make your life easier, but they can also
|
|
38
|
+
get out of your way when you don't need them. Think of them as pre-built endpoints that can execute
|
|
39
|
+
common functionality needed for web applications/APIs. Instead of defining a function that fetches
|
|
40
|
+
records from your backend and returns them to the end user, you can let the list endpoint do this for you
|
|
41
|
+
with a minimal amount of configuration. Instead of making an endpoint that creates records, just deploy
|
|
42
|
+
a create endpoint. While this gives clearskies some helpful capabiltiies for automation, it also has
|
|
43
|
+
the Callable endpoint which simply calls a developer-defined function, and therefore allows clearskies to
|
|
44
|
+
act like a much more typical framework.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
"""
|
|
48
|
+
The dependency injection container
|
|
49
|
+
"""
|
|
50
|
+
di = clearskies.di.inject.Di()
|
|
51
|
+
|
|
52
|
+
"""
|
|
53
|
+
Whether or not this endpoint can handle CORS
|
|
54
|
+
"""
|
|
55
|
+
has_cors = False
|
|
56
|
+
|
|
57
|
+
"""
|
|
58
|
+
The actual CORS header
|
|
59
|
+
"""
|
|
60
|
+
cors_header: Cors | None = None
|
|
61
|
+
|
|
62
|
+
"""
|
|
63
|
+
Set some response headers that should be returned for this endpoint.
|
|
64
|
+
|
|
65
|
+
Provide a list of response headers to return to the caller when this endpoint is executed.
|
|
66
|
+
This should be given a list containing a combination of strings or callables that return a list of strings.
|
|
67
|
+
The strings in question should be headers formatted as "key: value". If you attach a callable, it can accept
|
|
68
|
+
any of the standard dependencies or context-specific values like any other callable in a clearskies
|
|
69
|
+
application:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
def custom_headers(query_parameters):
|
|
73
|
+
some_value = "yes" if query_parameters.get("stuff") else "no"
|
|
74
|
+
return [f"x-custom: {some_value}", "content-type: application/custom"]
|
|
75
|
+
|
|
76
|
+
endpoint = clearskies.endpoints.Callable(
|
|
77
|
+
lambda: {"hello": "world"},
|
|
78
|
+
response_headers=custom_headers,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
wsgi = clearskies.contexts.WsgiRef(endpoint)
|
|
82
|
+
wsgi()
|
|
83
|
+
```
|
|
84
|
+
"""
|
|
85
|
+
response_headers = clearskies.configs.StringListOrCallable(default=[])
|
|
86
|
+
|
|
87
|
+
"""
|
|
88
|
+
Set the URL for the endpoint
|
|
89
|
+
|
|
90
|
+
When an endpoint is attached directly to a context, then the endpoint's URL becomes the exact URL
|
|
91
|
+
to invoke the endpoint. If it is instead attached to an endpoint group, then the URL of the endpoint
|
|
92
|
+
becomes a suffix on the URL of the group. This is described in more detail in the documentation for endpoint
|
|
93
|
+
groups, so here's an example of attaching endpoints directly and setting the URL:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
import clearskies
|
|
97
|
+
|
|
98
|
+
endpoint = clearskies.endpoints.Callable(
|
|
99
|
+
lambda: {"hello": "World"},
|
|
100
|
+
url="/hello/world",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
wsgi = clearskies.contexts.WsgiRef(endpoint)
|
|
104
|
+
wsgi()
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Which then acts as expected:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
$ curl 'http://localhost:8080/hello/asdf' | jq
|
|
111
|
+
{
|
|
112
|
+
"status": "client_error",
|
|
113
|
+
"error": "Not Found",
|
|
114
|
+
"data": [],
|
|
115
|
+
"pagination": {},
|
|
116
|
+
"input_errors": {}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
$ curl 'http://localhost:8080/hello/world' | jq
|
|
120
|
+
{
|
|
121
|
+
"status": "success",
|
|
122
|
+
"error": "",
|
|
123
|
+
"data": {
|
|
124
|
+
"hello": "world"
|
|
125
|
+
},
|
|
126
|
+
"pagination": {},
|
|
127
|
+
"input_errors": {}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Some endpoints allow or require the use of named routing parameters. Named routing paths are created using either the
|
|
132
|
+
`/{name}/` syntax or `/:name/`. These parameters can be injected into any callable via the `routing_data`
|
|
133
|
+
dependency injection name, as well as via their name:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
import clearskies
|
|
137
|
+
|
|
138
|
+
endpoint = clearskies.endpoints.Callable(
|
|
139
|
+
lambda first_name, last_name: {"hello": f"{first_name} {last_name}"},
|
|
140
|
+
url="/hello/:first_name/{last_name}",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
wsgi = clearskies.contexts.WsgiRef(endpoint)
|
|
144
|
+
wsgi()
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Which you can then invoke in the usual way:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
$ curl 'http://localhost:8080/hello/bob/brown' | jq
|
|
151
|
+
{
|
|
152
|
+
"status": "success",
|
|
153
|
+
"error": "",
|
|
154
|
+
"data": {
|
|
155
|
+
"hello": "bob brown"
|
|
156
|
+
},
|
|
157
|
+
"pagination": {},
|
|
158
|
+
"input_errors": {}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
"""
|
|
164
|
+
url = clearskies.configs.Url(default="")
|
|
165
|
+
|
|
166
|
+
"""
|
|
167
|
+
The allowed request methods for this endpoint.
|
|
168
|
+
|
|
169
|
+
By default, only GET is allowed.
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
import clearskies
|
|
173
|
+
|
|
174
|
+
endpoint = clearskies.endpoints.Callable(
|
|
175
|
+
lambda: {"hello": "world"},
|
|
176
|
+
request_methods=["POST"],
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
wsgi = clearskies.contexts.WsgiRef(endpoint)
|
|
180
|
+
wsgi()
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
And to execute:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
$ curl 'http://localhost:8080/' -X POST | jq
|
|
187
|
+
{
|
|
188
|
+
"status": "success",
|
|
189
|
+
"error": "",
|
|
190
|
+
"data": {
|
|
191
|
+
"hello": "world"
|
|
192
|
+
},
|
|
193
|
+
"pagination": {},
|
|
194
|
+
"input_errors": {}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
$ curl 'http://localhost:8080/' -X GET | jq
|
|
198
|
+
{
|
|
199
|
+
"status": "client_error",
|
|
200
|
+
"error": "Not Found",
|
|
201
|
+
"data": [],
|
|
202
|
+
"pagination": {},
|
|
203
|
+
"input_errors": {}
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
"""
|
|
207
|
+
request_methods = clearskies.configs.SelectList(
|
|
208
|
+
allowed_values=["GET", "POST", "PUT", "DELETE", "PATCH", "QUERY"], default=["GET"]
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
"""
|
|
212
|
+
The authentication for this endpoint (default is public)
|
|
213
|
+
|
|
214
|
+
Use this to attach an instance of `clearskies.authentication.Authentication` to an endpoint, which enforces authentication.
|
|
215
|
+
For more details, see the dedicated documentation section on authentication and authorization. By default, all endpoints are public.
|
|
216
|
+
"""
|
|
217
|
+
authentication = clearskies.configs.Authentication(default=Public())
|
|
218
|
+
|
|
219
|
+
"""
|
|
220
|
+
The authorization rules for this endpoint
|
|
221
|
+
|
|
222
|
+
Use this to attach an instance of `clearskies.authentication.Authorization` to an endpoint, which enforces authorization.
|
|
223
|
+
For more details, see the dedicated documentation section on authentication and authorization. By default, no authorization is enforced.
|
|
224
|
+
"""
|
|
225
|
+
authorization = clearskies.configs.Authorization(default=Authorization())
|
|
226
|
+
|
|
227
|
+
"""
|
|
228
|
+
An override of the default model-to-json mapping for endpoints that auto-convert models to json.
|
|
229
|
+
|
|
230
|
+
Many endpoints allow you to return a model which is then automatically converted into a JSON response. When this is the case,
|
|
231
|
+
you can provide a callable in the `output_map` parameter which will be called instead of following the usual method for
|
|
232
|
+
JSON conversion. Note that if you use this method, you should also specify `output_schema`, which the autodocumentation
|
|
233
|
+
will then use to document the endpoint.
|
|
234
|
+
|
|
235
|
+
Your function can request any named dependency injection parameter as well as the standard context parameters for the request.
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
import clearskies
|
|
239
|
+
import datetime
|
|
240
|
+
from dateutil.relativedelta import relativedelta
|
|
241
|
+
|
|
242
|
+
class User(clearskies.Model):
|
|
243
|
+
id_column_name = "id"
|
|
244
|
+
backend = clearskies.backends.MemoryBackend()
|
|
245
|
+
id = clearskies.columns.Uuid()
|
|
246
|
+
name = clearskies.columns.String()
|
|
247
|
+
dob = clearskies.columns.Datetime()
|
|
248
|
+
|
|
249
|
+
class UserResponse(clearskies.Schema):
|
|
250
|
+
id = clearskies.columns.String()
|
|
251
|
+
name = clearskies.columns.String()
|
|
252
|
+
age = clearskies.columns.Integer()
|
|
253
|
+
is_special = clearskies.columns.Boolean()
|
|
254
|
+
|
|
255
|
+
def user_to_json(model: User, utcnow: datetime.datetime, special_person: str):
|
|
256
|
+
return {
|
|
257
|
+
"id": model.id,
|
|
258
|
+
"name": model.name,
|
|
259
|
+
"age": relativedelta(utcnow, model.dob).years,
|
|
260
|
+
"is_special": model.name.lower() == special_person.lower(),
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
list_users = clearskies.endpoints.List(
|
|
264
|
+
model_class=User,
|
|
265
|
+
url="/{special_person}",
|
|
266
|
+
output_map = user_to_json,
|
|
267
|
+
output_schema = UserResponse,
|
|
268
|
+
readable_column_names=["id", "name"],
|
|
269
|
+
sortable_column_names=["id", "name", "dob"],
|
|
270
|
+
default_sort_column_name="dob",
|
|
271
|
+
default_sort_direction="DESC",
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
wsgi = clearskies.contexts.WsgiRef(
|
|
275
|
+
list_users,
|
|
276
|
+
classes=[User],
|
|
277
|
+
bindings={
|
|
278
|
+
"special_person": "jane",
|
|
279
|
+
"memory_backend_default_data": [
|
|
280
|
+
{
|
|
281
|
+
"model_class": User,
|
|
282
|
+
"records": [
|
|
283
|
+
{"id": "1-2-3-4", "name": "Bob", "dob": datetime.datetime(1990, 1, 1)},
|
|
284
|
+
{"id": "1-2-3-5", "name": "Jane", "dob": datetime.datetime(2020, 1, 1)},
|
|
285
|
+
{"id": "1-2-3-6", "name": "Greg", "dob": datetime.datetime(1980, 1, 1)},
|
|
286
|
+
]
|
|
287
|
+
},
|
|
288
|
+
]
|
|
289
|
+
}
|
|
290
|
+
)
|
|
291
|
+
wsgi()
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Which gives:
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
$ curl 'http://localhost:8080/jane' | jq
|
|
298
|
+
{
|
|
299
|
+
"status": "success",
|
|
300
|
+
"error": "",
|
|
301
|
+
"data": [
|
|
302
|
+
{
|
|
303
|
+
"id": "1-2-3-5",
|
|
304
|
+
"name": "Jane",
|
|
305
|
+
"age": 5,
|
|
306
|
+
"is_special": true
|
|
307
|
+
}
|
|
308
|
+
{
|
|
309
|
+
"id": "1-2-3-4",
|
|
310
|
+
"name": "Bob",
|
|
311
|
+
"age": 35,
|
|
312
|
+
"is_special": false
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
"id": "1-2-3-6",
|
|
316
|
+
"name": "Greg",
|
|
317
|
+
"age": 45,
|
|
318
|
+
"is_special": false
|
|
319
|
+
},
|
|
320
|
+
],
|
|
321
|
+
"pagination": {
|
|
322
|
+
"number_results": 3,
|
|
323
|
+
"limit": 50,
|
|
324
|
+
"next_page": {}
|
|
325
|
+
},
|
|
326
|
+
"input_errors": {}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
"""
|
|
332
|
+
output_map = clearskies.configs.Callable(default=None)
|
|
333
|
+
|
|
334
|
+
"""
|
|
335
|
+
A schema that describes the expected output to the client.
|
|
336
|
+
|
|
337
|
+
This is used to build the auto-documentation. See the documentation for clearskies.endpoint.output_map for examples.
|
|
338
|
+
Note that this is typically not required - when returning models and relying on clearskies to auto-convert to JSON,
|
|
339
|
+
it will also automatically generate your documentation.
|
|
340
|
+
"""
|
|
341
|
+
output_schema = clearskies.configs.Schema(default=None)
|
|
342
|
+
|
|
343
|
+
"""
|
|
344
|
+
The model class used by this endpoint.
|
|
345
|
+
|
|
346
|
+
The endpoint will use this to fetch/save/validate incoming data as needed.
|
|
347
|
+
"""
|
|
348
|
+
model_class = clearskies.configs.ModelClass(default=None)
|
|
349
|
+
|
|
350
|
+
"""
|
|
351
|
+
Columns from the model class that should be returned to the client.
|
|
352
|
+
|
|
353
|
+
Most endpoints use a model to build the return response to the user. In this case, `readable_column_names`
|
|
354
|
+
instructs the model what columns should be sent back to the user. This information is similarly used when generating
|
|
355
|
+
the documentation for the endpoint.
|
|
356
|
+
|
|
357
|
+
```python
|
|
358
|
+
import clearskies
|
|
359
|
+
|
|
360
|
+
class User(clearskies.Model):
|
|
361
|
+
id_column_name = "id"
|
|
362
|
+
backend = clearskies.backends.MemoryBackend()
|
|
363
|
+
id = clearskies.columns.Uuid()
|
|
364
|
+
name = clearskies.columns.String()
|
|
365
|
+
secret = clearskies.columns.String()
|
|
366
|
+
|
|
367
|
+
list_users = clearskies.endpoints.List(
|
|
368
|
+
model_class=User,
|
|
369
|
+
readable_column_names=["id", "name"],
|
|
370
|
+
sortable_column_names=["id", "name"],
|
|
371
|
+
default_sort_column_name="name",
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
wsgi = clearskies.contexts.WsgiRef(
|
|
375
|
+
list_users,
|
|
376
|
+
classes=[User],
|
|
377
|
+
bindings={
|
|
378
|
+
"memory_backend_default_data": [
|
|
379
|
+
{
|
|
380
|
+
"model_class": User,
|
|
381
|
+
"records": [
|
|
382
|
+
{"id": "1-2-3-4", "name": "Bob", "secret": "Awesome dude"},
|
|
383
|
+
{"id": "1-2-3-5", "name": "Jane", "secret": "Gets things done"},
|
|
384
|
+
{"id": "1-2-3-6", "name": "Greg", "secret": "Loves chocolate"},
|
|
385
|
+
]
|
|
386
|
+
},
|
|
387
|
+
]
|
|
388
|
+
}
|
|
389
|
+
)
|
|
390
|
+
wsgi()
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
And then:
|
|
394
|
+
|
|
395
|
+
```bash
|
|
396
|
+
$ curl 'http://localhost:8080'
|
|
397
|
+
{
|
|
398
|
+
"status": "success",
|
|
399
|
+
"error": "",
|
|
400
|
+
"data": [
|
|
401
|
+
{
|
|
402
|
+
"id": "1-2-3-4",
|
|
403
|
+
"name": "Bob"
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
"id": "1-2-3-6",
|
|
407
|
+
"name": "Greg"
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
"id": "1-2-3-5",
|
|
411
|
+
"name": "Jane"
|
|
412
|
+
}
|
|
413
|
+
],
|
|
414
|
+
"pagination": {
|
|
415
|
+
"number_results": 3,
|
|
416
|
+
"limit": 50,
|
|
417
|
+
"next_page": {}
|
|
418
|
+
},
|
|
419
|
+
"input_errors": {}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
```
|
|
423
|
+
"""
|
|
424
|
+
readable_column_names = clearskies.configs.ReadableModelColumns("model_class", default=[])
|
|
425
|
+
|
|
426
|
+
"""
|
|
427
|
+
Specifies which columns from a model class can be set by the client.
|
|
428
|
+
|
|
429
|
+
Many endpoints allow or require input from the client. The most common way to provide input validation
|
|
430
|
+
is by setting the model class and using `writeable_column_names` to specify which columns the end client can
|
|
431
|
+
set. Clearskies will then use the model schema to validate the input and also auto-generate documentation
|
|
432
|
+
for the endpoint.
|
|
433
|
+
|
|
434
|
+
```python
|
|
435
|
+
import clearskies
|
|
436
|
+
|
|
437
|
+
class User(clearskies.Model):
|
|
438
|
+
id_column_name = "id"
|
|
439
|
+
backend = clearskies.backends.MemoryBackend()
|
|
440
|
+
id = clearskies.columns.Uuid()
|
|
441
|
+
name = clearskies.columns.String(validators=[clearskies.validators.Required()])
|
|
442
|
+
date_of_birth = clearskies.columns.Date()
|
|
443
|
+
|
|
444
|
+
send_user = clearskies.endpoints.Callable(
|
|
445
|
+
lambda request_data: request_data,
|
|
446
|
+
request_methods=["GET","POST"],
|
|
447
|
+
writeable_column_names=["name", "date_of_birth"],
|
|
448
|
+
model_class=User,
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
wsgi = clearskies.contexts.WsgiRef(send_user)
|
|
452
|
+
wsgi()
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
If we send a valid payload:
|
|
456
|
+
|
|
457
|
+
```bash
|
|
458
|
+
$ curl 'http://localhost:8080' -d '{"name":"Jane","date_of_birth":"01/01/1990"}' | jq
|
|
459
|
+
{
|
|
460
|
+
"status": "success",
|
|
461
|
+
"error": "",
|
|
462
|
+
"data": {
|
|
463
|
+
"name": "Jane",
|
|
464
|
+
"date_of_birth": "01/01/1990"
|
|
465
|
+
},
|
|
466
|
+
"pagination": {},
|
|
467
|
+
"input_errors": {}
|
|
468
|
+
}
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
And we can see the automatic input validation by sending some incorrect data:
|
|
472
|
+
|
|
473
|
+
```bash
|
|
474
|
+
$ curl 'http://localhost:8080' -d '{"name":"","date_of_birth":"this is not a date","id":"hey"}' | jq
|
|
475
|
+
{
|
|
476
|
+
"status": "input_errors",
|
|
477
|
+
"error": "",
|
|
478
|
+
"data": [],
|
|
479
|
+
"pagination": {},
|
|
480
|
+
"input_errors": {
|
|
481
|
+
"name": "'name' is required.",
|
|
482
|
+
"date_of_birth": "given value did not appear to be a valid date",
|
|
483
|
+
"other_column": "Input column other_column is not an allowed input column."
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
"""
|
|
489
|
+
writeable_column_names = clearskies.configs.WriteableModelColumns("model_class", default=[])
|
|
490
|
+
|
|
491
|
+
"""
|
|
492
|
+
Columns from the model class that can be searched by the client.
|
|
493
|
+
|
|
494
|
+
Sets which columns the client is allowed to search (for endpoints that support searching).
|
|
495
|
+
"""
|
|
496
|
+
searchable_column_names = clearskies.configs.SearchableModelColumns("model_class", default=[])
|
|
497
|
+
|
|
498
|
+
"""
|
|
499
|
+
A function to call to add custom input validation logic.
|
|
500
|
+
|
|
501
|
+
Typically, input validation happens by choosing the appropriate column in your schema and adding validators where necessary. You
|
|
502
|
+
can also create custom columns with their own input validation logic. However, if desired, endpoints that accept user input also
|
|
503
|
+
allow you to add callables for custom validation logic. These functions should return a dictionary where the key name
|
|
504
|
+
represents the name of the column that has invalid input, and the value is a human-readable error message. If no input errors are
|
|
505
|
+
found, then the callable should return an empty dictionary. As usual, the callable can request any standard dependencies configured
|
|
506
|
+
in the dependency injection container or proivded by input_output.get_context_for_callables.
|
|
507
|
+
|
|
508
|
+
Note that most endpoints (such as Create and Update) explicitly require input. As a result, if a request comes in without input
|
|
509
|
+
from the end user, it will be rejected before calling your input validator. In these cases you can depend on request_data always
|
|
510
|
+
being a dictionary. The Callable endpoint, however, only requires input if `writeable_column_names` is set. If it's not set,
|
|
511
|
+
and the end-user doesn't provide a request body, then request_data will be None.
|
|
512
|
+
|
|
513
|
+
```python
|
|
514
|
+
import clearskies
|
|
515
|
+
|
|
516
|
+
def check_input(request_data):
|
|
517
|
+
if not request_data:
|
|
518
|
+
return {}
|
|
519
|
+
if request_data.get("name"):
|
|
520
|
+
return {"name":"This is a privacy-preserving system, so please don't tell us your name"}
|
|
521
|
+
return {}
|
|
522
|
+
|
|
523
|
+
send_user = clearskies.endpoints.Callable(
|
|
524
|
+
lambda request_data: request_data,
|
|
525
|
+
request_methods=["GET", "POST"],
|
|
526
|
+
input_validation_callable=check_input,
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
wsgi = clearskies.contexts.WsgiRef(send_user)
|
|
530
|
+
wsgi()
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
And when invoked:
|
|
534
|
+
|
|
535
|
+
```bash
|
|
536
|
+
$ curl http://localhost:8080 -d '{"name":"sup"}' | jq
|
|
537
|
+
{
|
|
538
|
+
"status": "input_errors",
|
|
539
|
+
"error": "",
|
|
540
|
+
"data": [],
|
|
541
|
+
"pagination": {},
|
|
542
|
+
"input_errors": {
|
|
543
|
+
"name": "This is a privacy-preserving system, so please don't tell us your name"
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
$ curl http://localhost:8080 -d '{"hello":"world"}' | jq
|
|
548
|
+
{
|
|
549
|
+
"status": "success",
|
|
550
|
+
"error": "",
|
|
551
|
+
"data": {
|
|
552
|
+
"hello": "world"
|
|
553
|
+
},
|
|
554
|
+
"pagination": {},
|
|
555
|
+
"input_errors": {}
|
|
556
|
+
}
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
"""
|
|
560
|
+
input_validation_callable = clearskies.configs.Callable(default=None)
|
|
561
|
+
|
|
562
|
+
"""
|
|
563
|
+
A dictionary with columns that should override columns in the model.
|
|
564
|
+
|
|
565
|
+
This is typically used to change column definitions on specific endpoints to adjust behavior: for intstance a model might use a `created_by_*`
|
|
566
|
+
column to auto-populate some data, but an admin endpoint may need to override that behavior so the user can set it directly.
|
|
567
|
+
|
|
568
|
+
This should be a dictionary with the column name as a key and the column itself as the value. Note that you cannot use this to remove
|
|
569
|
+
columns from the model. In general, if you want a column not to be exposed through an endpoint, then all you have to do is remove
|
|
570
|
+
that column from the list of writeable columns.
|
|
571
|
+
|
|
572
|
+
```python
|
|
573
|
+
import clearskies
|
|
574
|
+
|
|
575
|
+
endpoint = clearskies.Endpoint(
|
|
576
|
+
column_overrides = {
|
|
577
|
+
"name": clearskies.columns.String(validators=clearskies.validators.Required()),
|
|
578
|
+
}
|
|
579
|
+
)
|
|
580
|
+
```
|
|
581
|
+
"""
|
|
582
|
+
column_overrides = clearskies.configs.Columns(default={})
|
|
583
|
+
|
|
584
|
+
"""
|
|
585
|
+
Used in conjunction with external_casing to change the casing of the key names in the outputted JSON of the endpoint.
|
|
586
|
+
|
|
587
|
+
To use these, set internal_casing to the casing scheme used in your model, and then set external_casing to the casing
|
|
588
|
+
scheme you want for your API endpoints. clearskies will then automatically convert all output key names accordingly.
|
|
589
|
+
Note that for callables, this only works when you return a model and set `readable_columns`. If you set `writeable_columns`,
|
|
590
|
+
it will also map the incoming data.
|
|
591
|
+
|
|
592
|
+
The allowed casing schemas are:
|
|
593
|
+
|
|
594
|
+
1. `snake_case`
|
|
595
|
+
2. `camelCase`
|
|
596
|
+
3. `TitleCase`
|
|
597
|
+
|
|
598
|
+
By default internal_casing and external_casing are both set to 'snake_case', which means that no conversion happens.
|
|
599
|
+
|
|
600
|
+
```python
|
|
601
|
+
import clearskies
|
|
602
|
+
import datetime
|
|
603
|
+
|
|
604
|
+
class User(clearskies.Model):
|
|
605
|
+
id_column_name = "id"
|
|
606
|
+
backend = clearskies.backends.MemoryBackend()
|
|
607
|
+
id = clearskies.columns.Uuid()
|
|
608
|
+
name = clearskies.columns.String()
|
|
609
|
+
date_of_birth = clearskies.columns.Date()
|
|
610
|
+
|
|
611
|
+
send_user = clearskies.endpoints.Callable(
|
|
612
|
+
lambda users: users.create({"name":"Example","date_of_birth": datetime.datetime(2050, 1, 15)}),
|
|
613
|
+
readable_column_names=["name", "date_of_birth"],
|
|
614
|
+
internal_casing="snake_case",
|
|
615
|
+
external_casing="TitleCase",
|
|
616
|
+
model_class=User,
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
# because we're using name-based injection in our lambda callable (instead of type hinting) we have to explicitly
|
|
620
|
+
# add the user model to the dependency injection container
|
|
621
|
+
wsgi = clearskies.contexts.WsgiRef(send_user, classes=[User])
|
|
622
|
+
wsgi()
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
And then when called:
|
|
626
|
+
|
|
627
|
+
```bash
|
|
628
|
+
$ curl http://localhost:8080 | jq
|
|
629
|
+
{
|
|
630
|
+
"Status": "Success",
|
|
631
|
+
"Error": "",
|
|
632
|
+
"Data": {
|
|
633
|
+
"Name": "Example",
|
|
634
|
+
"DateOfBirth": "2050-01-15"
|
|
635
|
+
},
|
|
636
|
+
"Pagination": {},
|
|
637
|
+
"InputErrors": {}
|
|
638
|
+
}
|
|
639
|
+
```
|
|
640
|
+
"""
|
|
641
|
+
internal_casing = clearskies.configs.Select(["snake_case", "camelCase", "TitleCase"], default="snake_case")
|
|
642
|
+
|
|
643
|
+
"""
|
|
644
|
+
Used in conjunction with internal_casing to change the casing of the key names in the outputted JSON of the endpoint.
|
|
645
|
+
|
|
646
|
+
See the docs for `internal_casing` for more details and usage examples.
|
|
647
|
+
"""
|
|
648
|
+
external_casing = clearskies.configs.Select(["snake_case", "camelCase", "TitleCase"], default="snake_case")
|
|
649
|
+
|
|
650
|
+
"""
|
|
651
|
+
Configure standard security headers to be sent along in the response from this endpoint.
|
|
652
|
+
|
|
653
|
+
Note that, with CORS, you generally only have to specify the origin. The routing system will automatically add
|
|
654
|
+
in the appropriate HTTP verbs, and the authorization classes will add in the appropriate headers.
|
|
655
|
+
|
|
656
|
+
```python
|
|
657
|
+
import clearskies
|
|
658
|
+
|
|
659
|
+
hello_world = clearskies.endpoints.Callable(
|
|
660
|
+
lambda: {"hello": "world"},
|
|
661
|
+
request_methods=["PATCH", "POST"],
|
|
662
|
+
authentication=clearskies.authentication.SecretBearer(environment_key="MY_SECRET"),
|
|
663
|
+
security_headers=[
|
|
664
|
+
clearskies.security_headers.Hsts(),
|
|
665
|
+
clearskies.security_headers.Cors(origin="https://example.com"),
|
|
666
|
+
],
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
wsgi = clearskies.contexts.WsgiRef(hello_world)
|
|
670
|
+
wsgi()
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
And then execute the options endpoint to see all the security headers:
|
|
674
|
+
|
|
675
|
+
```bash
|
|
676
|
+
$ curl -v http://localhost:8080 -X OPTIONS
|
|
677
|
+
* Host localhost:8080 was resolved.
|
|
678
|
+
< HTTP/1.0 200 Ok
|
|
679
|
+
< Server: WSGIServer/0.2 CPython/3.11.6
|
|
680
|
+
< ACCESS-CONTROL-ALLOW-METHODS: PATCH, POST
|
|
681
|
+
< ACCESS-CONTROL-ALLOW-HEADERS: Authorization
|
|
682
|
+
< ACCESS-CONTROL-MAX-AGE: 5
|
|
683
|
+
< ACCESS-CONTROL-ALLOW-ORIGIN: https://example.com
|
|
684
|
+
< STRICT-TRANSPORT-SECURITY: max-age=31536000 ;
|
|
685
|
+
< CONTENT-TYPE: application/json; charset=UTF-8
|
|
686
|
+
< Content-Length: 0
|
|
687
|
+
<
|
|
688
|
+
* Closing connection
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
"""
|
|
692
|
+
security_headers = clearskies.configs.SecurityHeaders(default=[])
|
|
693
|
+
|
|
694
|
+
"""
|
|
695
|
+
A description for this endpoint. This is added to any auto-documentation
|
|
696
|
+
"""
|
|
697
|
+
description = clearskies.configs.String(default="")
|
|
698
|
+
|
|
699
|
+
"""
|
|
700
|
+
Whether or not the routing data should also be persisted to the model. Defaults to False.
|
|
701
|
+
|
|
702
|
+
Note: this is only relevant for handlers that accept request data
|
|
703
|
+
"""
|
|
704
|
+
include_routing_data_in_request_data = clearskies.configs.Boolean(default=False)
|
|
705
|
+
|
|
706
|
+
"""
|
|
707
|
+
Additional conditions to always add to the results.
|
|
708
|
+
|
|
709
|
+
where should be a single item or a list of items containing one of three things:
|
|
710
|
+
|
|
711
|
+
1. Conditions expressed as a string (e.g. `"name=example"`, `"age>5"`)
|
|
712
|
+
2. Queries built with a column (e.g. `SomeModel.name.equals("example")`, `SomeModel.age.greater_than(5)`)
|
|
713
|
+
3. A callable which accepts and returns the mode (e.g. `lambda model: model.where("name=example")`)
|
|
714
|
+
|
|
715
|
+
Here's an example:
|
|
716
|
+
|
|
717
|
+
```python
|
|
718
|
+
import clearskies
|
|
719
|
+
|
|
720
|
+
class Student(clearskies.Model):
|
|
721
|
+
backend = clearskies.backends.MemoryBackend()
|
|
722
|
+
id_column_name = "id"
|
|
723
|
+
|
|
724
|
+
id = clearskies.columns.Uuid()
|
|
725
|
+
name = clearskies.columns.String()
|
|
726
|
+
grade = clearskies.columns.Integer()
|
|
727
|
+
will_graduate = clearskies.columns.Boolean()
|
|
728
|
+
|
|
729
|
+
wsgi = clearskies.contexts.WsgiRef(
|
|
730
|
+
clearskies.endpoints.List(
|
|
731
|
+
Student,
|
|
732
|
+
readable_column_names=["id", "name", "grade"],
|
|
733
|
+
sortable_column_names=["name", "grade"],
|
|
734
|
+
default_sort_column_name="name",
|
|
735
|
+
where=["grade<10", Student.will_graduate.equals(True)],
|
|
736
|
+
),
|
|
737
|
+
bindings={
|
|
738
|
+
"memory_backend_default_data": [
|
|
739
|
+
{
|
|
740
|
+
"model_class": Student,
|
|
741
|
+
"records": [
|
|
742
|
+
{"id": "1-2-3-4", "name": "Bob", "grade": 5, "will_graduate": True},
|
|
743
|
+
{"id": "1-2-3-5", "name": "Jane", "grade": 3, "will_graduate": True},
|
|
744
|
+
{"id": "1-2-3-6", "name": "Greg", "grade": 3, "will_graduate": False},
|
|
745
|
+
{"id": "1-2-3-7", "name": "Bob", "grade": 2, "will_graduate": True},
|
|
746
|
+
{"id": "1-2-3-8", "name": "Ann", "grade": 12, "will_graduate": True},
|
|
747
|
+
],
|
|
748
|
+
},
|
|
749
|
+
],
|
|
750
|
+
},
|
|
751
|
+
)
|
|
752
|
+
wsgi()
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
Which you can invoke:
|
|
756
|
+
|
|
757
|
+
```bash
|
|
758
|
+
$ curl 'http://localhost:8080/' | jq
|
|
759
|
+
{
|
|
760
|
+
"status": "success",
|
|
761
|
+
"error": "",
|
|
762
|
+
"data": [
|
|
763
|
+
{
|
|
764
|
+
"id": "1-2-3-4",
|
|
765
|
+
"name": "Bob",
|
|
766
|
+
"grade": 5
|
|
767
|
+
},
|
|
768
|
+
{
|
|
769
|
+
"id": "1-2-3-7",
|
|
770
|
+
"name": "Bob",
|
|
771
|
+
"grade": 2
|
|
772
|
+
},
|
|
773
|
+
{
|
|
774
|
+
"id": "1-2-3-5",
|
|
775
|
+
"name": "Jane",
|
|
776
|
+
"grade": 3
|
|
777
|
+
}
|
|
778
|
+
],
|
|
779
|
+
"pagination": {},
|
|
780
|
+
"input_errors": {}
|
|
781
|
+
}
|
|
782
|
+
```
|
|
783
|
+
and note that neither Greg nor Ann are returned. Ann because she doesn't make the grade criteria, and Greg because
|
|
784
|
+
he won't graduate.
|
|
785
|
+
"""
|
|
786
|
+
where = clearskies.configs.Conditions(default=[])
|
|
787
|
+
|
|
788
|
+
"""
|
|
789
|
+
Additional joins to always add to the query.
|
|
790
|
+
|
|
791
|
+
```python
|
|
792
|
+
import clearskies
|
|
793
|
+
|
|
794
|
+
class Student(clearskies.Model):
|
|
795
|
+
backend = clearskies.backends.MemoryBackend()
|
|
796
|
+
id_column_name = "id"
|
|
797
|
+
|
|
798
|
+
id = clearskies.columns.Uuid()
|
|
799
|
+
name = clearskies.columns.String()
|
|
800
|
+
grade = clearskies.columns.Integer()
|
|
801
|
+
will_graduate = clearskies.columns.Boolean()
|
|
802
|
+
|
|
803
|
+
class PastRecord(clearskies.Model):
|
|
804
|
+
backend = clearskies.backends.MemoryBackend()
|
|
805
|
+
id_column_name = "id"
|
|
806
|
+
|
|
807
|
+
id = clearskies.columns.Uuid()
|
|
808
|
+
student_id = clearskies.columns.BelongsToId(Student)
|
|
809
|
+
school_name = clearskies.columns.String()
|
|
810
|
+
|
|
811
|
+
wsgi = clearskies.contexts.WsgiRef(
|
|
812
|
+
clearskies.endpoints.List(
|
|
813
|
+
Student,
|
|
814
|
+
readable_column_names=["id", "name", "grade"],
|
|
815
|
+
sortable_column_names=["name", "grade"],
|
|
816
|
+
default_sort_column_name="name",
|
|
817
|
+
joins=["INNER JOIN past_records ON past_records.student_id=students.id"],
|
|
818
|
+
),
|
|
819
|
+
bindings={
|
|
820
|
+
"memory_backend_default_data": [
|
|
821
|
+
{
|
|
822
|
+
"model_class": Student,
|
|
823
|
+
"records": [
|
|
824
|
+
{"id": "1-2-3-4", "name": "Bob", "grade": 5, "will_graduate": True},
|
|
825
|
+
{"id": "1-2-3-5", "name": "Jane", "grade": 3, "will_graduate": True},
|
|
826
|
+
{"id": "1-2-3-6", "name": "Greg", "grade": 3, "will_graduate": False},
|
|
827
|
+
{"id": "1-2-3-7", "name": "Bob", "grade": 2, "will_graduate": True},
|
|
828
|
+
{"id": "1-2-3-8", "name": "Ann", "grade": 12, "will_graduate": True},
|
|
829
|
+
],
|
|
830
|
+
},
|
|
831
|
+
{
|
|
832
|
+
"model_class": PastRecord,
|
|
833
|
+
"records": [
|
|
834
|
+
{"id": "5-2-3-4", "student_id": "1-2-3-4", "school_name": "Best Academy"},
|
|
835
|
+
{"id": "5-2-3-5", "student_id": "1-2-3-5", "school_name": "Awesome School"},
|
|
836
|
+
],
|
|
837
|
+
},
|
|
838
|
+
],
|
|
839
|
+
},
|
|
840
|
+
)
|
|
841
|
+
wsgi()
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
Which when invoked:
|
|
845
|
+
|
|
846
|
+
```bash
|
|
847
|
+
$ curl 'http://localhost:8080/' | jq
|
|
848
|
+
{
|
|
849
|
+
"status": "success",
|
|
850
|
+
"error": "",
|
|
851
|
+
"data": [
|
|
852
|
+
{
|
|
853
|
+
"id": "1-2-3-4",
|
|
854
|
+
"name": "Bob",
|
|
855
|
+
"grade": 5
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
"id": "1-2-3-5",
|
|
859
|
+
"name": "Jane",
|
|
860
|
+
"grade": 3
|
|
861
|
+
}
|
|
862
|
+
],
|
|
863
|
+
"pagination": {},
|
|
864
|
+
"input_errors": {}
|
|
865
|
+
}
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
e.g., the inner join reomves all the students that don't have an entry in the PastRecord model.
|
|
869
|
+
|
|
870
|
+
"""
|
|
871
|
+
joins = clearskies.configs.Joins(default=[])
|
|
872
|
+
|
|
873
|
+
cors_header: Cors = None # type: ignore
|
|
874
|
+
_model: clearskies.model.Model = None # type: ignore
|
|
875
|
+
_columns: dict[str, clearskies.column.Column] = None # type: ignore
|
|
876
|
+
_readable_columns: dict[str, clearskies.column.Column] = None # type: ignore
|
|
877
|
+
_writeable_columns: dict[str, clearskies.column.Column] = None # type: ignore
|
|
878
|
+
_searchable_columns: dict[str, clearskies.column.Column] = None # type: ignore
|
|
879
|
+
_sortable_columns: dict[str, clearskies.column.Column] = None # type: ignore
|
|
880
|
+
_as_json_map: dict[str, clearskies.column.Column] = None # type: ignore
|
|
881
|
+
|
|
882
|
+
@clearskies.decorators.parameters_to_properties
|
|
883
|
+
def __init__(
|
|
884
|
+
self,
|
|
885
|
+
url: str = "",
|
|
886
|
+
request_methods: list[str] = ["GET"],
|
|
887
|
+
response_headers: list[str | Callable[..., list[str]]] = [],
|
|
888
|
+
output_map: Callable[..., dict[str, Any]] | None = None,
|
|
889
|
+
column_overrides: dict[str, Column] = {},
|
|
890
|
+
internal_casing: str = "snake_case",
|
|
891
|
+
external_casing: str = "snake_case",
|
|
892
|
+
security_headers: list[SecurityHeader] = [],
|
|
893
|
+
description: str = "",
|
|
894
|
+
authentication: Authentication = Public(),
|
|
895
|
+
authorization: Authorization = Authorization(),
|
|
896
|
+
):
|
|
897
|
+
self.finalize_and_validate_configuration()
|
|
898
|
+
for security_header in self.security_headers:
|
|
899
|
+
if not security_header.is_cors:
|
|
900
|
+
continue
|
|
901
|
+
self.cors_header = security_header # type: ignore
|
|
902
|
+
self.has_cors = True
|
|
903
|
+
break
|
|
904
|
+
|
|
905
|
+
@property
|
|
906
|
+
def model(self) -> Model:
|
|
907
|
+
if self._model is None:
|
|
908
|
+
self._model = self.di.build(self.model_class)
|
|
909
|
+
return self._model
|
|
910
|
+
|
|
911
|
+
@property
|
|
912
|
+
def columns(self) -> dict[str, Column]:
|
|
913
|
+
if self._columns is None:
|
|
914
|
+
self._columns = self.model.get_columns()
|
|
915
|
+
return self._columns
|
|
916
|
+
|
|
917
|
+
@property
|
|
918
|
+
def readable_columns(self) -> dict[str, Column]:
|
|
919
|
+
if self._readable_columns is None:
|
|
920
|
+
self._readable_columns = {name: self.columns[name] for name in self.readable_column_names}
|
|
921
|
+
return self._readable_columns
|
|
922
|
+
|
|
923
|
+
@property
|
|
924
|
+
def writeable_columns(self) -> dict[str, Column]:
|
|
925
|
+
if self._writeable_columns is None:
|
|
926
|
+
self._writeable_columns = {name: self.columns[name] for name in self.writeable_column_names}
|
|
927
|
+
return self._writeable_columns
|
|
928
|
+
|
|
929
|
+
@property
|
|
930
|
+
def searchable_columns(self) -> dict[str, Column]:
|
|
931
|
+
if self._searchable_columns is None:
|
|
932
|
+
self._searchable_columns = {name: self._columns[name] for name in self.sortable_column_names}
|
|
933
|
+
return self._searchable_columns
|
|
934
|
+
|
|
935
|
+
@property
|
|
936
|
+
def sortable_columns(self) -> dict[str, Column]:
|
|
937
|
+
if self._sortable_columns is None:
|
|
938
|
+
self._sortable_columns = {name: self._columns[name] for name in self.sortable_column_names}
|
|
939
|
+
return self._sortable_columns
|
|
940
|
+
|
|
941
|
+
def get_request_data(self, input_output: InputOutput, required=True) -> dict[str, Any]:
|
|
942
|
+
if not input_output.request_data:
|
|
943
|
+
if input_output.has_body():
|
|
944
|
+
raise exceptions.ClientError("Request body was not valid JSON")
|
|
945
|
+
raise exceptions.ClientError("Missing required JSON body")
|
|
946
|
+
if not isinstance(input_output.request_data, dict):
|
|
947
|
+
raise exceptions.ClientError("Request body was not a JSON dictionary.")
|
|
948
|
+
|
|
949
|
+
return {
|
|
950
|
+
**input_output.request_data, # type: ignore
|
|
951
|
+
**(input_output.routing_data if self.include_routing_data_in_request_data else {}),
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
def fetch_model_with_base_query(self, input_output: InputOutput) -> Model:
|
|
955
|
+
model = self.model
|
|
956
|
+
for join in self.joins:
|
|
957
|
+
if callable(join):
|
|
958
|
+
model = self.di.call_function(join, model=model, **input_output.get_context_for_callables())
|
|
959
|
+
else:
|
|
960
|
+
model = model.join(join)
|
|
961
|
+
for where in self.where:
|
|
962
|
+
if callable(where):
|
|
963
|
+
model = self.di.call_function(where, model=model, **input_output.get_context_for_callables())
|
|
964
|
+
else:
|
|
965
|
+
model = model.where(where)
|
|
966
|
+
model = model.where_for_request_all(
|
|
967
|
+
model,
|
|
968
|
+
input_output,
|
|
969
|
+
input_output.routing_data,
|
|
970
|
+
input_output.authorization_data,
|
|
971
|
+
overrides=self.column_overrides,
|
|
972
|
+
)
|
|
973
|
+
return self.authorization.filter_model(model, input_output.authorization_data, input_output)
|
|
974
|
+
|
|
975
|
+
def handle(self, input_output: InputOutput) -> Any:
|
|
976
|
+
raise NotImplementedError()
|
|
977
|
+
|
|
978
|
+
def matches_request(self, input_output: InputOutput, allow_partial=False) -> bool:
|
|
979
|
+
"""Whether or not we can handle an incoming request based on URL and request method."""
|
|
980
|
+
# soo..... this excessively duplicates the logic in __call__, but I'm being lazy right now
|
|
981
|
+
# and not fixing it.
|
|
982
|
+
request_method = input_output.get_request_method().upper()
|
|
983
|
+
if request_method == "OPTIONS":
|
|
984
|
+
return True
|
|
985
|
+
if request_method not in self.request_methods:
|
|
986
|
+
return False
|
|
987
|
+
expected_url = self.url.strip("/")
|
|
988
|
+
incoming_url = input_output.get_full_path().strip("/")
|
|
989
|
+
if not expected_url and not incoming_url:
|
|
990
|
+
return True
|
|
991
|
+
|
|
992
|
+
matches, routing_data = routing.match_route(expected_url, incoming_url, allow_partial=allow_partial)
|
|
993
|
+
return matches
|
|
994
|
+
|
|
995
|
+
def populate_routing_data(self, input_output: InputOutput) -> Any:
|
|
996
|
+
# matches_request is only checked by the endpoint group, not by the context. As a result, we need to check our
|
|
997
|
+
# route. However we always have to check our route anyway because the full routing data can only be figured
|
|
998
|
+
# out at the endpoint level, so calling out to routing.mattch_route is unavoidable.
|
|
999
|
+
request_method = input_output.get_request_method().upper()
|
|
1000
|
+
if request_method == "OPTIONS":
|
|
1001
|
+
return self.cors(input_output)
|
|
1002
|
+
if request_method not in self.request_methods:
|
|
1003
|
+
return self.error(input_output, "Not Found", 404)
|
|
1004
|
+
expected_url = self.url.strip("/")
|
|
1005
|
+
incoming_url = input_output.get_full_path().strip("/")
|
|
1006
|
+
if expected_url or incoming_url:
|
|
1007
|
+
matches, routing_data = routing.match_route(expected_url, incoming_url, allow_partial=False)
|
|
1008
|
+
if not matches:
|
|
1009
|
+
return self.error(input_output, "Not Found", 404)
|
|
1010
|
+
input_output.routing_data = routing_data
|
|
1011
|
+
|
|
1012
|
+
def failure(self, input_output: InputOutput) -> Any:
|
|
1013
|
+
return self.respond_json(input_output, {"status": "failure"}, 500)
|
|
1014
|
+
|
|
1015
|
+
def input_errors(self, input_output: InputOutput, errors: dict[str, str], status_code: int = 200) -> Any:
|
|
1016
|
+
"""Return input errors to the client."""
|
|
1017
|
+
return self.respond_json(input_output, {"status": "input_errors", "input_errors": errors}, status_code)
|
|
1018
|
+
|
|
1019
|
+
def error(self, input_output: InputOutput, message: str, status_code: int) -> Any:
|
|
1020
|
+
"""Return a client-side error (e.g. 400)."""
|
|
1021
|
+
return self.respond_json(input_output, {"status": "client_error", "error": message}, status_code)
|
|
1022
|
+
|
|
1023
|
+
def redirect(self, input_output: InputOutput, location: str, status_code: int) -> Any:
|
|
1024
|
+
"""Return a redirect."""
|
|
1025
|
+
input_output.response_headers.add("content-type", "text/html")
|
|
1026
|
+
input_output.response_headers.add("location", location)
|
|
1027
|
+
return self.respond(
|
|
1028
|
+
'<meta http-equiv="refresh" content="0; url=' + urllib.parse.quote(location) + '">Redirecting', status_code
|
|
1029
|
+
)
|
|
1030
|
+
|
|
1031
|
+
def success(
|
|
1032
|
+
self,
|
|
1033
|
+
input_output: InputOutput,
|
|
1034
|
+
data: dict[str, Any] | list[Any],
|
|
1035
|
+
number_results: int | None = None,
|
|
1036
|
+
limit: int | None = None,
|
|
1037
|
+
next_page: Any = None,
|
|
1038
|
+
) -> Any:
|
|
1039
|
+
"""Return a successful response."""
|
|
1040
|
+
response_data = {"status": "success", "data": data, "pagination": {}}
|
|
1041
|
+
|
|
1042
|
+
if next_page or number_results:
|
|
1043
|
+
if number_results is not None:
|
|
1044
|
+
for value in [number_results, limit]:
|
|
1045
|
+
if value is not None and type(value) != int:
|
|
1046
|
+
raise ValueError("number_results and limit must all be integers")
|
|
1047
|
+
|
|
1048
|
+
response_data["pagination"] = {
|
|
1049
|
+
"number_results": number_results,
|
|
1050
|
+
"limit": limit,
|
|
1051
|
+
"next_page": next_page,
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
return self.respond_json(input_output, response_data, 200)
|
|
1055
|
+
|
|
1056
|
+
def model_as_json(self, model: clearskies.model.Model, input_output: InputOutput) -> dict[str, Any]:
|
|
1057
|
+
if self.output_map:
|
|
1058
|
+
return self.di.call_function(self.output_map, model=model, **input_output.get_context_for_callables())
|
|
1059
|
+
|
|
1060
|
+
if self._as_json_map is None:
|
|
1061
|
+
self._as_json_map = self._build_as_json_map(model)
|
|
1062
|
+
|
|
1063
|
+
json = OrderedDict()
|
|
1064
|
+
for output_name, column in self._as_json_map.items():
|
|
1065
|
+
column_data = column.to_json(model)
|
|
1066
|
+
if len(column_data) == 1:
|
|
1067
|
+
json[output_name] = list(column_data.values())[0]
|
|
1068
|
+
else:
|
|
1069
|
+
for key, value in column_data.items():
|
|
1070
|
+
json[self.auto_case_column_name(key, True)] = value
|
|
1071
|
+
return json
|
|
1072
|
+
|
|
1073
|
+
def _build_as_json_map(self, model: clearskies.model.Model) -> dict[str, clearskies.column.Column]:
|
|
1074
|
+
conversion_map = {}
|
|
1075
|
+
if not self.readable_column_names:
|
|
1076
|
+
raise ValueError(
|
|
1077
|
+
"I was asked to convert a model to JSON but I wasn't provided with `readable_column_names'"
|
|
1078
|
+
)
|
|
1079
|
+
for column in self.readable_columns.values():
|
|
1080
|
+
conversion_map[self.auto_case_column_name(column.name, True)] = column
|
|
1081
|
+
return conversion_map
|
|
1082
|
+
|
|
1083
|
+
def validate_input_against_schema(
|
|
1084
|
+
self, request_data: dict[str, Any], input_output: InputOutput, schema: Schema | type[Schema]
|
|
1085
|
+
) -> None:
|
|
1086
|
+
if not self.writeable_column_names:
|
|
1087
|
+
raise ValueError(
|
|
1088
|
+
f"I was asked to validate input against a schema, but no writeable columns are defined, so I can't :( This is probably a bug in the endpoint class - {self.__class__.__name__}."
|
|
1089
|
+
)
|
|
1090
|
+
request_data = self.map_request_data_external_to_internal(request_data)
|
|
1091
|
+
self.find_input_errors(request_data, input_output, schema)
|
|
1092
|
+
|
|
1093
|
+
def map_request_data_external_to_internal(self, request_data, required=True):
|
|
1094
|
+
# we have to map from internal names to external names, because case mapping
|
|
1095
|
+
# isn't always one-to-one, so we want to do it exactly the same way that the documentation
|
|
1096
|
+
# is built.
|
|
1097
|
+
key_map = {self.auto_case_column_name(key, True): key for key in self.writeable_column_names}
|
|
1098
|
+
|
|
1099
|
+
# and make sure we don't drop any data along the way, because the input validation
|
|
1100
|
+
# needs to return an error for unexpected data.
|
|
1101
|
+
return {key_map.get(key, key): value for (key, value) in request_data.items()}
|
|
1102
|
+
|
|
1103
|
+
def find_input_errors(
|
|
1104
|
+
self, request_data: dict[str, Any], input_output: InputOutput, schema: Schema | type[Schema]
|
|
1105
|
+
) -> None:
|
|
1106
|
+
input_errors: dict[str, str] = {}
|
|
1107
|
+
columns = schema.get_columns()
|
|
1108
|
+
model = self.di.build(schema) if inspect.isclass(schema) else schema
|
|
1109
|
+
for column_name in self.writeable_column_names:
|
|
1110
|
+
column = columns[column_name]
|
|
1111
|
+
input_errors = {
|
|
1112
|
+
**input_errors,
|
|
1113
|
+
**column.input_errors(model, request_data), # type: ignore
|
|
1114
|
+
}
|
|
1115
|
+
input_errors = {
|
|
1116
|
+
**input_errors,
|
|
1117
|
+
**self.find_input_errors_from_callable(request_data, input_output),
|
|
1118
|
+
}
|
|
1119
|
+
for extra_column_name in set(request_data.keys()) - set(self.writeable_column_names):
|
|
1120
|
+
external_column_name = self.auto_case_column_name(extra_column_name, False)
|
|
1121
|
+
input_errors[external_column_name] = f"Input column {external_column_name} is not an allowed input column."
|
|
1122
|
+
if input_errors:
|
|
1123
|
+
raise exceptions.InputErrors(input_errors)
|
|
1124
|
+
|
|
1125
|
+
def find_input_errors_from_callable(
|
|
1126
|
+
self, request_data: dict[str, Any] | list[Any] | None, input_output: InputOutput
|
|
1127
|
+
) -> dict[str, str]:
|
|
1128
|
+
if not self.input_validation_callable:
|
|
1129
|
+
return {}
|
|
1130
|
+
|
|
1131
|
+
more_input_errors = self.di.call_function(
|
|
1132
|
+
self.input_validation_callable, **input_output.get_context_for_callables()
|
|
1133
|
+
)
|
|
1134
|
+
if not isinstance(more_input_errors, dict):
|
|
1135
|
+
raise ValueError("The input error callable did not return a dictionary as required")
|
|
1136
|
+
return more_input_errors
|
|
1137
|
+
|
|
1138
|
+
def cors(self, input_output: InputOutput):
|
|
1139
|
+
cors_header = self.cors_header if self.cors_header else Cors()
|
|
1140
|
+
for method in self.request_methods:
|
|
1141
|
+
cors_header.add_method(method)
|
|
1142
|
+
if self.authentication:
|
|
1143
|
+
self.authentication.set_headers_for_cors(cors_header)
|
|
1144
|
+
cors_header.set_headers_for_input_output(input_output)
|
|
1145
|
+
for security_header in self.security_headers:
|
|
1146
|
+
if security_header.is_cors:
|
|
1147
|
+
continue
|
|
1148
|
+
security_header.set_headers_for_input_output(input_output)
|
|
1149
|
+
return input_output.respond("", 200)
|
|
1150
|
+
|
|
1151
|
+
def documentation(self) -> list[Request]:
|
|
1152
|
+
return []
|
|
1153
|
+
|
|
1154
|
+
def documentation_components(self) -> dict[str, Any]:
|
|
1155
|
+
return {
|
|
1156
|
+
"models": self.documentation_models(),
|
|
1157
|
+
"securitySchemes": self.documentation_security_schemes(),
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
def documentation_security_schemes(self) -> dict[str, Any]:
|
|
1161
|
+
if not self.authentication or not self.authentication.documentation_security_scheme_name():
|
|
1162
|
+
return {}
|
|
1163
|
+
|
|
1164
|
+
return {
|
|
1165
|
+
self.authentication.documentation_security_scheme_name(): (
|
|
1166
|
+
self.authentication.documentation_security_scheme()
|
|
1167
|
+
),
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
def documentation_models(self) -> dict[str, schema.Schema]:
|
|
1171
|
+
return {}
|
|
1172
|
+
|
|
1173
|
+
def documentation_pagination_response(self, include_pagination=True) -> schema.Schema:
|
|
1174
|
+
if not include_pagination:
|
|
1175
|
+
return schema.Object(self.auto_case_internal_column_name("pagination"), [], value={})
|
|
1176
|
+
model = self.di.build(self.model_class)
|
|
1177
|
+
return schema.Object(
|
|
1178
|
+
self.auto_case_internal_column_name("pagination"),
|
|
1179
|
+
[
|
|
1180
|
+
schema.Integer(self.auto_case_internal_column_name("number_results"), example=10),
|
|
1181
|
+
schema.Integer(self.auto_case_internal_column_name("limit"), example=100),
|
|
1182
|
+
schema.Object(
|
|
1183
|
+
self.auto_case_internal_column_name("next_page"),
|
|
1184
|
+
model.documentation_pagination_next_page_response(self.auto_case_internal_column_name),
|
|
1185
|
+
model.documentation_pagination_next_page_example(self.auto_case_internal_column_name),
|
|
1186
|
+
),
|
|
1187
|
+
],
|
|
1188
|
+
)
|
|
1189
|
+
|
|
1190
|
+
def documentation_success_response(
|
|
1191
|
+
self, data_schema: schema.Object | schema.Array, description: str = "", include_pagination: bool = False
|
|
1192
|
+
) -> Response:
|
|
1193
|
+
return Response(
|
|
1194
|
+
200,
|
|
1195
|
+
schema.Object(
|
|
1196
|
+
"body",
|
|
1197
|
+
[
|
|
1198
|
+
schema.String(self.auto_case_internal_column_name("status"), value="success"),
|
|
1199
|
+
data_schema,
|
|
1200
|
+
self.documentation_pagination_response(include_pagination=include_pagination),
|
|
1201
|
+
schema.String(self.auto_case_internal_column_name("error"), value=""),
|
|
1202
|
+
schema.Object(self.auto_case_internal_column_name("input_errors"), [], value={}),
|
|
1203
|
+
],
|
|
1204
|
+
),
|
|
1205
|
+
description=description,
|
|
1206
|
+
)
|
|
1207
|
+
|
|
1208
|
+
def documentation_generic_error_response(self, description="Invalid Call", status=400) -> Response:
|
|
1209
|
+
return Response(
|
|
1210
|
+
status,
|
|
1211
|
+
schema.Object(
|
|
1212
|
+
"body",
|
|
1213
|
+
[
|
|
1214
|
+
schema.String(self.auto_case_internal_column_name("status"), value="error"),
|
|
1215
|
+
schema.Object(self.auto_case_internal_column_name("data"), [], value={}),
|
|
1216
|
+
self.documentation_pagination_response(include_pagination=False),
|
|
1217
|
+
schema.String(self.auto_case_internal_column_name("error"), example="User readable error message"),
|
|
1218
|
+
schema.Object(self.auto_case_internal_column_name("input_errors"), [], value={}),
|
|
1219
|
+
],
|
|
1220
|
+
),
|
|
1221
|
+
description=description,
|
|
1222
|
+
)
|
|
1223
|
+
|
|
1224
|
+
def documentation_input_error_response(self, description="Invalid client-side input") -> Response:
|
|
1225
|
+
email_example = self.auto_case_internal_column_name("email")
|
|
1226
|
+
return Response(
|
|
1227
|
+
200,
|
|
1228
|
+
schema.Object(
|
|
1229
|
+
"body",
|
|
1230
|
+
[
|
|
1231
|
+
schema.String(self.auto_case_internal_column_name("status"), value="input_errors"),
|
|
1232
|
+
schema.Object(self.auto_case_internal_column_name("data"), [], value={}),
|
|
1233
|
+
self.documentation_pagination_response(include_pagination=False),
|
|
1234
|
+
schema.String(self.auto_case_internal_column_name("error"), value=""),
|
|
1235
|
+
schema.Object(
|
|
1236
|
+
self.auto_case_internal_column_name("input_errors"),
|
|
1237
|
+
[schema.String("[COLUMN_NAME]", example="User friendly error message")],
|
|
1238
|
+
example={email_example: f"{email_example} was not a valid email address"},
|
|
1239
|
+
),
|
|
1240
|
+
],
|
|
1241
|
+
),
|
|
1242
|
+
description=description,
|
|
1243
|
+
)
|
|
1244
|
+
|
|
1245
|
+
def documentation_access_denied_response(self) -> Response:
|
|
1246
|
+
return self.documentation_generic_error_response(description="Access Denied", status=401)
|
|
1247
|
+
|
|
1248
|
+
def documentation_unauthorized_response(self) -> Response:
|
|
1249
|
+
return self.documentation_generic_error_response(description="Unauthorized", status=403)
|
|
1250
|
+
|
|
1251
|
+
def documentation_not_found(self) -> Response:
|
|
1252
|
+
return self.documentation_generic_error_response(description="Not Found", status=404)
|
|
1253
|
+
|
|
1254
|
+
def documentation_request_security(self):
|
|
1255
|
+
authentication = self.authentication
|
|
1256
|
+
name = authentication.documentation_security_scheme_name()
|
|
1257
|
+
return [{name: []}] if name else []
|
|
1258
|
+
|
|
1259
|
+
def documentation_data_schema(
|
|
1260
|
+
self, schema: type[Schema] | None = None, column_names: list[str] = []
|
|
1261
|
+
) -> list[schema.Schema]:
|
|
1262
|
+
if schema is None:
|
|
1263
|
+
schema = self.model_class
|
|
1264
|
+
readable_column_names = [*column_names]
|
|
1265
|
+
if not readable_column_names and self.readable_column_names:
|
|
1266
|
+
readable_column_names: list[str] = self.readable_column_names # type: ignore
|
|
1267
|
+
properties = []
|
|
1268
|
+
|
|
1269
|
+
columns = schema.get_columns()
|
|
1270
|
+
for column_name in readable_column_names:
|
|
1271
|
+
column = columns[column_name]
|
|
1272
|
+
for doc in column.documentation():
|
|
1273
|
+
doc.name = self.auto_case_internal_column_name(doc.name)
|
|
1274
|
+
properties.append(doc)
|
|
1275
|
+
|
|
1276
|
+
return properties
|
|
1277
|
+
|
|
1278
|
+
def standard_json_request_parameters(
|
|
1279
|
+
self, schema: type[Schema] | None = None, column_names: list[str] = []
|
|
1280
|
+
) -> list[Parameter]:
|
|
1281
|
+
if not column_names:
|
|
1282
|
+
if not self.writeable_column_names:
|
|
1283
|
+
return []
|
|
1284
|
+
column_names = self.writeable_column_names
|
|
1285
|
+
|
|
1286
|
+
if not schema:
|
|
1287
|
+
if not self.model_class:
|
|
1288
|
+
return []
|
|
1289
|
+
schema = self.model_class
|
|
1290
|
+
|
|
1291
|
+
model_name = string.camel_case_to_snake_case(schema.__name__)
|
|
1292
|
+
columns = schema.get_columns()
|
|
1293
|
+
parameters = []
|
|
1294
|
+
for column_name in column_names:
|
|
1295
|
+
columns[column_name].injectable_properties(self.di)
|
|
1296
|
+
parameters.append(
|
|
1297
|
+
autodoc.request.JSONBody(
|
|
1298
|
+
columns[column_name].documentation(name=self.auto_case_column_name(column_name, True)),
|
|
1299
|
+
description=f"Set '{column_name}' for the {model_name}",
|
|
1300
|
+
required=columns[column_name].is_required,
|
|
1301
|
+
)
|
|
1302
|
+
)
|
|
1303
|
+
return parameters # type: ignore
|
|
1304
|
+
|
|
1305
|
+
def documentation_url_parameters(self) -> list[Parameter]:
|
|
1306
|
+
parameter_names = routing.extract_url_parameter_name_map(self.url.strip("/"))
|
|
1307
|
+
return [
|
|
1308
|
+
autodoc.request.URLPath(
|
|
1309
|
+
autodoc.schema.String(parameter_name),
|
|
1310
|
+
description=f"The {parameter_name}.",
|
|
1311
|
+
required=True,
|
|
1312
|
+
)
|
|
1313
|
+
for parameter_name in parameter_names.keys()
|
|
1314
|
+
]
|