clear-skies 2.0.6__py3-none-any.whl → 2.0.8__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.6.dist-info → clear_skies-2.0.8.dist-info}/METADATA +1 -1
- clear_skies-2.0.8.dist-info/RECORD +252 -0
- clearskies/__init__.py +2 -2
- clearskies/authentication/authentication.py +1 -3
- clearskies/authentication/authorization.py +12 -5
- clearskies/authentication/authorization_pass_through.py +5 -3
- clearskies/authentication/jwks.py +25 -23
- clearskies/authentication/secret_bearer.py +15 -17
- clearskies/autodoc/schema/schema.py +1 -1
- clearskies/backends/api_backend.py +50 -56
- clearskies/backends/backend.py +14 -14
- clearskies/backends/cursor_backend.py +17 -23
- clearskies/backends/memory_backend.py +27 -30
- clearskies/backends/secrets_backend.py +13 -18
- clearskies/column.py +44 -56
- clearskies/columns/audit.py +14 -13
- clearskies/columns/belongs_to_id.py +10 -15
- clearskies/columns/belongs_to_model.py +6 -9
- clearskies/columns/belongs_to_self.py +13 -9
- clearskies/columns/boolean.py +13 -16
- clearskies/columns/category_tree.py +9 -11
- clearskies/columns/category_tree_children.py +2 -3
- clearskies/columns/category_tree_descendants.py +1 -1
- clearskies/columns/created.py +8 -11
- clearskies/columns/created_by_authorization_data.py +7 -9
- clearskies/columns/created_by_header.py +12 -8
- clearskies/columns/created_by_ip.py +6 -8
- clearskies/columns/created_by_routing_data.py +12 -7
- clearskies/columns/created_by_user_agent.py +6 -9
- clearskies/columns/date.py +12 -14
- clearskies/columns/datetime.py +19 -17
- clearskies/columns/email.py +3 -1
- clearskies/columns/float.py +10 -14
- clearskies/columns/has_many.py +8 -10
- clearskies/columns/has_many_self.py +13 -7
- clearskies/columns/has_one.py +2 -0
- clearskies/columns/integer.py +9 -11
- clearskies/columns/json.py +10 -12
- clearskies/columns/many_to_many_ids.py +14 -16
- clearskies/columns/many_to_many_ids_with_data.py +16 -16
- clearskies/columns/many_to_many_models.py +5 -7
- clearskies/columns/many_to_many_pivots.py +3 -5
- clearskies/columns/phone.py +12 -9
- clearskies/columns/select.py +12 -9
- clearskies/columns/string.py +1 -1
- clearskies/columns/timestamp.py +15 -15
- clearskies/columns/updated.py +8 -10
- clearskies/columns/uuid.py +7 -10
- clearskies/configs/any.py +2 -0
- clearskies/configs/any_dict.py +2 -0
- clearskies/configs/any_dict_or_callable.py +2 -0
- clearskies/configs/boolean.py +2 -0
- clearskies/configs/boolean_or_callable.py +2 -0
- clearskies/configs/callable_config.py +2 -0
- clearskies/configs/config.py +2 -0
- clearskies/configs/datetime.py +2 -0
- clearskies/configs/datetime_or_callable.py +2 -0
- clearskies/configs/float.py +2 -0
- clearskies/configs/float_or_callable.py +2 -0
- clearskies/configs/integer.py +2 -0
- clearskies/configs/integer_or_callable.py +2 -0
- clearskies/configs/list_any_dict.py +2 -0
- clearskies/configs/list_any_dict_or_callable.py +2 -0
- clearskies/configs/model_column.py +2 -0
- clearskies/configs/model_columns.py +2 -0
- clearskies/configs/model_destination_name.py +2 -1
- clearskies/configs/model_to_id_column.py +2 -0
- clearskies/configs/readable_model_column.py +2 -0
- clearskies/configs/readable_model_columns.py +2 -0
- clearskies/configs/searchable_model_columns.py +2 -0
- clearskies/configs/select.py +2 -0
- clearskies/configs/select_list.py +2 -0
- clearskies/configs/string.py +2 -0
- clearskies/configs/string_dict.py +2 -0
- clearskies/configs/string_list.py +2 -0
- clearskies/configs/string_list_or_callable.py +2 -0
- clearskies/configs/timedelta.py +2 -0
- clearskies/configs/timezone.py +2 -0
- clearskies/configs/url.py +2 -0
- clearskies/configs/writeable_model_column.py +2 -0
- clearskies/configs/writeable_model_columns.py +2 -0
- clearskies/configurable.py +2 -0
- clearskies/contexts/cli.py +9 -1
- clearskies/contexts/context.py +13 -14
- clearskies/contexts/wsgi.py +12 -10
- clearskies/contexts/wsgi_ref.py +12 -6
- clearskies/decorators.py +1 -1
- clearskies/decorators.pyi +10 -0
- clearskies/di/__init__.py +2 -1
- clearskies/di/di.py +7 -6
- clearskies/di/inject/by_class.py +2 -0
- clearskies/di/inject/by_name.py +2 -0
- clearskies/di/inject/di.py +2 -0
- clearskies/di/inject/environment.py +1 -1
- clearskies/di/inject/now.py +2 -0
- clearskies/di/inject/requests.py +2 -0
- clearskies/di/inject/secrets.py +2 -2
- clearskies/di/inject/utcnow.py +2 -0
- clearskies/di/inject/uuid.py +2 -2
- clearskies/end.py +45 -7
- clearskies/endpoint.py +43 -59
- clearskies/endpoint_group.py +15 -18
- clearskies/endpoints/advanced_search.py +19 -26
- clearskies/endpoints/callable.py +10 -16
- clearskies/endpoints/create.py +6 -10
- clearskies/endpoints/delete.py +5 -11
- clearskies/endpoints/get.py +11 -15
- clearskies/endpoints/health_check.py +9 -11
- clearskies/endpoints/list.py +29 -36
- clearskies/endpoints/restful_api.py +43 -53
- clearskies/endpoints/schema.py +14 -18
- clearskies/endpoints/simple_search.py +5 -12
- clearskies/endpoints/update.py +6 -11
- clearskies/environment.py +2 -0
- clearskies/input_outputs/cli.py +2 -0
- clearskies/input_outputs/headers.py +2 -0
- clearskies/input_outputs/input_output.py +15 -15
- clearskies/input_outputs/programmatic.py +2 -2
- clearskies/input_outputs/wsgi.py +2 -2
- clearskies/model.py +120 -25
- clearskies/query/query.py +1 -4
- clearskies/secrets/__init__.py +2 -1
- clearskies/secrets/akeyless.py +12 -10
- clearskies/secrets/secrets.py +7 -2
- clearskies/security_header.py +4 -2
- clearskies/security_headers/cache_control.py +15 -14
- clearskies/security_headers/cors.py +10 -9
- clearskies/security_headers/csp.py +25 -24
- clearskies/security_headers/hsts.py +6 -5
- clearskies/typing.py +1 -1
- clearskies/validator.py +5 -6
- clearskies/validators/after_column.py +6 -7
- clearskies/validators/before_column.py +2 -0
- clearskies/validators/in_the_future.py +5 -8
- clearskies/validators/in_the_future_at_least.py +2 -0
- clearskies/validators/in_the_future_at_most.py +2 -0
- clearskies/validators/in_the_past.py +5 -8
- clearskies/validators/in_the_past_at_least.py +2 -0
- clearskies/validators/in_the_past_at_most.py +2 -0
- clearskies/validators/maximum_length.py +4 -5
- clearskies/validators/maximum_value.py +4 -4
- clearskies/validators/minimum_length.py +4 -4
- clearskies/validators/minimum_value.py +4 -4
- clearskies/validators/required.py +2 -4
- clearskies/validators/timedelta.py +8 -9
- clearskies/validators/unique.py +2 -3
- clear_skies-2.0.6.dist-info/RECORD +0 -251
- {clear_skies-2.0.6.dist-info → clear_skies-2.0.8.dist-info}/WHEEL +0 -0
- {clear_skies-2.0.6.dist-info → clear_skies-2.0.8.dist-info}/licenses/LICENSE +0 -0
clearskies/end.py
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
|
-
# type: ignore
|
|
2
1
|
from __future__ import annotations
|
|
3
2
|
|
|
4
3
|
from abc import ABC
|
|
5
4
|
from typing import TYPE_CHECKING, Any
|
|
6
5
|
|
|
7
6
|
if TYPE_CHECKING:
|
|
8
|
-
from clearskies
|
|
7
|
+
from clearskies import typing
|
|
8
|
+
from clearskies.input_outputs import InputOutput
|
|
9
9
|
|
|
10
|
-
from clearskies import exceptions
|
|
10
|
+
from clearskies import configs, configurable, di, exceptions
|
|
11
|
+
from clearskies.authentication import Authorization, Public
|
|
11
12
|
from clearskies.functional import string
|
|
12
13
|
|
|
13
14
|
|
|
14
|
-
class End(
|
|
15
|
+
class End(
|
|
16
|
+
ABC,
|
|
17
|
+
configurable.Configurable,
|
|
18
|
+
di.InjectableProperties,
|
|
19
|
+
):
|
|
15
20
|
"""
|
|
16
21
|
DRY for endpoint and endpoint groups.
|
|
17
22
|
|
|
@@ -20,6 +25,24 @@ class End(ABC):
|
|
|
20
25
|
from the other.
|
|
21
26
|
"""
|
|
22
27
|
|
|
28
|
+
di = di.inject.Di()
|
|
29
|
+
|
|
30
|
+
url = configs.String(required=False, default="")
|
|
31
|
+
|
|
32
|
+
authentication = configs.Authentication(default=None)
|
|
33
|
+
|
|
34
|
+
authorization = configs.Authorization(default=None)
|
|
35
|
+
|
|
36
|
+
internal_casing = configs.Select(["snake_case", "camelCase", "TitleCase"], default="snake_case")
|
|
37
|
+
external_casing = configs.Select(["snake_case", "camelCase", "TitleCase"], default="snake_case")
|
|
38
|
+
response_headers = configs.StringListOrCallable(default=[])
|
|
39
|
+
authentication = configs.Authentication(default=Public())
|
|
40
|
+
authorization = configs.Authorization(default=Authorization())
|
|
41
|
+
security_headers = configs.SecurityHeaders(default=[])
|
|
42
|
+
|
|
43
|
+
cors_header: SecurityHeader = None # type: ignore
|
|
44
|
+
has_cors: bool = False
|
|
45
|
+
|
|
23
46
|
def add_url_prefix(self, prefix: str) -> None:
|
|
24
47
|
self.url = (prefix.rstrip("/") + "/" + self.url.lstrip("/")).lstrip("/")
|
|
25
48
|
|
|
@@ -41,7 +64,7 @@ class End(ABC):
|
|
|
41
64
|
if not self.authorization.gate(input_output.authorization_data, input_output):
|
|
42
65
|
raise exceptions.Authorization("Not Authorized")
|
|
43
66
|
except exceptions.ClientError as client_error:
|
|
44
|
-
raise
|
|
67
|
+
raise exceptions.Authorization(str(client_error))
|
|
45
68
|
|
|
46
69
|
def __call__(self, input_output: InputOutput) -> Any:
|
|
47
70
|
"""
|
|
@@ -116,7 +139,7 @@ class End(ABC):
|
|
|
116
139
|
for index, response_header in enumerate(response_headers):
|
|
117
140
|
if not isinstance(response_header, str):
|
|
118
141
|
raise TypeError(
|
|
119
|
-
f"Invalid response header in entry #{index + 1}: the header should be a string, but I was given a type of '{
|
|
142
|
+
f"Invalid response header in entry #{index + 1}: the header should be a string, but I was given a type of '{response_header.__class__.__name__}' instead."
|
|
120
143
|
)
|
|
121
144
|
parts = response_header.split(":", 1)
|
|
122
145
|
if len(parts) != 2:
|
|
@@ -127,7 +150,7 @@ class End(ABC):
|
|
|
127
150
|
for security_header in self.security_headers:
|
|
128
151
|
security_header.set_headers_for_input_output(input_output)
|
|
129
152
|
|
|
130
|
-
def respond(self, input_output: InputOutput, response:
|
|
153
|
+
def respond(self, input_output: InputOutput, response: typing.response, status_code: int) -> Any:
|
|
131
154
|
self.add_response_headers(input_output)
|
|
132
155
|
return input_output.respond(response, status_code)
|
|
133
156
|
|
|
@@ -181,3 +204,18 @@ class End(ABC):
|
|
|
181
204
|
self.external_casing,
|
|
182
205
|
self.internal_casing,
|
|
183
206
|
)
|
|
207
|
+
|
|
208
|
+
def error(self, input_output: InputOutput, message: str, status_code: int) -> Any:
|
|
209
|
+
"""Return a client-side error (e.g. 400)."""
|
|
210
|
+
return self.respond_json(input_output, {"status": "client_error", "error": message}, status_code)
|
|
211
|
+
|
|
212
|
+
def input_errors(self, input_output: InputOutput, errors: dict[str, str], status_code: int = 200) -> Any:
|
|
213
|
+
"""Return input errors to the client."""
|
|
214
|
+
return self.respond_json(input_output, {"status": "input_errors", "input_errors": errors}, status_code)
|
|
215
|
+
|
|
216
|
+
def redirect(self, input_output: InputOutput, location: str, status_code: int) -> Any:
|
|
217
|
+
"""Return a redirect response (e.g. 302)."""
|
|
218
|
+
return self.respond_json(input_output, {"status": "redirect", "location": location}, status_code)
|
|
219
|
+
|
|
220
|
+
def handle(self, input_output: InputOutput) -> Any:
|
|
221
|
+
raise NotImplementedError()
|
clearskies/endpoint.py
CHANGED
|
@@ -3,16 +3,9 @@ from __future__ import annotations
|
|
|
3
3
|
import inspect
|
|
4
4
|
import urllib.parse
|
|
5
5
|
from collections import OrderedDict
|
|
6
|
-
from typing import TYPE_CHECKING, Any, Callable
|
|
7
|
-
|
|
8
|
-
import
|
|
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
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional
|
|
7
|
+
|
|
8
|
+
from clearskies import autodoc, column, configs, configurable, decorators, di, end, exceptions
|
|
16
9
|
from clearskies.authentication import Authentication, Authorization, Public
|
|
17
10
|
from clearskies.autodoc import schema
|
|
18
11
|
from clearskies.autodoc.request import Parameter, Request
|
|
@@ -27,12 +20,12 @@ if TYPE_CHECKING:
|
|
|
27
20
|
|
|
28
21
|
|
|
29
22
|
class Endpoint(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
23
|
+
end.End, # type: ignore
|
|
24
|
+
configurable.Configurable,
|
|
25
|
+
di.InjectableProperties,
|
|
33
26
|
):
|
|
34
27
|
"""
|
|
35
|
-
Automating drudgery
|
|
28
|
+
Automating drudgery.
|
|
36
29
|
|
|
37
30
|
With clearskies, endpoints exist to offload some drudgery and make your life easier, but they can also
|
|
38
31
|
get out of your way when you don't need them. Think of them as pre-built endpoints that can execute
|
|
@@ -47,7 +40,7 @@ class Endpoint(
|
|
|
47
40
|
"""
|
|
48
41
|
The dependency injection container
|
|
49
42
|
"""
|
|
50
|
-
di =
|
|
43
|
+
di = di.inject.Di()
|
|
51
44
|
|
|
52
45
|
"""
|
|
53
46
|
Whether or not this endpoint can handle CORS
|
|
@@ -57,7 +50,7 @@ class Endpoint(
|
|
|
57
50
|
"""
|
|
58
51
|
The actual CORS header
|
|
59
52
|
"""
|
|
60
|
-
cors_header: Cors
|
|
53
|
+
cors_header: Optional[Cors] = None
|
|
61
54
|
|
|
62
55
|
"""
|
|
63
56
|
Set some response headers that should be returned for this endpoint.
|
|
@@ -82,7 +75,7 @@ class Endpoint(
|
|
|
82
75
|
wsgi()
|
|
83
76
|
```
|
|
84
77
|
"""
|
|
85
|
-
response_headers =
|
|
78
|
+
response_headers = configs.StringListOrCallable(default=[])
|
|
86
79
|
|
|
87
80
|
"""
|
|
88
81
|
Set the URL for the endpoint
|
|
@@ -161,7 +154,7 @@ class Endpoint(
|
|
|
161
154
|
```
|
|
162
155
|
|
|
163
156
|
"""
|
|
164
|
-
url =
|
|
157
|
+
url = configs.Url(default="")
|
|
165
158
|
|
|
166
159
|
"""
|
|
167
160
|
The allowed request methods for this endpoint.
|
|
@@ -204,7 +197,7 @@ class Endpoint(
|
|
|
204
197
|
}
|
|
205
198
|
```
|
|
206
199
|
"""
|
|
207
|
-
request_methods =
|
|
200
|
+
request_methods = configs.SelectList(
|
|
208
201
|
allowed_values=["GET", "POST", "PUT", "DELETE", "PATCH", "QUERY"], default=["GET"]
|
|
209
202
|
)
|
|
210
203
|
|
|
@@ -214,7 +207,7 @@ class Endpoint(
|
|
|
214
207
|
Use this to attach an instance of `clearskies.authentication.Authentication` to an endpoint, which enforces authentication.
|
|
215
208
|
For more details, see the dedicated documentation section on authentication and authorization. By default, all endpoints are public.
|
|
216
209
|
"""
|
|
217
|
-
authentication =
|
|
210
|
+
authentication = configs.Authentication(default=Public())
|
|
218
211
|
|
|
219
212
|
"""
|
|
220
213
|
The authorization rules for this endpoint
|
|
@@ -222,7 +215,7 @@ class Endpoint(
|
|
|
222
215
|
Use this to attach an instance of `clearskies.authentication.Authorization` to an endpoint, which enforces authorization.
|
|
223
216
|
For more details, see the dedicated documentation section on authentication and authorization. By default, no authorization is enforced.
|
|
224
217
|
"""
|
|
225
|
-
authorization =
|
|
218
|
+
authorization = configs.Authorization(default=Authorization())
|
|
226
219
|
|
|
227
220
|
"""
|
|
228
221
|
An override of the default model-to-json mapping for endpoints that auto-convert models to json.
|
|
@@ -329,7 +322,7 @@ class Endpoint(
|
|
|
329
322
|
```
|
|
330
323
|
|
|
331
324
|
"""
|
|
332
|
-
output_map =
|
|
325
|
+
output_map = configs.Callable(default=None)
|
|
333
326
|
|
|
334
327
|
"""
|
|
335
328
|
A schema that describes the expected output to the client.
|
|
@@ -338,14 +331,14 @@ class Endpoint(
|
|
|
338
331
|
Note that this is typically not required - when returning models and relying on clearskies to auto-convert to JSON,
|
|
339
332
|
it will also automatically generate your documentation.
|
|
340
333
|
"""
|
|
341
|
-
output_schema =
|
|
334
|
+
output_schema = configs.Schema(default=None)
|
|
342
335
|
|
|
343
336
|
"""
|
|
344
337
|
The model class used by this endpoint.
|
|
345
338
|
|
|
346
339
|
The endpoint will use this to fetch/save/validate incoming data as needed.
|
|
347
340
|
"""
|
|
348
|
-
model_class =
|
|
341
|
+
model_class = configs.ModelClass(default=None)
|
|
349
342
|
|
|
350
343
|
"""
|
|
351
344
|
Columns from the model class that should be returned to the client.
|
|
@@ -421,7 +414,7 @@ class Endpoint(
|
|
|
421
414
|
|
|
422
415
|
```
|
|
423
416
|
"""
|
|
424
|
-
readable_column_names =
|
|
417
|
+
readable_column_names = configs.ReadableModelColumns("model_class", default=[])
|
|
425
418
|
|
|
426
419
|
"""
|
|
427
420
|
Specifies which columns from a model class can be set by the client.
|
|
@@ -486,14 +479,14 @@ class Endpoint(
|
|
|
486
479
|
```
|
|
487
480
|
|
|
488
481
|
"""
|
|
489
|
-
writeable_column_names =
|
|
482
|
+
writeable_column_names = configs.WriteableModelColumns("model_class", default=[])
|
|
490
483
|
|
|
491
484
|
"""
|
|
492
485
|
Columns from the model class that can be searched by the client.
|
|
493
486
|
|
|
494
487
|
Sets which columns the client is allowed to search (for endpoints that support searching).
|
|
495
488
|
"""
|
|
496
|
-
searchable_column_names =
|
|
489
|
+
searchable_column_names = configs.SearchableModelColumns("model_class", default=[])
|
|
497
490
|
|
|
498
491
|
"""
|
|
499
492
|
A function to call to add custom input validation logic.
|
|
@@ -557,7 +550,7 @@ class Endpoint(
|
|
|
557
550
|
```
|
|
558
551
|
|
|
559
552
|
"""
|
|
560
|
-
input_validation_callable =
|
|
553
|
+
input_validation_callable = configs.Callable(default=None)
|
|
561
554
|
|
|
562
555
|
"""
|
|
563
556
|
A dictionary with columns that should override columns in the model.
|
|
@@ -579,7 +572,7 @@ class Endpoint(
|
|
|
579
572
|
)
|
|
580
573
|
```
|
|
581
574
|
"""
|
|
582
|
-
column_overrides =
|
|
575
|
+
column_overrides = configs.Columns(default={})
|
|
583
576
|
|
|
584
577
|
"""
|
|
585
578
|
Used in conjunction with external_casing to change the casing of the key names in the outputted JSON of the endpoint.
|
|
@@ -638,14 +631,14 @@ class Endpoint(
|
|
|
638
631
|
}
|
|
639
632
|
```
|
|
640
633
|
"""
|
|
641
|
-
internal_casing =
|
|
634
|
+
internal_casing = configs.Select(["snake_case", "camelCase", "TitleCase"], default="snake_case")
|
|
642
635
|
|
|
643
636
|
"""
|
|
644
637
|
Used in conjunction with internal_casing to change the casing of the key names in the outputted JSON of the endpoint.
|
|
645
638
|
|
|
646
639
|
See the docs for `internal_casing` for more details and usage examples.
|
|
647
640
|
"""
|
|
648
|
-
external_casing =
|
|
641
|
+
external_casing = configs.Select(["snake_case", "camelCase", "TitleCase"], default="snake_case")
|
|
649
642
|
|
|
650
643
|
"""
|
|
651
644
|
Configure standard security headers to be sent along in the response from this endpoint.
|
|
@@ -689,19 +682,19 @@ class Endpoint(
|
|
|
689
682
|
```
|
|
690
683
|
|
|
691
684
|
"""
|
|
692
|
-
security_headers =
|
|
685
|
+
security_headers = configs.SecurityHeaders(default=[])
|
|
693
686
|
|
|
694
687
|
"""
|
|
695
688
|
A description for this endpoint. This is added to any auto-documentation
|
|
696
689
|
"""
|
|
697
|
-
description =
|
|
690
|
+
description = configs.String(default="")
|
|
698
691
|
|
|
699
692
|
"""
|
|
700
693
|
Whether or not the routing data should also be persisted to the model. Defaults to False.
|
|
701
694
|
|
|
702
695
|
Note: this is only relevant for handlers that accept request data
|
|
703
696
|
"""
|
|
704
|
-
include_routing_data_in_request_data =
|
|
697
|
+
include_routing_data_in_request_data = configs.Boolean(default=False)
|
|
705
698
|
|
|
706
699
|
"""
|
|
707
700
|
Additional conditions to always add to the results.
|
|
@@ -783,7 +776,7 @@ class Endpoint(
|
|
|
783
776
|
and note that neither Greg nor Ann are returned. Ann because she doesn't make the grade criteria, and Greg because
|
|
784
777
|
he won't graduate.
|
|
785
778
|
"""
|
|
786
|
-
where =
|
|
779
|
+
where = configs.Conditions(default=[])
|
|
787
780
|
|
|
788
781
|
"""
|
|
789
782
|
Additional joins to always add to the query.
|
|
@@ -868,18 +861,18 @@ class Endpoint(
|
|
|
868
861
|
e.g., the inner join reomves all the students that don't have an entry in the PastRecord model.
|
|
869
862
|
|
|
870
863
|
"""
|
|
871
|
-
joins =
|
|
864
|
+
joins = configs.Joins(default=[])
|
|
872
865
|
|
|
873
866
|
cors_header: Cors = None # type: ignore
|
|
874
|
-
_model:
|
|
875
|
-
_columns: dict[str,
|
|
876
|
-
_readable_columns: dict[str,
|
|
877
|
-
_writeable_columns: dict[str,
|
|
878
|
-
_searchable_columns: dict[str,
|
|
879
|
-
_sortable_columns: dict[str,
|
|
880
|
-
_as_json_map: dict[str,
|
|
881
|
-
|
|
882
|
-
@
|
|
867
|
+
_model: Model = None # type: ignore
|
|
868
|
+
_columns: dict[str, column.Column] = None # type: ignore
|
|
869
|
+
_readable_columns: dict[str, column.Column] = None # type: ignore
|
|
870
|
+
_writeable_columns: dict[str, column.Column] = None # type: ignore
|
|
871
|
+
_searchable_columns: dict[str, column.Column] = None # type: ignore
|
|
872
|
+
_sortable_columns: dict[str, column.Column] = None # type: ignore
|
|
873
|
+
_as_json_map: dict[str, column.Column] = None # type: ignore
|
|
874
|
+
|
|
875
|
+
@decorators.parameters_to_properties
|
|
883
876
|
def __init__(
|
|
884
877
|
self,
|
|
885
878
|
url: str = "",
|
|
@@ -972,9 +965,6 @@ class Endpoint(
|
|
|
972
965
|
)
|
|
973
966
|
return self.authorization.filter_model(model, input_output.authorization_data, input_output)
|
|
974
967
|
|
|
975
|
-
def handle(self, input_output: InputOutput) -> Any:
|
|
976
|
-
raise NotImplementedError()
|
|
977
|
-
|
|
978
968
|
def matches_request(self, input_output: InputOutput, allow_partial=False) -> bool:
|
|
979
969
|
"""Whether or not we can handle an incoming request based on URL and request method."""
|
|
980
970
|
# soo..... this excessively duplicates the logic in __call__, but I'm being lazy right now
|
|
@@ -1012,20 +1002,14 @@ class Endpoint(
|
|
|
1012
1002
|
def failure(self, input_output: InputOutput) -> Any:
|
|
1013
1003
|
return self.respond_json(input_output, {"status": "failure"}, 500)
|
|
1014
1004
|
|
|
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
1005
|
def redirect(self, input_output: InputOutput, location: str, status_code: int) -> Any:
|
|
1024
1006
|
"""Return a redirect."""
|
|
1025
1007
|
input_output.response_headers.add("content-type", "text/html")
|
|
1026
1008
|
input_output.response_headers.add("location", location)
|
|
1027
1009
|
return self.respond(
|
|
1028
|
-
|
|
1010
|
+
input_output,
|
|
1011
|
+
'<meta http-equiv="refresh" content="0; url=' + urllib.parse.quote(location) + '">Redirecting',
|
|
1012
|
+
status_code,
|
|
1029
1013
|
)
|
|
1030
1014
|
|
|
1031
1015
|
def success(
|
|
@@ -1053,7 +1037,7 @@ class Endpoint(
|
|
|
1053
1037
|
|
|
1054
1038
|
return self.respond_json(input_output, response_data, 200)
|
|
1055
1039
|
|
|
1056
|
-
def model_as_json(self, model:
|
|
1040
|
+
def model_as_json(self, model: Model, input_output: InputOutput) -> dict[str, Any]:
|
|
1057
1041
|
if self.output_map:
|
|
1058
1042
|
return self.di.call_function(self.output_map, model=model, **input_output.get_context_for_callables())
|
|
1059
1043
|
|
|
@@ -1070,7 +1054,7 @@ class Endpoint(
|
|
|
1070
1054
|
json[self.auto_case_column_name(key, True)] = value
|
|
1071
1055
|
return json
|
|
1072
1056
|
|
|
1073
|
-
def _build_as_json_map(self, model:
|
|
1057
|
+
def _build_as_json_map(self, model: Model) -> dict[str, column.Column]:
|
|
1074
1058
|
conversion_map = {}
|
|
1075
1059
|
if not self.readable_column_names:
|
|
1076
1060
|
raise ValueError(
|
clearskies/endpoint_group.py
CHANGED
|
@@ -2,10 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import TYPE_CHECKING, Any, Callable, Self
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
import clearskies.di
|
|
7
|
-
import clearskies.end
|
|
8
|
-
from clearskies import exceptions
|
|
5
|
+
from clearskies import configs, configurable, decorators, di, end
|
|
9
6
|
from clearskies.authentication import Authentication, Authorization, Public
|
|
10
7
|
from clearskies.endpoint import Endpoint
|
|
11
8
|
from clearskies.functional import routing
|
|
@@ -16,9 +13,9 @@ if TYPE_CHECKING:
|
|
|
16
13
|
|
|
17
14
|
|
|
18
15
|
class EndpointGroup(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
end.End, # type: ignore
|
|
17
|
+
configurable.Configurable,
|
|
18
|
+
di.InjectableProperties,
|
|
22
19
|
):
|
|
23
20
|
"""
|
|
24
21
|
An endpoint group brings endpoints together: it basically handles routing.
|
|
@@ -216,32 +213,32 @@ class EndpointGroup(
|
|
|
216
213
|
"""
|
|
217
214
|
The dependency injection container
|
|
218
215
|
"""
|
|
219
|
-
di =
|
|
216
|
+
di = di.inject.Di()
|
|
220
217
|
|
|
221
218
|
"""
|
|
222
219
|
The base URL for the endpoint group.
|
|
223
220
|
|
|
224
221
|
This URL is added as a prefix to all endpoints attached to the group. This includes any named URL parameters:
|
|
225
222
|
"""
|
|
226
|
-
url =
|
|
223
|
+
url = configs.String(default="")
|
|
227
224
|
|
|
228
225
|
"""
|
|
229
226
|
The list of endpoints connected to this endpoint group
|
|
230
227
|
"""
|
|
231
|
-
endpoints =
|
|
228
|
+
endpoints = configs.EndpointList()
|
|
232
229
|
|
|
233
|
-
internal_casing =
|
|
234
|
-
external_casing =
|
|
235
|
-
response_headers =
|
|
236
|
-
authentication =
|
|
237
|
-
authorization =
|
|
238
|
-
security_headers =
|
|
230
|
+
internal_casing = configs.Select(["snake_case", "camelCase", "TitleCase"], default="snake_case")
|
|
231
|
+
external_casing = configs.Select(["snake_case", "camelCase", "TitleCase"], default="snake_case")
|
|
232
|
+
response_headers = configs.StringListOrCallable(default=[])
|
|
233
|
+
authentication = configs.Authentication(default=Public())
|
|
234
|
+
authorization = configs.Authorization(default=Authorization())
|
|
235
|
+
security_headers = configs.SecurityHeaders(default=[])
|
|
239
236
|
|
|
240
237
|
cors_header: SecurityHeader = None # type: ignore
|
|
241
238
|
has_cors: bool = False
|
|
242
239
|
endpoints_initialized = False
|
|
243
240
|
|
|
244
|
-
@
|
|
241
|
+
@decorators.parameters_to_properties
|
|
245
242
|
def __init__(
|
|
246
243
|
self,
|
|
247
244
|
endpoints: list[Endpoint | Self],
|
|
@@ -325,7 +322,7 @@ class EndpointGroup(
|
|
|
325
322
|
return self.respond_json(input_output, {"status": "client_error", "error": message}, status_code)
|
|
326
323
|
|
|
327
324
|
def all_endpoints(self) -> list[Endpoint]:
|
|
328
|
-
"""
|
|
325
|
+
"""Return the full (recursive) list of all endpoints associated with this endpoint group."""
|
|
329
326
|
all_endpoints: list[Endpoint] = []
|
|
330
327
|
for endpoint in self.endpoints:
|
|
331
328
|
if hasattr(endpoint, "all_endpoints"):
|
|
@@ -1,19 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
from collections import OrderedDict
|
|
5
|
-
from typing import TYPE_CHECKING, Any, Type
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
6
4
|
|
|
7
|
-
import
|
|
8
|
-
import clearskies.exceptions
|
|
9
|
-
from clearskies import authentication, autodoc, typing
|
|
5
|
+
from clearskies import exceptions
|
|
10
6
|
from clearskies.endpoints.simple_search import SimpleSearch
|
|
11
|
-
from clearskies.functional import string
|
|
12
|
-
from clearskies.input_outputs import InputOutput
|
|
13
7
|
|
|
14
8
|
if TYPE_CHECKING:
|
|
15
|
-
from clearskies import
|
|
16
|
-
from clearskies.model import Model
|
|
9
|
+
from clearskies import Model, autodoc
|
|
17
10
|
|
|
18
11
|
|
|
19
12
|
class AdvancedSearch(SimpleSearch):
|
|
@@ -350,12 +343,12 @@ class AdvancedSearch(SimpleSearch):
|
|
|
350
343
|
if pagination_data:
|
|
351
344
|
error = self.model.validate_pagination_data(pagination_data, self.auto_case_internal_column_name)
|
|
352
345
|
if error:
|
|
353
|
-
raise
|
|
346
|
+
raise exceptions.ClientError(error)
|
|
354
347
|
if query_parameters:
|
|
355
|
-
raise
|
|
348
|
+
raise exceptions.ClientError("Query parameters were found but are not supported.")
|
|
356
349
|
for key in request_data.keys():
|
|
357
350
|
if key not in self.allowed_request_keys:
|
|
358
|
-
raise
|
|
351
|
+
raise exceptions.ClientError(
|
|
359
352
|
f"Invalid request parameter found in request body: '{key}'. Expected parameters: "
|
|
360
353
|
+ ", ".join([self.auto_case_internal_column_name(key) for key in self.allowed_request_keys])
|
|
361
354
|
)
|
|
@@ -363,7 +356,7 @@ class AdvancedSearch(SimpleSearch):
|
|
|
363
356
|
sort_key_name = self.auto_case_internal_column_name("sort")
|
|
364
357
|
sort = request_data.get(sort_key_name, [])
|
|
365
358
|
if not isinstance(sort, list):
|
|
366
|
-
raise
|
|
359
|
+
raise exceptions.ClientError(
|
|
367
360
|
f"'{sort_key_name}' property in request body should be a list, but I found a value of type "
|
|
368
361
|
+ sort.__class__.__name
|
|
369
362
|
)
|
|
@@ -372,25 +365,25 @@ class AdvancedSearch(SimpleSearch):
|
|
|
372
365
|
direction_key_name = self.auto_case_internal_column_name("direction")
|
|
373
366
|
for index, sort_entry in enumerate(sort):
|
|
374
367
|
if not isinstance(sort_entry, dict):
|
|
375
|
-
raise
|
|
368
|
+
raise exceptions.ClientError(
|
|
376
369
|
f"'{sort_key_name}' should be a list of dictionaries, but entry #{index + 1} is a value of type '{sort_entry.__class__.__name}', not a dict"
|
|
377
370
|
)
|
|
378
371
|
for key_name in [column_key_name, direction_key_name]:
|
|
379
372
|
if not sort_entry.get(key_name):
|
|
380
|
-
raise
|
|
373
|
+
raise exceptions.ClientError(
|
|
381
374
|
f"Each entry in the sort list should contain both '{column_key_name}' and '{direction_key_name}' but entry #{index + 1} is missing '{key_name}'"
|
|
382
375
|
)
|
|
383
376
|
if not isinstance(sort_entry[key_name], str):
|
|
384
|
-
raise
|
|
377
|
+
raise exceptions.ClientError(
|
|
385
378
|
f"{key_name}' must be a string, but for entry #{index + 1} it is a value of type "
|
|
386
379
|
+ sort_entry[key_name].__class__.__name__
|
|
387
380
|
)
|
|
388
381
|
if sort_entry[direction_key_name].lower() not in ["asc", "desc"]:
|
|
389
|
-
raise
|
|
382
|
+
raise exceptions.ClientError(
|
|
390
383
|
f"{direction_key_name}' must be either 'ASC' or 'DESC', but a different value was found for entry #{index + 1}"
|
|
391
384
|
)
|
|
392
385
|
if self.auto_case_column_name(sort_entry[column_key_name], False) not in self.sortable_column_names:
|
|
393
|
-
raise
|
|
386
|
+
raise exceptions.ClientError(
|
|
394
387
|
f"Invalid sort column for entry #{index + 1}. Allowed values are: "
|
|
395
388
|
+ ", ".join(
|
|
396
389
|
[
|
|
@@ -402,7 +395,7 @@ class AdvancedSearch(SimpleSearch):
|
|
|
402
395
|
where_key_name = self.auto_case_internal_column_name("where")
|
|
403
396
|
where = request_data.get(where_key_name, [])
|
|
404
397
|
if not isinstance(where, list):
|
|
405
|
-
raise
|
|
398
|
+
raise exceptions.ClientError(
|
|
406
399
|
f"'{where_key_name}' property in request body should be a list, but I found a value of type "
|
|
407
400
|
+ where.__class__.__name
|
|
408
401
|
)
|
|
@@ -412,21 +405,21 @@ class AdvancedSearch(SimpleSearch):
|
|
|
412
405
|
value_key_name = self.auto_case_internal_column_name("value")
|
|
413
406
|
for index, where_entry in enumerate(where):
|
|
414
407
|
if not isinstance(where_entry, dict):
|
|
415
|
-
raise
|
|
408
|
+
raise exceptions.ClientError(
|
|
416
409
|
f"'{where_key_name}' should be a list of dictionaries, but entry #{index + 1} is a value of type '{where_entry.__class__.__name}', not a dict"
|
|
417
410
|
)
|
|
418
411
|
for key_name in [column_key_name, operator_key_name, value_key_name]:
|
|
419
412
|
if key_name not in where_entry:
|
|
420
|
-
raise
|
|
413
|
+
raise exceptions.ClientError(
|
|
421
414
|
f"Each entry in the where list should contain '{column_key_name}', '{operator_key_name}', and '{value_key_name}', but entry #{index + 1} is missing '{key_name}'"
|
|
422
415
|
)
|
|
423
416
|
if key_name != value_key_name and not isinstance(where_entry[key_name], str):
|
|
424
|
-
raise
|
|
417
|
+
raise exceptions.ClientError(
|
|
425
418
|
f"{key_name}' must be a string, but for entry #{index + 1} it is a value of type "
|
|
426
419
|
+ sort_entry[key_name].__class__.__name__
|
|
427
420
|
)
|
|
428
421
|
if where_entry[column_key_name] not in self.searchable_column_names:
|
|
429
|
-
raise
|
|
422
|
+
raise exceptions.ClientError(
|
|
430
423
|
f"Invalid where column for entry #{index + 1}. Allowed values are: "
|
|
431
424
|
+ ", ".join(
|
|
432
425
|
[
|
|
@@ -462,12 +455,12 @@ class AdvancedSearch(SimpleSearch):
|
|
|
462
455
|
value if operator != "in" else value[0], where_entry[operator_key_name]
|
|
463
456
|
)
|
|
464
457
|
if error_allowed_operators:
|
|
465
|
-
raise
|
|
458
|
+
raise exceptions.ClientError(
|
|
466
459
|
f"Invalid operator for entry #{index + 1}. Allowed operators are: "
|
|
467
460
|
+ ", ".join(column.allowed_search_operators(relationship_reference=column_name))
|
|
468
461
|
)
|
|
469
462
|
if error:
|
|
470
|
-
raise
|
|
463
|
+
raise exceptions.ClientError(f"Invalid search value for entry #{index + 1}: {error}")
|
|
471
464
|
|
|
472
465
|
def configure_model_from_request_data(
|
|
473
466
|
self,
|
clearskies/endpoints/callable.py
CHANGED
|
@@ -1,22 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import inspect
|
|
4
|
-
from collections import OrderedDict
|
|
5
3
|
from typing import TYPE_CHECKING, Any
|
|
6
4
|
from typing import Callable as CallableType
|
|
7
5
|
|
|
8
|
-
import
|
|
9
|
-
import clearskies.exceptions
|
|
10
|
-
from clearskies import authentication, autodoc, typing
|
|
6
|
+
from clearskies import authentication, autodoc, configs, decorators, exceptions
|
|
11
7
|
from clearskies.endpoint import Endpoint
|
|
12
8
|
from clearskies.functional import string
|
|
13
|
-
from clearskies.input_outputs import InputOutput
|
|
14
9
|
|
|
15
10
|
if TYPE_CHECKING:
|
|
16
|
-
from clearskies import Column, SecurityHeader
|
|
11
|
+
from clearskies import Column, Model, Schema, SecurityHeader
|
|
17
12
|
from clearskies.authentication import Authentication, Authorization
|
|
18
|
-
from clearskies.
|
|
19
|
-
from clearskies.schema import Schema
|
|
13
|
+
from clearskies.input_outputs import InputOutput
|
|
20
14
|
|
|
21
15
|
|
|
22
16
|
class Callable(Endpoint):
|
|
@@ -114,7 +108,7 @@ class Callable(Endpoint):
|
|
|
114
108
|
"""
|
|
115
109
|
The callable to execute when the endpoint is invoked
|
|
116
110
|
"""
|
|
117
|
-
to_call =
|
|
111
|
+
to_call = configs.Callable(default=None)
|
|
118
112
|
|
|
119
113
|
"""
|
|
120
114
|
A schema that describes the expected input from the client.
|
|
@@ -172,7 +166,7 @@ class Callable(Endpoint):
|
|
|
172
166
|
```
|
|
173
167
|
|
|
174
168
|
"""
|
|
175
|
-
input_schema =
|
|
169
|
+
input_schema = configs.Schema(default=None)
|
|
176
170
|
|
|
177
171
|
"""
|
|
178
172
|
Whether or not the return value is meant to be wrapped up in the standard clearskies response schema.
|
|
@@ -232,20 +226,20 @@ class Callable(Endpoint):
|
|
|
232
226
|
Note that you can also return strings this way instead of objects/JSON.
|
|
233
227
|
|
|
234
228
|
"""
|
|
235
|
-
return_standard_response =
|
|
229
|
+
return_standard_response = configs.Boolean(default=True)
|
|
236
230
|
|
|
237
231
|
"""
|
|
238
232
|
Set to true if the callable will be returning multiple records (used when building the auto-documentation)
|
|
239
233
|
"""
|
|
240
|
-
return_records =
|
|
234
|
+
return_records = configs.Boolean(default=False)
|
|
241
235
|
|
|
242
|
-
@
|
|
236
|
+
@decorators.parameters_to_properties
|
|
243
237
|
def __init__(
|
|
244
238
|
self,
|
|
245
239
|
to_call: CallableType,
|
|
246
240
|
url: str = "",
|
|
247
241
|
request_methods: list[str] = ["GET"],
|
|
248
|
-
model_class: type[
|
|
242
|
+
model_class: type[Model] | None = None,
|
|
249
243
|
readable_column_names: list[str] = [],
|
|
250
244
|
writeable_column_names: list[str] = [],
|
|
251
245
|
input_schema: type[Schema] | None = None,
|
|
@@ -281,7 +275,7 @@ class Callable(Endpoint):
|
|
|
281
275
|
else:
|
|
282
276
|
input_errors = self.find_input_errors_from_callable(input_output.request_data, input_output)
|
|
283
277
|
if input_errors:
|
|
284
|
-
raise
|
|
278
|
+
raise exceptions.InputErrors(input_errors)
|
|
285
279
|
response = self.di.call_function(self.to_call, **input_output.get_context_for_callables())
|
|
286
280
|
|
|
287
281
|
if not self.return_standard_response:
|