clear-skies 2.0.27__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.27.dist-info/METADATA +78 -0
- clear_skies-2.0.27.dist-info/RECORD +270 -0
- clear_skies-2.0.27.dist-info/WHEEL +4 -0
- clear_skies-2.0.27.dist-info/licenses/LICENSE +7 -0
- clearskies/__init__.py +69 -0
- clearskies/action.py +7 -0
- clearskies/authentication/__init__.py +15 -0
- clearskies/authentication/authentication.py +44 -0
- clearskies/authentication/authorization.py +23 -0
- clearskies/authentication/authorization_pass_through.py +22 -0
- clearskies/authentication/jwks.py +165 -0
- clearskies/authentication/public.py +5 -0
- clearskies/authentication/secret_bearer.py +551 -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 +67 -0
- clearskies/backends/api_backend.py +1194 -0
- clearskies/backends/backend.py +137 -0
- clearskies/backends/cursor_backend.py +339 -0
- clearskies/backends/graphql_backend.py +977 -0
- clearskies/backends/memory_backend.py +794 -0
- clearskies/backends/secrets_backend.py +100 -0
- clearskies/clients/__init__.py +5 -0
- clearskies/clients/graphql_client.py +182 -0
- clearskies/column.py +1221 -0
- clearskies/columns/__init__.py +71 -0
- clearskies/columns/audit.py +306 -0
- clearskies/columns/belongs_to_id.py +478 -0
- clearskies/columns/belongs_to_model.py +145 -0
- clearskies/columns/belongs_to_self.py +109 -0
- clearskies/columns/boolean.py +110 -0
- clearskies/columns/category_tree.py +274 -0
- clearskies/columns/category_tree_ancestors.py +51 -0
- clearskies/columns/category_tree_children.py +125 -0
- clearskies/columns/category_tree_descendants.py +48 -0
- clearskies/columns/created.py +92 -0
- clearskies/columns/created_by_authorization_data.py +114 -0
- clearskies/columns/created_by_header.py +103 -0
- clearskies/columns/created_by_ip.py +90 -0
- clearskies/columns/created_by_routing_data.py +102 -0
- clearskies/columns/created_by_user_agent.py +89 -0
- clearskies/columns/date.py +232 -0
- clearskies/columns/datetime.py +284 -0
- clearskies/columns/email.py +78 -0
- clearskies/columns/float.py +149 -0
- clearskies/columns/has_many.py +552 -0
- clearskies/columns/has_many_self.py +62 -0
- clearskies/columns/has_one.py +21 -0
- clearskies/columns/integer.py +158 -0
- clearskies/columns/json.py +126 -0
- clearskies/columns/many_to_many_ids.py +335 -0
- clearskies/columns/many_to_many_ids_with_data.py +281 -0
- clearskies/columns/many_to_many_models.py +163 -0
- clearskies/columns/many_to_many_pivots.py +132 -0
- clearskies/columns/phone.py +162 -0
- clearskies/columns/select.py +95 -0
- clearskies/columns/string.py +102 -0
- clearskies/columns/timestamp.py +164 -0
- clearskies/columns/updated.py +107 -0
- clearskies/columns/uuid.py +83 -0
- clearskies/configs/README.md +105 -0
- clearskies/configs/__init__.py +170 -0
- clearskies/configs/actions.py +43 -0
- clearskies/configs/any.py +15 -0
- clearskies/configs/any_dict.py +24 -0
- clearskies/configs/any_dict_or_callable.py +25 -0
- clearskies/configs/authentication.py +23 -0
- clearskies/configs/authorization.py +23 -0
- clearskies/configs/boolean.py +18 -0
- clearskies/configs/boolean_or_callable.py +20 -0
- clearskies/configs/callable_config.py +20 -0
- clearskies/configs/columns.py +34 -0
- clearskies/configs/conditions.py +30 -0
- clearskies/configs/config.py +26 -0
- clearskies/configs/datetime.py +20 -0
- clearskies/configs/datetime_or_callable.py +21 -0
- clearskies/configs/email.py +10 -0
- clearskies/configs/email_list.py +17 -0
- clearskies/configs/email_list_or_callable.py +17 -0
- clearskies/configs/email_or_email_list_or_callable.py +59 -0
- clearskies/configs/endpoint.py +23 -0
- clearskies/configs/endpoint_list.py +29 -0
- clearskies/configs/float.py +18 -0
- clearskies/configs/float_or_callable.py +20 -0
- clearskies/configs/headers.py +28 -0
- clearskies/configs/integer.py +18 -0
- clearskies/configs/integer_or_callable.py +20 -0
- clearskies/configs/joins.py +30 -0
- clearskies/configs/list_any_dict.py +32 -0
- clearskies/configs/list_any_dict_or_callable.py +33 -0
- clearskies/configs/model_class.py +35 -0
- clearskies/configs/model_column.py +67 -0
- clearskies/configs/model_columns.py +58 -0
- clearskies/configs/model_destination_name.py +26 -0
- clearskies/configs/model_to_id_column.py +45 -0
- clearskies/configs/readable_model_column.py +11 -0
- clearskies/configs/readable_model_columns.py +11 -0
- clearskies/configs/schema.py +23 -0
- clearskies/configs/searchable_model_columns.py +11 -0
- clearskies/configs/security_headers.py +39 -0
- clearskies/configs/select.py +28 -0
- clearskies/configs/select_list.py +49 -0
- clearskies/configs/string.py +31 -0
- clearskies/configs/string_dict.py +34 -0
- clearskies/configs/string_list.py +47 -0
- clearskies/configs/string_list_or_callable.py +48 -0
- clearskies/configs/string_or_callable.py +18 -0
- clearskies/configs/timedelta.py +20 -0
- clearskies/configs/timezone.py +20 -0
- clearskies/configs/url.py +25 -0
- clearskies/configs/validators.py +45 -0
- clearskies/configs/writeable_model_column.py +11 -0
- clearskies/configs/writeable_model_columns.py +11 -0
- clearskies/configurable.py +78 -0
- clearskies/contexts/__init__.py +11 -0
- clearskies/contexts/cli.py +130 -0
- clearskies/contexts/context.py +99 -0
- clearskies/contexts/wsgi.py +79 -0
- clearskies/contexts/wsgi_ref.py +87 -0
- clearskies/cursors/__init__.py +10 -0
- clearskies/cursors/cursor.py +161 -0
- clearskies/cursors/from_environment/__init__.py +5 -0
- clearskies/cursors/from_environment/mysql.py +51 -0
- clearskies/cursors/from_environment/postgresql.py +49 -0
- clearskies/cursors/from_environment/sqlite.py +35 -0
- clearskies/cursors/mysql.py +61 -0
- clearskies/cursors/postgresql.py +61 -0
- clearskies/cursors/sqlite.py +62 -0
- clearskies/decorators.py +33 -0
- clearskies/decorators.pyi +10 -0
- clearskies/di/__init__.py +15 -0
- clearskies/di/additional_config.py +130 -0
- clearskies/di/additional_config_auto_import.py +17 -0
- clearskies/di/di.py +948 -0
- clearskies/di/inject/__init__.py +25 -0
- clearskies/di/inject/akeyless_sdk.py +16 -0
- clearskies/di/inject/by_class.py +24 -0
- clearskies/di/inject/by_name.py +22 -0
- clearskies/di/inject/di.py +16 -0
- clearskies/di/inject/environment.py +15 -0
- clearskies/di/inject/input_output.py +19 -0
- clearskies/di/inject/logger.py +16 -0
- clearskies/di/inject/now.py +16 -0
- clearskies/di/inject/requests.py +16 -0
- clearskies/di/inject/secrets.py +15 -0
- clearskies/di/inject/utcnow.py +16 -0
- clearskies/di/inject/uuid.py +16 -0
- clearskies/di/injectable.py +32 -0
- clearskies/di/injectable_properties.py +131 -0
- clearskies/end.py +219 -0
- clearskies/endpoint.py +1303 -0
- clearskies/endpoint_group.py +333 -0
- clearskies/endpoints/__init__.py +25 -0
- clearskies/endpoints/advanced_search.py +519 -0
- clearskies/endpoints/callable.py +382 -0
- clearskies/endpoints/create.py +201 -0
- clearskies/endpoints/delete.py +133 -0
- clearskies/endpoints/get.py +267 -0
- clearskies/endpoints/health_check.py +181 -0
- clearskies/endpoints/list.py +567 -0
- clearskies/endpoints/restful_api.py +417 -0
- clearskies/endpoints/schema.py +185 -0
- clearskies/endpoints/simple_search.py +279 -0
- clearskies/endpoints/update.py +188 -0
- clearskies/environment.py +106 -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/json.py +47 -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 +157 -0
- clearskies/input_outputs/exceptions/__init__.py +7 -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 +54 -0
- clearskies/input_outputs/input_output.py +116 -0
- clearskies/input_outputs/programmatic.py +62 -0
- clearskies/input_outputs/py.typed +0 -0
- clearskies/input_outputs/wsgi.py +80 -0
- clearskies/loggable.py +19 -0
- clearskies/model.py +2039 -0
- clearskies/py.typed +0 -0
- clearskies/query/__init__.py +12 -0
- clearskies/query/condition.py +228 -0
- clearskies/query/join.py +136 -0
- clearskies/query/query.py +195 -0
- clearskies/query/sort.py +27 -0
- clearskies/schema.py +82 -0
- clearskies/secrets/__init__.py +7 -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 +507 -0
- clearskies/secrets/exceptions/__init__.py +7 -0
- clearskies/secrets/exceptions/not_found_error.py +2 -0
- clearskies/secrets/exceptions/permissions_error.py +2 -0
- clearskies/secrets/secrets.py +39 -0
- clearskies/security_header.py +17 -0
- clearskies/security_headers/__init__.py +11 -0
- clearskies/security_headers/cache_control.py +68 -0
- clearskies/security_headers/cors.py +51 -0
- clearskies/security_headers/csp.py +95 -0
- clearskies/security_headers/hsts.py +23 -0
- clearskies/security_headers/x_content_type_options.py +0 -0
- clearskies/security_headers/x_frame_options.py +0 -0
- clearskies/typing.py +11 -0
- clearskies/validator.py +36 -0
- clearskies/validators/__init__.py +33 -0
- clearskies/validators/after_column.py +61 -0
- clearskies/validators/before_column.py +15 -0
- clearskies/validators/in_the_future.py +29 -0
- clearskies/validators/in_the_future_at_least.py +13 -0
- clearskies/validators/in_the_future_at_most.py +12 -0
- clearskies/validators/in_the_past.py +29 -0
- clearskies/validators/in_the_past_at_least.py +12 -0
- clearskies/validators/in_the_past_at_most.py +12 -0
- clearskies/validators/maximum_length.py +25 -0
- clearskies/validators/maximum_value.py +28 -0
- clearskies/validators/minimum_length.py +25 -0
- clearskies/validators/minimum_value.py +28 -0
- clearskies/validators/required.py +32 -0
- clearskies/validators/timedelta.py +58 -0
- clearskies/validators/unique.py +28 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
from typing import Callable as CallableType
|
|
5
|
+
|
|
6
|
+
from clearskies import authentication, autodoc, configs, decorators, exceptions
|
|
7
|
+
from clearskies.endpoint import Endpoint
|
|
8
|
+
from clearskies.functional import string
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from clearskies import Column, Model, Schema, SecurityHeader
|
|
12
|
+
from clearskies.authentication import Authentication, Authorization
|
|
13
|
+
from clearskies.input_outputs import InputOutput
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Callable(Endpoint):
|
|
17
|
+
"""
|
|
18
|
+
An endpoint that executes a user-defined function.
|
|
19
|
+
|
|
20
|
+
The Callable endpoint does exactly that - you provide a function that will be called when the endpoint is invoked. Like
|
|
21
|
+
all callables invoked by clearskies, you can request any defined dependency that can be provided by the clearskies
|
|
22
|
+
framework.
|
|
23
|
+
|
|
24
|
+
Whatever you return will be returned to the client. By default, the return value is sent along in the `data` parameter
|
|
25
|
+
of the standard clearskies response. To suppress this behavior, set `return_standard_response` to `False`. You can also
|
|
26
|
+
return a model instance, a model query, or a list of model instances and the callable endpoint will automatically return
|
|
27
|
+
the columns specified in `readable_column_names` to the client.
|
|
28
|
+
|
|
29
|
+
Here's a basic working example:
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import clearskies
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class User(clearskies.Model):
|
|
36
|
+
id_column_name = "id"
|
|
37
|
+
backend = clearskies.backends.MemoryBackend()
|
|
38
|
+
id = clearskies.columns.Uuid()
|
|
39
|
+
first_name = clearskies.columns.String()
|
|
40
|
+
last_name = clearskies.columns.String()
|
|
41
|
+
age = clearskies.columns.Integer()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def my_users_callable(users: User):
|
|
45
|
+
bob = users.create({"first_name": "Bob", "last_name": "Brown", "age": 10})
|
|
46
|
+
jane = users.create({"first_name": "Jane", "last_name": "Brown", "age": 10})
|
|
47
|
+
alice = users.create({"first_name": "Alice", "last_name": "Green", "age": 10})
|
|
48
|
+
|
|
49
|
+
return jane
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
my_users = clearskies.endpoints.Callable(
|
|
53
|
+
my_users_callable,
|
|
54
|
+
model_class=User,
|
|
55
|
+
readable_column_names=["id", "first_name", "last_name"],
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
wsgi = clearskies.contexts.WsgiRef(
|
|
59
|
+
my_users,
|
|
60
|
+
classes=[User],
|
|
61
|
+
)
|
|
62
|
+
wsgi()
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
If you run the above script and invoke the server:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
$ curl 'http://localhost:8080' | jq
|
|
69
|
+
{
|
|
70
|
+
"status": "success",
|
|
71
|
+
"error": "",
|
|
72
|
+
"data": {
|
|
73
|
+
"id": "4a35a616-3d57-456f-8306-7c610a5e80e1",
|
|
74
|
+
"first_name": "Jane",
|
|
75
|
+
"last_name": "Brown"
|
|
76
|
+
},
|
|
77
|
+
"pagination": {},
|
|
78
|
+
"input_errors": {}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The above example demonstrates returning a model and using readable_column_names to decide what is actually sent to the client
|
|
83
|
+
(note that age is left out of the response). The advantage of doing it this way is that clearskies can also auto-generate
|
|
84
|
+
OpenAPI documentation using this strategy. Of course, you can also just return any arbitrary data you want. If you do return
|
|
85
|
+
custom data, and also want your API to be documented, you can pass a schema along to output_schema so clearskies can document
|
|
86
|
+
it:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
import clearskies
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class DogResponse(clearskies.Schema):
|
|
93
|
+
species = (clearskies.columns.String(),)
|
|
94
|
+
nickname = (clearskies.columns.String(),)
|
|
95
|
+
level = (clearskies.columns.Integer(),)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
clearskies.contexts.WsgiRef(
|
|
99
|
+
clearskies.endpoints.Callable(
|
|
100
|
+
lambda: {"species": "dog", "nickname": "Spot", "level": 100},
|
|
101
|
+
output_schema=DogResponse,
|
|
102
|
+
)
|
|
103
|
+
)()
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
"""
|
|
109
|
+
The callable to execute when the endpoint is invoked
|
|
110
|
+
"""
|
|
111
|
+
to_call = configs.Callable(default=None)
|
|
112
|
+
|
|
113
|
+
"""
|
|
114
|
+
A schema that describes the expected input from the client.
|
|
115
|
+
|
|
116
|
+
Note that if this is specified it will take precedence over writeable_column_names and model_class, which
|
|
117
|
+
can also be used to specify the expected input.
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
import clearskies
|
|
121
|
+
|
|
122
|
+
class ExpectedInput(clearskies.Schema):
|
|
123
|
+
first_name = clearskies.columns.String(validators=[clearskies.validators.Required()])
|
|
124
|
+
last_name = clearskies.columns.String()
|
|
125
|
+
age = clearskies.columns.Integer(validators=[clearskies.validators.MinimumValue(0)])
|
|
126
|
+
|
|
127
|
+
reflect = clearskies.endpoints.Callable(
|
|
128
|
+
lambda request_data: request_data,
|
|
129
|
+
request_methods=["POST"],
|
|
130
|
+
input_schema=ExpectedInput,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
wsgi = clearskies.contexts.WsgiRef(reflect)
|
|
134
|
+
wsgi()
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
And then valid and invalid requests:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
$ curl http://localhost:8080 -d '{"first_name":"Jane","last_name":"Doe","age":1}' | jq
|
|
141
|
+
{
|
|
142
|
+
"status": "success",
|
|
143
|
+
"error": "",
|
|
144
|
+
"data": {
|
|
145
|
+
"first_name": "Jane",
|
|
146
|
+
"last_name": "Doe",
|
|
147
|
+
"age": 1
|
|
148
|
+
},
|
|
149
|
+
"pagination": {},
|
|
150
|
+
"input_errors": {}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
$ curl http://localhost:8080 -d '{"last_name":10,"age":-1,"check":"cool"}' | jq
|
|
154
|
+
{
|
|
155
|
+
"status": "input_errors",
|
|
156
|
+
"error": "",
|
|
157
|
+
"data": [],
|
|
158
|
+
"pagination": {},
|
|
159
|
+
"input_errors": {
|
|
160
|
+
"age": "'age' must be at least 0.",
|
|
161
|
+
"first_name": "'first_name' is required.",
|
|
162
|
+
"last_name": "value should be a string",
|
|
163
|
+
"check": "Input column check is not an allowed input column."
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
"""
|
|
169
|
+
input_schema = configs.Schema(default=None)
|
|
170
|
+
|
|
171
|
+
"""
|
|
172
|
+
Whether or not the return value is meant to be wrapped up in the standard clearskies response schema.
|
|
173
|
+
|
|
174
|
+
With the standard response schema, the return value of the function will be placed in the `data` portion of
|
|
175
|
+
the standard clearskies response:
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
import clearskies
|
|
179
|
+
|
|
180
|
+
wsgi = clearskies.contexts.WsgiRef(
|
|
181
|
+
clearskies.endpoints.Callable(
|
|
182
|
+
lambda: {"hello": "world"},
|
|
183
|
+
return_standard_response=True, # the default value
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
wsgi()
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Results in:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
$ curl http://localhost:8080 | jq
|
|
193
|
+
{
|
|
194
|
+
"status": "success",
|
|
195
|
+
"error": "",
|
|
196
|
+
"data": {
|
|
197
|
+
"hello": "world"
|
|
198
|
+
},
|
|
199
|
+
"pagination": {},
|
|
200
|
+
"input_errors": {}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
But if you want to build your own response:
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
import clearskies
|
|
207
|
+
|
|
208
|
+
wsgi = clearskies.contexts.WsgiRef(
|
|
209
|
+
clearskies.endpoints.Callable(
|
|
210
|
+
lambda: {"hello": "world"},
|
|
211
|
+
return_standard_response=False,
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
wsgi()
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Results in:
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
$ curl http://localhost:8080 | jq
|
|
221
|
+
{
|
|
222
|
+
"hello": "world"
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Note that you can also return strings this way instead of objects/JSON.
|
|
227
|
+
|
|
228
|
+
"""
|
|
229
|
+
return_standard_response = configs.Boolean(default=True)
|
|
230
|
+
|
|
231
|
+
"""
|
|
232
|
+
Set to true if the callable will be returning multiple records (used when building the auto-documentation)
|
|
233
|
+
"""
|
|
234
|
+
return_records = configs.Boolean(default=False)
|
|
235
|
+
|
|
236
|
+
@decorators.parameters_to_properties
|
|
237
|
+
def __init__(
|
|
238
|
+
self,
|
|
239
|
+
to_call: CallableType,
|
|
240
|
+
url: str = "",
|
|
241
|
+
request_methods: list[str] = ["GET"],
|
|
242
|
+
model_class: type[Model] | None = None,
|
|
243
|
+
readable_column_names: list[str] = [],
|
|
244
|
+
writeable_column_names: list[str] = [],
|
|
245
|
+
input_schema: type[Schema] | None = None,
|
|
246
|
+
output_schema: type[Schema] | None = None,
|
|
247
|
+
input_validation_callable: CallableType | None = None,
|
|
248
|
+
return_standard_response: bool = True,
|
|
249
|
+
return_records: bool = False,
|
|
250
|
+
response_headers: list[str | CallableType[..., list[str]]] = [],
|
|
251
|
+
output_map: CallableType[..., dict[str, Any]] | None = None,
|
|
252
|
+
column_overrides: dict[str, Column] = {},
|
|
253
|
+
internal_casing: str = "snake_case",
|
|
254
|
+
external_casing: str = "snake_case",
|
|
255
|
+
security_headers: list[SecurityHeader] = [],
|
|
256
|
+
description: str = "",
|
|
257
|
+
authentication: Authentication = authentication.Public(),
|
|
258
|
+
authorization: Authorization = authentication.Authorization(),
|
|
259
|
+
):
|
|
260
|
+
# we need to call the parent but don't have to pass along any of our kwargs. They are all optional in our parent, and our parent class
|
|
261
|
+
# just stores them in parameters, which we have already done. However, the parent does do some extra initialization stuff that we need,
|
|
262
|
+
# which is why we have to call the parent.
|
|
263
|
+
super().__init__()
|
|
264
|
+
|
|
265
|
+
if self.input_schema and not self.writeable_column_names:
|
|
266
|
+
self.writeable_column_names = list(self.input_schema.get_columns().keys())
|
|
267
|
+
|
|
268
|
+
def handle(self, input_output: InputOutput):
|
|
269
|
+
if self.writeable_column_names or self.input_schema:
|
|
270
|
+
self.validate_input_against_schema(
|
|
271
|
+
self.get_request_data(input_output),
|
|
272
|
+
input_output,
|
|
273
|
+
self.input_schema if self.input_schema else self.model_class,
|
|
274
|
+
)
|
|
275
|
+
else:
|
|
276
|
+
input_errors = self.find_input_errors_from_callable(input_output.request_data, input_output)
|
|
277
|
+
if input_errors:
|
|
278
|
+
raise exceptions.InputErrors(input_errors)
|
|
279
|
+
response = self.di.call_function(self.to_call, **input_output.get_context_for_callables())
|
|
280
|
+
|
|
281
|
+
if not self.return_standard_response:
|
|
282
|
+
return input_output.respond(response, 200)
|
|
283
|
+
|
|
284
|
+
# did the developer return a model?
|
|
285
|
+
if self.model_class and isinstance(response, self.model_class):
|
|
286
|
+
# and is it a query or a single model?
|
|
287
|
+
if response._data:
|
|
288
|
+
return self.success(input_output, self.model_as_json(response, input_output))
|
|
289
|
+
else:
|
|
290
|
+
# with a query we can also get pagination data, maybe?
|
|
291
|
+
converted_models = [self.model_as_json(model, input_output) for model in response]
|
|
292
|
+
return self.success(
|
|
293
|
+
input_output,
|
|
294
|
+
converted_models,
|
|
295
|
+
number_results=len(response) if response.backend.can_count else None,
|
|
296
|
+
next_page=response.next_page_data(),
|
|
297
|
+
limit=response.get_query().limit,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# or did they return a list of models?
|
|
301
|
+
if isinstance(response, list) and all(isinstance(item, self.model_class) for item in response):
|
|
302
|
+
return self.success(input_output, [self.model_as_json(model, input_output) for model in response])
|
|
303
|
+
|
|
304
|
+
# if none of the above, just return the data
|
|
305
|
+
return self.success(input_output, response)
|
|
306
|
+
|
|
307
|
+
def documentation(self) -> list[autodoc.request.Request]:
|
|
308
|
+
output_schema = self.output_schema if self.output_schema else self.model_class
|
|
309
|
+
nice_model = string.camel_case_to_words(output_schema.__name__)
|
|
310
|
+
|
|
311
|
+
schema_model_name = string.camel_case_to_snake_case(output_schema.__name__)
|
|
312
|
+
output_data_schema = (
|
|
313
|
+
self.documentation_data_schema(output_schema, self.readable_column_names)
|
|
314
|
+
if self.readable_column_names
|
|
315
|
+
else []
|
|
316
|
+
)
|
|
317
|
+
output_autodoc = (
|
|
318
|
+
autodoc.schema.Object(
|
|
319
|
+
self.auto_case_internal_column_name("data"),
|
|
320
|
+
children=output_data_schema,
|
|
321
|
+
model_name=schema_model_name if self.readable_column_names else "",
|
|
322
|
+
),
|
|
323
|
+
)
|
|
324
|
+
if self.return_records:
|
|
325
|
+
output_autodoc.name = nice_model # type: ignore
|
|
326
|
+
output_autodoc = autodoc.schema.Array(
|
|
327
|
+
self.auto_case_internal_column_name("data"),
|
|
328
|
+
output_autodoc,
|
|
329
|
+
) # type: ignore
|
|
330
|
+
|
|
331
|
+
authentication = self.authentication
|
|
332
|
+
standard_error_responses = []
|
|
333
|
+
if not getattr(authentication, "is_public", False):
|
|
334
|
+
standard_error_responses.append(self.documentation_access_denied_response())
|
|
335
|
+
if getattr(authentication, "can_authorize", False):
|
|
336
|
+
standard_error_responses.append(self.documentation_unauthorized_response())
|
|
337
|
+
if self.writeable_column_names:
|
|
338
|
+
standard_error_responses.append(self.documentation_input_error_response())
|
|
339
|
+
|
|
340
|
+
return [
|
|
341
|
+
autodoc.request.Request(
|
|
342
|
+
self.description,
|
|
343
|
+
[
|
|
344
|
+
self.documentation_success_response(
|
|
345
|
+
output_autodoc, # type: ignore
|
|
346
|
+
description=self.description,
|
|
347
|
+
include_pagination=self.return_records,
|
|
348
|
+
),
|
|
349
|
+
*standard_error_responses,
|
|
350
|
+
self.documentation_generic_error_response(),
|
|
351
|
+
],
|
|
352
|
+
relative_path=self.url,
|
|
353
|
+
request_methods=self.request_methods,
|
|
354
|
+
parameters=[
|
|
355
|
+
*self.documentation_request_parameters(),
|
|
356
|
+
*self.documentation_url_parameters(),
|
|
357
|
+
],
|
|
358
|
+
root_properties={
|
|
359
|
+
"security": self.documentation_request_security(),
|
|
360
|
+
},
|
|
361
|
+
),
|
|
362
|
+
]
|
|
363
|
+
|
|
364
|
+
def documentation_request_parameters(self) -> list[autodoc.request.Parameter]:
|
|
365
|
+
if not self.writeable_column_names:
|
|
366
|
+
return []
|
|
367
|
+
|
|
368
|
+
return self.standard_json_request_parameters(self.input_schema if self.input_schema else self.model_class)
|
|
369
|
+
|
|
370
|
+
def documentation_models(self) -> dict[str, autodoc.schema.Schema]:
|
|
371
|
+
if not self.readable_column_names:
|
|
372
|
+
return {}
|
|
373
|
+
|
|
374
|
+
output_schema = self.output_schema if self.output_schema else self.model_class
|
|
375
|
+
schema_model_name = string.camel_case_to_snake_case(output_schema.__name__)
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
schema_model_name: autodoc.schema.Object(
|
|
379
|
+
self.auto_case_internal_column_name("data"),
|
|
380
|
+
children=self.documentation_data_schema(output_schema, self.readable_column_names),
|
|
381
|
+
),
|
|
382
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
4
|
+
|
|
5
|
+
from clearskies import authentication, autodoc, decorators, exceptions
|
|
6
|
+
from clearskies.endpoint import Endpoint
|
|
7
|
+
from clearskies.functional import string
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from clearskies import Column, Schema, SecurityHeader
|
|
11
|
+
from clearskies.input_outputs import InputOutput
|
|
12
|
+
from clearskies.model import Model
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Create(Endpoint):
|
|
16
|
+
"""
|
|
17
|
+
An endpoint to create a record.
|
|
18
|
+
|
|
19
|
+
This endpoint accepts user input and uses it to create a record for the given model class. You have
|
|
20
|
+
to provide the model class, which columns the end-user can set, and which columns get returned
|
|
21
|
+
to the client. The column definitions in the model class are used to strictly validate the user
|
|
22
|
+
input. Here's a basic example of a model class with the create endpoint in use:
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import clearskies
|
|
26
|
+
from clearskies import validators, columns
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MyAwesomeModel(clearskies.Model):
|
|
30
|
+
id_column_name = "id"
|
|
31
|
+
backend = clearskies.backends.MemoryBackend()
|
|
32
|
+
|
|
33
|
+
id = columns.Uuid()
|
|
34
|
+
name = clearskies.columns.String(
|
|
35
|
+
validators=[
|
|
36
|
+
validators.Required(),
|
|
37
|
+
validators.MaximumLength(50),
|
|
38
|
+
]
|
|
39
|
+
)
|
|
40
|
+
email = columns.Email(validators=[validators.Unique()])
|
|
41
|
+
some_number = columns.Integer()
|
|
42
|
+
expires_at = columns.Date()
|
|
43
|
+
created_at = columns.Created()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
wsgi = clearskies.contexts.WsgiRef(
|
|
47
|
+
clearskies.endpoints.Create(
|
|
48
|
+
MyAwesomeModel,
|
|
49
|
+
readable_column_names=["id", "name", "email", "some_number", "expires_at", "created_at"],
|
|
50
|
+
writeable_column_names=["name", "email", "some_number", "expires_at"],
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
wsgi()
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The following shows how to invoke it, and demonstrates the strict input validation that happens as part of the
|
|
57
|
+
process:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
$ curl 'http://localhost:8080/' -d '{"name":"Example", "email":"test@example.com","some_number":5,"expires_at":"2024-12-31"}' | jq
|
|
61
|
+
{
|
|
62
|
+
"status": "success",
|
|
63
|
+
"error": "",
|
|
64
|
+
"data": {
|
|
65
|
+
"id": "74eda1c6-fe66-44ec-9246-758d16e1a304",
|
|
66
|
+
"name": "Example",
|
|
67
|
+
"email": "test@example.com",
|
|
68
|
+
"some_number": 5,
|
|
69
|
+
"expires_at": "2024-12-31",
|
|
70
|
+
"created_at": "2025-05-23T16:36:30+00:00"
|
|
71
|
+
},
|
|
72
|
+
"pagination": {},
|
|
73
|
+
"input_errors": {}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
$ curl 'http://localhost:8080/' -d '{"name":"", "email":"test@example.com","some_number":"asdf","expires_at":"not-a-date", "not_a_column": "sup"}' | jq
|
|
77
|
+
{
|
|
78
|
+
"status": "input_errors",
|
|
79
|
+
"error": "",
|
|
80
|
+
"data": [],
|
|
81
|
+
"pagination": {},
|
|
82
|
+
"input_errors": {
|
|
83
|
+
"name": "'name' is required.",
|
|
84
|
+
"email": "Invalid value for 'email': the given value already exists, and must be unique.",
|
|
85
|
+
"some_number": "value should be an integer",
|
|
86
|
+
"expires_at": "given value did not appear to be a valid date",
|
|
87
|
+
"not_a_column": "Input column not_a_column is not an allowed input column."
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The first call successfully creates a new record. The second call fails with a variety of error messages:
|
|
93
|
+
|
|
94
|
+
1. A name wasn't provided by the model class marked this as required
|
|
95
|
+
2. We provided the same email address again, but this column is marked as unique
|
|
96
|
+
3. The number provided in `some_number` wasn't actually a number
|
|
97
|
+
4. The provided value for `expires_at` wasn't actually a date.
|
|
98
|
+
5. We provided an extra column (`not_a_column`) that wasn't in the list of allowed columns.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
@decorators.parameters_to_properties
|
|
102
|
+
def __init__(
|
|
103
|
+
self,
|
|
104
|
+
model_class: type[Model],
|
|
105
|
+
writeable_column_names: list[str],
|
|
106
|
+
readable_column_names: list[str],
|
|
107
|
+
input_validation_callable: Callable | None = None,
|
|
108
|
+
include_routing_data_in_request_data: bool = False,
|
|
109
|
+
url: str = "",
|
|
110
|
+
request_methods: list[str] = ["POST"],
|
|
111
|
+
response_headers: list[str | Callable[..., list[str]]] = [],
|
|
112
|
+
output_map: Callable[..., dict[str, Any]] | None = None,
|
|
113
|
+
output_schema: Schema | None = None,
|
|
114
|
+
column_overrides: dict[str, Column] = {},
|
|
115
|
+
internal_casing: str = "snake_case",
|
|
116
|
+
external_casing: str = "snake_case",
|
|
117
|
+
security_headers: list[SecurityHeader] = [],
|
|
118
|
+
description: str = "",
|
|
119
|
+
authentication: authentication.Authentication = authentication.Public(),
|
|
120
|
+
authorization: authentication.Authorization = authentication.Authorization(),
|
|
121
|
+
):
|
|
122
|
+
# a bit weird, but we have to do this because the default in the above definition is different than
|
|
123
|
+
# the default set on the request_mehtods config in the bsae endpoint class. parameters_to_properties will copy
|
|
124
|
+
# parameters to properties, but only for things set by the developer - not for default values set in the kwarg
|
|
125
|
+
# definitions. Therefore, we always set it here to make sure we user our default, not the one in the base class.
|
|
126
|
+
self.request_methods = request_methods
|
|
127
|
+
|
|
128
|
+
# we need to call the parent but don't have to pass along any of our kwargs. They are all optional in our parent, and our parent class
|
|
129
|
+
# just stores them in parameters, which we have already done. However, the parent does do some extra initialization stuff that we need,
|
|
130
|
+
# which is why we have to call the parent.
|
|
131
|
+
super().__init__()
|
|
132
|
+
|
|
133
|
+
def handle(self, input_output: InputOutput) -> Any:
|
|
134
|
+
request_data = self.get_request_data(input_output)
|
|
135
|
+
if not request_data and input_output.has_body():
|
|
136
|
+
raise exceptions.ClientError("Request body was not valid JSON")
|
|
137
|
+
self.validate_input_against_schema(request_data, input_output, self.model_class)
|
|
138
|
+
new_model = self.model.create(request_data, columns=self.columns)
|
|
139
|
+
return self.success(input_output, self.model_as_json(new_model, input_output))
|
|
140
|
+
|
|
141
|
+
def documentation(self) -> list[autodoc.request.Request]:
|
|
142
|
+
output_schema = self.model_class
|
|
143
|
+
nice_model = string.camel_case_to_words(output_schema.__name__)
|
|
144
|
+
|
|
145
|
+
schema_model_name = string.camel_case_to_snake_case(output_schema.__name__)
|
|
146
|
+
output_data_schema = self.documentation_data_schema(output_schema, self.readable_column_names)
|
|
147
|
+
output_autodoc = (
|
|
148
|
+
autodoc.schema.Object(
|
|
149
|
+
self.auto_case_internal_column_name("data"), children=output_data_schema, model_name=schema_model_name
|
|
150
|
+
),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
authentication = self.authentication
|
|
154
|
+
# Many swagger UIs will only allow one response per status code, and we use the same status code (200)
|
|
155
|
+
# for both a success response and an input error response. This could be fixed by changing the status
|
|
156
|
+
# code for input error responses, but there's not actually a great HTTP status code for that, so :shrug:
|
|
157
|
+
# standard_error_responses = [self.documentation_input_error_response()]
|
|
158
|
+
standard_error_responses = []
|
|
159
|
+
if not getattr(authentication, "is_public", False):
|
|
160
|
+
standard_error_responses.append(self.documentation_access_denied_response())
|
|
161
|
+
if getattr(authentication, "can_authorize", False):
|
|
162
|
+
standard_error_responses.append(self.documentation_unauthorized_response())
|
|
163
|
+
|
|
164
|
+
return [
|
|
165
|
+
autodoc.request.Request(
|
|
166
|
+
self.description,
|
|
167
|
+
[
|
|
168
|
+
self.documentation_success_response(
|
|
169
|
+
output_autodoc, # type: ignore
|
|
170
|
+
description=self.description,
|
|
171
|
+
),
|
|
172
|
+
*standard_error_responses,
|
|
173
|
+
self.documentation_generic_error_response(),
|
|
174
|
+
],
|
|
175
|
+
relative_path=self.url,
|
|
176
|
+
request_methods=self.request_methods,
|
|
177
|
+
parameters=[
|
|
178
|
+
*self.documentation_request_parameters(),
|
|
179
|
+
*self.documentation_url_parameters(),
|
|
180
|
+
],
|
|
181
|
+
root_properties={
|
|
182
|
+
"security": self.documentation_request_security(),
|
|
183
|
+
},
|
|
184
|
+
),
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
def documentation_request_parameters(self) -> list[autodoc.request.Parameter]:
|
|
188
|
+
return [
|
|
189
|
+
*self.standard_json_request_parameters(self.model_class),
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
def documentation_models(self) -> dict[str, autodoc.schema.Schema]:
|
|
193
|
+
output_schema = self.output_schema if self.output_schema else self.model_class
|
|
194
|
+
schema_model_name = string.camel_case_to_snake_case(output_schema.__name__)
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
schema_model_name: autodoc.schema.Object(
|
|
198
|
+
self.auto_case_internal_column_name("data"),
|
|
199
|
+
children=self.documentation_data_schema(output_schema, self.readable_column_names),
|
|
200
|
+
),
|
|
201
|
+
}
|