clear-skies 2.0.5__py3-none-any.whl → 2.0.7__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.7.dist-info}/METADATA +1 -1
- clear_skies-2.0.7.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 +15 -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.7.dist-info}/WHEEL +0 -0
- {clear_skies-2.0.5.dist-info → clear_skies-2.0.7.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Any, Callable
|
|
3
|
+
|
|
4
|
+
import clearskies.decorators
|
|
5
|
+
import clearskies.typing
|
|
6
|
+
from clearskies import configs
|
|
7
|
+
from clearskies.columns.string import String
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Phone(String):
|
|
11
|
+
"""
|
|
12
|
+
A string column that stores a phone number.
|
|
13
|
+
|
|
14
|
+
The main difference between this and a plain string column is that this will validate that the string contains
|
|
15
|
+
a phone number (containing only digits, dashes, spaces, plus sign, and parenthesis) of the appropriate length.
|
|
16
|
+
When persisting the value to the backend, this column removes all non-digit characters.
|
|
17
|
+
|
|
18
|
+
If you also set the usa_only flag to true then it will also validate that it is a valid US number containing
|
|
19
|
+
9 digits and, optionally, a leading `1`. Example:
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import clearskies
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class User(clearskies.Model):
|
|
26
|
+
id_column_name = "id"
|
|
27
|
+
backend = clearskies.backends.MemoryBackend()
|
|
28
|
+
|
|
29
|
+
id = clearskies.columns.Uuid()
|
|
30
|
+
name = clearskies.columns.String()
|
|
31
|
+
phone = clearskies.columns.Phone(usa_only=True)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
wsgi = clearskies.contexts.WsgiRef(
|
|
35
|
+
clearskies.endpoints.Create(
|
|
36
|
+
User,
|
|
37
|
+
writeable_column_names=["name", "phone"],
|
|
38
|
+
readable_column_names=["id", "name", "phone"],
|
|
39
|
+
),
|
|
40
|
+
)
|
|
41
|
+
wsgi()
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Which you can invoke:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
$ curl http://localhost:8080 -d '{"name":"John Doe", "phone": "+1 (555) 451-1234"}' | jq
|
|
48
|
+
{
|
|
49
|
+
"status": "success",
|
|
50
|
+
"error": "",
|
|
51
|
+
"data": {
|
|
52
|
+
"id": "e2b4bdad-b70f-4d44-a94c-0e265868b4d2",
|
|
53
|
+
"name": "John Doe",
|
|
54
|
+
"phone": "15554511234"
|
|
55
|
+
},
|
|
56
|
+
"pagination": {},
|
|
57
|
+
"input_errors": {}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
$ curl http://localhost:8080 -d '{"name":"John Doe", "phone": "555 451-1234"}' | jq
|
|
61
|
+
{
|
|
62
|
+
"status": "success",
|
|
63
|
+
"error": "",
|
|
64
|
+
"data": {
|
|
65
|
+
"id": "aea34022-4b75-4eed-ac92-65fa4f4511ae",
|
|
66
|
+
"name": "John Doe",
|
|
67
|
+
"phone": "5554511234"
|
|
68
|
+
},
|
|
69
|
+
"pagination": {},
|
|
70
|
+
"input_errors": {}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
$ curl http://localhost:8080 -d '{"name":"John Doe", "phone": "555 451-12341"}' | jq
|
|
75
|
+
{
|
|
76
|
+
"status": "input_errors",
|
|
77
|
+
"error": "",
|
|
78
|
+
"data": [],
|
|
79
|
+
"pagination": {},
|
|
80
|
+
"input_errors": {
|
|
81
|
+
"phone": "Invalid phone number"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
$ curl http://localhost:8080 -d '{"name":"John Doe", "phone": "1-2-3-4 asdf"}' | jq
|
|
86
|
+
{
|
|
87
|
+
"status": "input_errors",
|
|
88
|
+
"error": "",
|
|
89
|
+
"data": [],
|
|
90
|
+
"pagination": {},
|
|
91
|
+
"input_errors": {
|
|
92
|
+
"phone": "Invalid phone number"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
""" Whether or not to allow non-USA numbers. """
|
|
99
|
+
usa_only = configs.Boolean(default=True)
|
|
100
|
+
_descriptor_config_map = None
|
|
101
|
+
|
|
102
|
+
@clearskies.decorators.parameters_to_properties
|
|
103
|
+
def __init__(
|
|
104
|
+
self,
|
|
105
|
+
usa_only: bool = True,
|
|
106
|
+
default: str | None = None,
|
|
107
|
+
setable: str | Callable[..., str] | None = None,
|
|
108
|
+
is_readable: bool = True,
|
|
109
|
+
is_writeable: bool = True,
|
|
110
|
+
is_searchable: bool = True,
|
|
111
|
+
is_temporary: bool = False,
|
|
112
|
+
validators: clearskies.typing.validator | list[clearskies.typing.validator] = [],
|
|
113
|
+
on_change_pre_save: clearskies.typing.action | list[clearskies.typing.action] = [],
|
|
114
|
+
on_change_post_save: clearskies.typing.action | list[clearskies.typing.action] = [],
|
|
115
|
+
on_change_save_finished: clearskies.typing.action | list[clearskies.typing.action] = [],
|
|
116
|
+
created_by_source_type: str = "",
|
|
117
|
+
created_by_source_key: str = "",
|
|
118
|
+
created_by_source_strict: bool = True,
|
|
119
|
+
):
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
def to_backend(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
123
|
+
if not data.get(self.name):
|
|
124
|
+
return data
|
|
125
|
+
|
|
126
|
+
# phone numbers are stored as only digits.
|
|
127
|
+
return {**data, **{self.name: re.sub(r"\D", "", data[self.name])}}
|
|
128
|
+
|
|
129
|
+
def input_error_for_value(self, value: str, operator: str | None = None) -> str:
|
|
130
|
+
if type(value) != str:
|
|
131
|
+
return f"Value must be a string for {self.name}"
|
|
132
|
+
|
|
133
|
+
# we'll allow spaces, dashes, parenthesis, dashes, and plus signs.
|
|
134
|
+
# if there is anything else then it's not a valid phone number.
|
|
135
|
+
# However, we don't do more detailed validation, because I'm too lazy to
|
|
136
|
+
# figure out what is and is not a valid phone number, especially when
|
|
137
|
+
# you get to the world of international numbers.
|
|
138
|
+
if re.search(r"[^\d \-()+]", value):
|
|
139
|
+
return "Invalid phone number"
|
|
140
|
+
|
|
141
|
+
# for some final validation (especially US numbers) work only with the digits.
|
|
142
|
+
value = re.sub(r"\D", "", value)
|
|
143
|
+
|
|
144
|
+
if len(value) > 15:
|
|
145
|
+
return "Invalid phone number"
|
|
146
|
+
|
|
147
|
+
# we can't be too short unless we're doing a fuzzy search
|
|
148
|
+
if len(value) < 10 and operator and operator.lower() != "like":
|
|
149
|
+
return "Invalid phone number"
|
|
150
|
+
|
|
151
|
+
if self.usa_only:
|
|
152
|
+
if len(value) > 11:
|
|
153
|
+
return "Invalid phone number"
|
|
154
|
+
if value[0] == "1" and len(value) != 11:
|
|
155
|
+
return "Invalid phone number"
|
|
156
|
+
if value[0] != "1" and len(value) != 10:
|
|
157
|
+
return "Invalid phone number"
|
|
158
|
+
|
|
159
|
+
return ""
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from typing import Callable
|
|
2
|
+
|
|
3
|
+
import clearskies.decorators
|
|
4
|
+
import clearskies.typing
|
|
5
|
+
from clearskies import configs
|
|
6
|
+
from clearskies.columns.string import String
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Select(String):
|
|
10
|
+
"""
|
|
11
|
+
A string column but, when writeable via an endpoint, only specific values are allowed.
|
|
12
|
+
|
|
13
|
+
Note: the allowed values are case sensitive.
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import clearskies
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Order(clearskies.Model):
|
|
20
|
+
id_column_name = "id"
|
|
21
|
+
backend = clearskies.backends.MemoryBackend()
|
|
22
|
+
|
|
23
|
+
id = clearskies.columns.Uuid()
|
|
24
|
+
total = clearskies.columns.Float()
|
|
25
|
+
status = clearskies.columns.Select(["Open", "Processing", "Shipped", "Complete"])
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
wsgi = clearskies.contexts.WsgiRef(
|
|
29
|
+
clearskies.endpoints.Create(
|
|
30
|
+
Order,
|
|
31
|
+
writeable_column_names=["total", "status"],
|
|
32
|
+
readable_column_names=["id", "total", "status"],
|
|
33
|
+
),
|
|
34
|
+
)
|
|
35
|
+
wsgi()
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
And when invoked:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
$ curl http://localhost:8080 -d '{"total": 125, "status": "Open"}' | jq
|
|
42
|
+
{
|
|
43
|
+
"status": "success",
|
|
44
|
+
"error": "",
|
|
45
|
+
"data": {
|
|
46
|
+
"id": "22f2c950-6519-4d8e-9084-013455449b07",
|
|
47
|
+
"total": 125.0,
|
|
48
|
+
"status": "Open"
|
|
49
|
+
},
|
|
50
|
+
"pagination": {},
|
|
51
|
+
"input_errors": {}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
$ curl http://localhost:8080 -d '{"total": 125, "status": "huh"}' | jq
|
|
55
|
+
{
|
|
56
|
+
"status": "input_errors",
|
|
57
|
+
"error": "",
|
|
58
|
+
"data": [],
|
|
59
|
+
"pagination": {},
|
|
60
|
+
"input_errors": {
|
|
61
|
+
"status": "Invalid value for status"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
""" The allowed values. """
|
|
68
|
+
allowed_values = configs.StringList(required=True)
|
|
69
|
+
_descriptor_config_map = None
|
|
70
|
+
|
|
71
|
+
@clearskies.decorators.parameters_to_properties
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
allowed_values: list[str],
|
|
75
|
+
default: str | None = None,
|
|
76
|
+
setable: str | Callable[..., str] | None = None,
|
|
77
|
+
is_readable: bool = True,
|
|
78
|
+
is_writeable: bool = True,
|
|
79
|
+
is_searchable: bool = True,
|
|
80
|
+
is_temporary: bool = False,
|
|
81
|
+
validators: clearskies.typing.validator | list[clearskies.typing.validator] = [],
|
|
82
|
+
on_change_pre_save: clearskies.typing.action | list[clearskies.typing.action] = [],
|
|
83
|
+
on_change_post_save: clearskies.typing.action | list[clearskies.typing.action] = [],
|
|
84
|
+
on_change_save_finished: clearskies.typing.action | list[clearskies.typing.action] = [],
|
|
85
|
+
created_by_source_type: str = "",
|
|
86
|
+
created_by_source_key: str = "",
|
|
87
|
+
created_by_source_strict: bool = True,
|
|
88
|
+
):
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
def input_error_for_value(self, value: str, operator: str | None = None) -> str:
|
|
92
|
+
return f"Invalid value for {self.name}" if value not in self.allowed_values else ""
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Self, overload
|
|
4
|
+
|
|
5
|
+
from clearskies.column import Column
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from clearskies import Model
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class String(Column):
|
|
12
|
+
"""
|
|
13
|
+
A simple string column.
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import clearskies
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Pet(clearskies.Model):
|
|
20
|
+
id_column_name = "id"
|
|
21
|
+
backend = clearskies.backends.MemoryBackend()
|
|
22
|
+
|
|
23
|
+
id = clearskies.columns.Uuid()
|
|
24
|
+
name = clearskies.columns.String()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
wsgi = clearskies.contexts.WsgiRef(
|
|
28
|
+
clearskies.endpoints.Create(
|
|
29
|
+
Pet,
|
|
30
|
+
writeable_column_names=["name"],
|
|
31
|
+
readable_column_names=["id", "name"],
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
wsgi()
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
And when invoked:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
$ curl http://localhost:8080 -d '{"name": "Spot"}' | jq
|
|
41
|
+
{
|
|
42
|
+
"status": "success",
|
|
43
|
+
"error": "",
|
|
44
|
+
"data": {
|
|
45
|
+
"id": "e5b8417f-91bc-4fe5-9b64-04f571a7b10a",
|
|
46
|
+
"name": "Spot"
|
|
47
|
+
},
|
|
48
|
+
"pagination": {},
|
|
49
|
+
"input_errors": {}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
$ curl http://localhost:8080 -d '{"name": 10}' | jq
|
|
53
|
+
{
|
|
54
|
+
"status": "input_errors",
|
|
55
|
+
"error": "",
|
|
56
|
+
"data": [],
|
|
57
|
+
"pagination": {},
|
|
58
|
+
"input_errors": {
|
|
59
|
+
"name": "value should be a string"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
_allowed_search_operators = ["<=>", "!=", "<=", ">=", ">", "<", "=", "in", "is not null", "is null", "like"]
|
|
67
|
+
_descriptor_config_map = None
|
|
68
|
+
|
|
69
|
+
@overload
|
|
70
|
+
def __get__(self, instance: None, cls: type[Model]) -> Self:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
@overload
|
|
74
|
+
def __get__(self, instance: Model, cls: type[Model]) -> str:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
def __get__(self, instance, cls):
|
|
78
|
+
if instance is None:
|
|
79
|
+
self.model_class = cls
|
|
80
|
+
return self
|
|
81
|
+
|
|
82
|
+
# this makes sure we're initialized
|
|
83
|
+
if "name" not in self._config: # type: ignore
|
|
84
|
+
instance.get_columns()
|
|
85
|
+
|
|
86
|
+
if self.name not in instance._data:
|
|
87
|
+
return None # type: ignore
|
|
88
|
+
|
|
89
|
+
if self.name not in instance._transformed_data:
|
|
90
|
+
instance._transformed_data[self.name] = self.from_backend(instance._data[self.name])
|
|
91
|
+
|
|
92
|
+
return instance._transformed_data[self.name]
|
|
93
|
+
|
|
94
|
+
def __set__(self, instance: Model, value: str) -> None:
|
|
95
|
+
# this makes sure we're initialized
|
|
96
|
+
if "name" not in self._config: # type: ignore
|
|
97
|
+
instance.get_columns()
|
|
98
|
+
|
|
99
|
+
instance._next_data[self.name] = value
|
|
100
|
+
|
|
101
|
+
def input_error_for_value(self, value: str, operator: str | None = None) -> str:
|
|
102
|
+
return "value should be a string" if type(value) != str else ""
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Callable, Self, Type, overload
|
|
5
|
+
|
|
6
|
+
import clearskies.decorators
|
|
7
|
+
import clearskies.typing
|
|
8
|
+
from clearskies import configs
|
|
9
|
+
from clearskies.columns.datetime import Datetime
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from clearskies import Model
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Timestamp(Datetime):
|
|
16
|
+
"""
|
|
17
|
+
A timestamp column.
|
|
18
|
+
|
|
19
|
+
The difference between this and the datetime column is that this stores the datetime
|
|
20
|
+
as a standard unix timestamp - the number of seconds since the unix epoch.
|
|
21
|
+
|
|
22
|
+
Also, this **always** assumes the timezone for the timestamp is UTC
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import datetime
|
|
26
|
+
import clearskies
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Pet(clearskies.Model):
|
|
30
|
+
id_column_name = "id"
|
|
31
|
+
backend = clearskies.backends.MemoryBackend()
|
|
32
|
+
|
|
33
|
+
id = clearskies.columns.Uuid()
|
|
34
|
+
name = clearskies.columns.String()
|
|
35
|
+
last_fed = clearskies.columns.Timestamp()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def demo_timestamp(utcnow: datetime.datetime, pets: Pet) -> dict[str, str | int]:
|
|
39
|
+
pet = pets.create({
|
|
40
|
+
"name": "Spot",
|
|
41
|
+
"last_fed": utcnow,
|
|
42
|
+
})
|
|
43
|
+
return {
|
|
44
|
+
"last_fed": pet.last_fed.isoformat(),
|
|
45
|
+
"raw_data": pet.get_raw_data()["last_fed"],
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
cli = clearskies.contexts.Cli(
|
|
50
|
+
clearskies.endpoints.Callable(
|
|
51
|
+
demo_timestamp,
|
|
52
|
+
),
|
|
53
|
+
classes=[Pet],
|
|
54
|
+
)
|
|
55
|
+
cli()
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
And when invoked it returns:
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"status": "success",
|
|
63
|
+
"error": "",
|
|
64
|
+
"data": {"last_fed": "2025-05-18T19:14:56+00:00", "raw_data": 1747595696},
|
|
65
|
+
"pagination": {},
|
|
66
|
+
"input_errors": {},
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Note that if you pull the column from the model in the usual way (e.g. `pet.last_fed` you get a timestamp,
|
|
71
|
+
but if you check the raw data straight out of the backend (e.g. `pet.get_raw_data()["last_fed"]`) it's an
|
|
72
|
+
integer.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
# whether or not to include the microseconds in the timestamp
|
|
76
|
+
include_microseconds = configs.Boolean(default=False)
|
|
77
|
+
_descriptor_config_map = None
|
|
78
|
+
|
|
79
|
+
@clearskies.decorators.parameters_to_properties
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
include_microseconds: bool = False,
|
|
83
|
+
default: datetime.datetime | None = None,
|
|
84
|
+
setable: datetime.datetime | Callable[..., datetime.datetime] | None = None,
|
|
85
|
+
is_readable: bool = True,
|
|
86
|
+
is_writeable: bool = True,
|
|
87
|
+
is_searchable: bool = True,
|
|
88
|
+
is_temporary: bool = False,
|
|
89
|
+
validators: clearskies.typing.validator | list[clearskies.typing.validator] = [],
|
|
90
|
+
on_change_pre_save: clearskies.typing.action | list[clearskies.typing.action] = [],
|
|
91
|
+
on_change_post_save: clearskies.typing.action | list[clearskies.typing.action] = [],
|
|
92
|
+
on_change_save_finished: clearskies.typing.action | list[clearskies.typing.action] = [],
|
|
93
|
+
created_by_source_type: str = "",
|
|
94
|
+
created_by_source_key: str = "",
|
|
95
|
+
created_by_source_strict: bool = True,
|
|
96
|
+
):
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
def from_backend(self, value) -> datetime.datetime | None:
|
|
100
|
+
mult = 1000 if self.include_microseconds else 1
|
|
101
|
+
if not value:
|
|
102
|
+
date = None
|
|
103
|
+
elif isinstance(value, str):
|
|
104
|
+
if not value.isdigit():
|
|
105
|
+
raise ValueError(
|
|
106
|
+
f"Invalid data was found in the backend for model {self.model_class.__name__} and column {self.name}: a string value was found that is not a timestamp. It was '{value}'"
|
|
107
|
+
)
|
|
108
|
+
date = datetime.datetime.fromtimestamp(int(value) / mult, datetime.timezone.utc)
|
|
109
|
+
elif isinstance(value, int) or isinstance(value, float):
|
|
110
|
+
date = datetime.datetime.fromtimestamp(value / mult, datetime.timezone.utc)
|
|
111
|
+
else:
|
|
112
|
+
if not isinstance(value, datetime.datetime):
|
|
113
|
+
raise ValueError(
|
|
114
|
+
f"Invalid data was found in the backend for model {self.model_class.__name__} and column {self.name}: the value was neither an integer, float, string, or datetime object"
|
|
115
|
+
)
|
|
116
|
+
date = value
|
|
117
|
+
return date.replace(tzinfo=datetime.timezone.utc) if date else None
|
|
118
|
+
|
|
119
|
+
def to_backend(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
120
|
+
if not self.name in data or isinstance(data[self.name], int) or data[self.name] == None:
|
|
121
|
+
return data
|
|
122
|
+
|
|
123
|
+
value = data[self.name]
|
|
124
|
+
if isinstance(value, str):
|
|
125
|
+
if not value.isdigit():
|
|
126
|
+
raise ValueError(
|
|
127
|
+
f"Invalid data was sent to the backend for model {self.model_class.__name__} and column {self.name}: a string value was found that is not a timestamp. It was '{value}'"
|
|
128
|
+
)
|
|
129
|
+
value = int(value)
|
|
130
|
+
elif isinstance(value, datetime.datetime):
|
|
131
|
+
value = value.timestamp()
|
|
132
|
+
else:
|
|
133
|
+
raise ValueError(
|
|
134
|
+
f"Invalid data was sent to the backend for model {self.model_class.__name__} and column {self.name}: the value was neither an integer, a string, nor a datetime object"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
return {**data, self.name: int(value)}
|
|
138
|
+
|
|
139
|
+
@overload
|
|
140
|
+
def __get__(self, instance: None, cls: type) -> Self:
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
@overload
|
|
144
|
+
def __get__(self, instance: Model, cls: type) -> datetime.datetime:
|
|
145
|
+
pass
|
|
146
|
+
|
|
147
|
+
def __get__(self, instance, cls):
|
|
148
|
+
return super().__get__(instance, cls)
|
|
149
|
+
|
|
150
|
+
def __set__(self, instance, value: datetime.datetime) -> None:
|
|
151
|
+
# this makes sure we're initialized
|
|
152
|
+
if "name" not in self._config: # type: ignore
|
|
153
|
+
instance.get_columns()
|
|
154
|
+
|
|
155
|
+
instance._next_data[self.name] = value
|
|
156
|
+
|
|
157
|
+
def input_error_for_value(self, value: str, operator: str | None = None) -> str:
|
|
158
|
+
if not isinstance(value, int):
|
|
159
|
+
return f"'{self.name}' must be an integer"
|
|
160
|
+
return ""
|
|
161
|
+
|
|
162
|
+
def values_match(self, value_1, value_2):
|
|
163
|
+
"""Compare two values to see if they are the same."""
|
|
164
|
+
return value_1 == value_2
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
import clearskies.decorators
|
|
7
|
+
import clearskies.di
|
|
8
|
+
import clearskies.typing
|
|
9
|
+
from clearskies import configs
|
|
10
|
+
from clearskies.columns.datetime import Datetime
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from clearskies import Model
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Updated(Datetime):
|
|
17
|
+
"""
|
|
18
|
+
The updated column records the time that a record is created or updated.
|
|
19
|
+
|
|
20
|
+
Note that this will always populate the column anytime the model is created or updated.
|
|
21
|
+
You don't have to provide the timestamp yourself and you should never expose it as
|
|
22
|
+
a writeable column through an endpoint (in fact, you can't).
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import clearskies
|
|
26
|
+
import time
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MyModel(clearskies.Model):
|
|
30
|
+
backend = clearskies.backends.MemoryBackend()
|
|
31
|
+
id_column_name = "id"
|
|
32
|
+
|
|
33
|
+
id = clearskies.columns.Uuid()
|
|
34
|
+
name = clearskies.columns.String()
|
|
35
|
+
created = clearskies.columns.Created()
|
|
36
|
+
updated = clearskies.columns.Updated()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_updated(my_models: MyModel) -> MyModel:
|
|
40
|
+
my_model = my_models.create({"name": "Jane"})
|
|
41
|
+
updated_column_after_create = my_model.updated
|
|
42
|
+
|
|
43
|
+
time.sleep(2)
|
|
44
|
+
|
|
45
|
+
my_model.save({"name": "Susan"})
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
"updated_column_after_create": updated_column_after_create.isoformat(),
|
|
49
|
+
"updated_column_at_end": my_model.updated.isoformat(),
|
|
50
|
+
"difference_in_seconds": (my_model.updated - updated_column_after_create).total_seconds(),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
cli = clearskies.contexts.Cli(clearskies.endpoints.Callable(test_updated), classes=[MyModel])
|
|
55
|
+
cli()
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
And when invoked:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
$ ./test.py | jq
|
|
62
|
+
{
|
|
63
|
+
"status": "success",
|
|
64
|
+
"error": "",
|
|
65
|
+
"data": {
|
|
66
|
+
"updated_column_after_create": "2025-05-18T19:28:46+00:00",
|
|
67
|
+
"updated_column_at_end": "2025-05-18T19:28:48+00:00",
|
|
68
|
+
"difference_in_seconds": 2.0
|
|
69
|
+
},
|
|
70
|
+
"pagination": {},
|
|
71
|
+
"input_errors": {}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Note that the `updated` column was set both when the record was first created and when it was updated,
|
|
76
|
+
so there is a two second difference between them (since we slept for two seconds).
|
|
77
|
+
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
"""
|
|
81
|
+
Created fields are never writeable because they always set the created time automatically.
|
|
82
|
+
"""
|
|
83
|
+
is_writeable = configs.Boolean(default=False)
|
|
84
|
+
_descriptor_config_map = None
|
|
85
|
+
|
|
86
|
+
now = clearskies.di.inject.Now()
|
|
87
|
+
|
|
88
|
+
@clearskies.decorators.parameters_to_properties
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
in_utc: bool = True,
|
|
92
|
+
date_format: str = "%Y-%m-%d %H:%M:%S",
|
|
93
|
+
backend_default: str = "0000-00-00 00:00:00",
|
|
94
|
+
is_readable: bool = True,
|
|
95
|
+
is_searchable: bool = True,
|
|
96
|
+
is_temporary: bool = False,
|
|
97
|
+
on_change_pre_save: clearskies.typing.action | list[clearskies.typing.action] = [],
|
|
98
|
+
on_change_post_save: clearskies.typing.action | list[clearskies.typing.action] = [],
|
|
99
|
+
on_change_save_finished: clearskies.typing.action | list[clearskies.typing.action] = [],
|
|
100
|
+
):
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
def pre_save(self, data: dict[str, Any], model: Model) -> dict[str, Any]:
|
|
104
|
+
now = self.now
|
|
105
|
+
if self.timezone_aware:
|
|
106
|
+
now = now.astimezone(self.timezone)
|
|
107
|
+
data = {**data, self.name: now}
|
|
108
|
+
if self.on_change_pre_save:
|
|
109
|
+
data = self.execute_actions_with_data(self.on_change_pre_save, model, data)
|
|
110
|
+
return data
|