clear-skies 2.0.3__py3-none-any.whl → 2.0.5__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/METADATA +74 -0
- clear_skies-2.0.5.dist-info/RECORD +4 -0
- {clear_skies-2.0.3.dist-info → clear_skies-2.0.5.dist-info}/WHEEL +1 -1
- clear_skies-2.0.3.dist-info/METADATA +0 -46
- clear_skies-2.0.3.dist-info/RECORD +0 -249
- clearskies/__init__.py +0 -59
- clearskies/action.py +0 -7
- clearskies/authentication/__init__.py +0 -15
- clearskies/authentication/authentication.py +0 -46
- clearskies/authentication/authorization.py +0 -16
- clearskies/authentication/authorization_pass_through.py +0 -20
- clearskies/authentication/jwks.py +0 -163
- clearskies/authentication/public.py +0 -5
- clearskies/authentication/secret_bearer.py +0 -553
- clearskies/autodoc/__init__.py +0 -8
- clearskies/autodoc/formats/__init__.py +0 -5
- clearskies/autodoc/formats/oai3_json/__init__.py +0 -7
- clearskies/autodoc/formats/oai3_json/oai3_json.py +0 -87
- clearskies/autodoc/formats/oai3_json/oai3_schema_resolver.py +0 -15
- clearskies/autodoc/formats/oai3_json/parameter.py +0 -35
- clearskies/autodoc/formats/oai3_json/request.py +0 -68
- clearskies/autodoc/formats/oai3_json/response.py +0 -28
- clearskies/autodoc/formats/oai3_json/schema/__init__.py +0 -11
- clearskies/autodoc/formats/oai3_json/schema/array.py +0 -9
- clearskies/autodoc/formats/oai3_json/schema/default.py +0 -13
- clearskies/autodoc/formats/oai3_json/schema/enum.py +0 -7
- clearskies/autodoc/formats/oai3_json/schema/object.py +0 -29
- clearskies/autodoc/formats/oai3_json/test.json +0 -1985
- clearskies/autodoc/py.typed +0 -0
- clearskies/autodoc/request/__init__.py +0 -15
- clearskies/autodoc/request/header.py +0 -6
- clearskies/autodoc/request/json_body.py +0 -6
- clearskies/autodoc/request/parameter.py +0 -8
- clearskies/autodoc/request/request.py +0 -38
- clearskies/autodoc/request/url_parameter.py +0 -6
- clearskies/autodoc/request/url_path.py +0 -6
- clearskies/autodoc/response/__init__.py +0 -5
- clearskies/autodoc/response/response.py +0 -9
- clearskies/autodoc/schema/__init__.py +0 -31
- clearskies/autodoc/schema/array.py +0 -10
- clearskies/autodoc/schema/base64.py +0 -8
- clearskies/autodoc/schema/boolean.py +0 -5
- clearskies/autodoc/schema/date.py +0 -5
- clearskies/autodoc/schema/datetime.py +0 -5
- clearskies/autodoc/schema/double.py +0 -5
- clearskies/autodoc/schema/enum.py +0 -17
- clearskies/autodoc/schema/integer.py +0 -6
- clearskies/autodoc/schema/long.py +0 -5
- clearskies/autodoc/schema/number.py +0 -6
- clearskies/autodoc/schema/object.py +0 -13
- clearskies/autodoc/schema/password.py +0 -5
- clearskies/autodoc/schema/schema.py +0 -11
- clearskies/autodoc/schema/string.py +0 -5
- clearskies/backends/__init__.py +0 -65
- clearskies/backends/api_backend.py +0 -1178
- clearskies/backends/backend.py +0 -136
- clearskies/backends/cursor_backend.py +0 -335
- clearskies/backends/memory_backend.py +0 -797
- clearskies/backends/secrets_backend.py +0 -106
- clearskies/column.py +0 -1233
- clearskies/columns/__init__.py +0 -71
- clearskies/columns/audit.py +0 -206
- clearskies/columns/belongs_to_id.py +0 -483
- clearskies/columns/belongs_to_model.py +0 -132
- clearskies/columns/belongs_to_self.py +0 -105
- clearskies/columns/boolean.py +0 -113
- clearskies/columns/category_tree.py +0 -275
- clearskies/columns/category_tree_ancestors.py +0 -51
- clearskies/columns/category_tree_children.py +0 -127
- clearskies/columns/category_tree_descendants.py +0 -48
- clearskies/columns/created.py +0 -95
- clearskies/columns/created_by_authorization_data.py +0 -116
- clearskies/columns/created_by_header.py +0 -99
- clearskies/columns/created_by_ip.py +0 -92
- clearskies/columns/created_by_routing_data.py +0 -97
- clearskies/columns/created_by_user_agent.py +0 -92
- clearskies/columns/date.py +0 -234
- clearskies/columns/datetime.py +0 -282
- clearskies/columns/email.py +0 -76
- clearskies/columns/float.py +0 -153
- clearskies/columns/has_many.py +0 -505
- clearskies/columns/has_many_self.py +0 -56
- clearskies/columns/has_one.py +0 -14
- clearskies/columns/integer.py +0 -160
- clearskies/columns/json.py +0 -126
- clearskies/columns/many_to_many_ids.py +0 -337
- clearskies/columns/many_to_many_ids_with_data.py +0 -274
- clearskies/columns/many_to_many_models.py +0 -158
- clearskies/columns/many_to_many_pivots.py +0 -134
- clearskies/columns/phone.py +0 -159
- clearskies/columns/select.py +0 -92
- clearskies/columns/string.py +0 -102
- clearskies/columns/timestamp.py +0 -164
- clearskies/columns/updated.py +0 -110
- clearskies/columns/uuid.py +0 -86
- clearskies/configs/README.md +0 -105
- clearskies/configs/__init__.py +0 -162
- clearskies/configs/actions.py +0 -43
- clearskies/configs/any.py +0 -13
- clearskies/configs/any_dict.py +0 -22
- clearskies/configs/any_dict_or_callable.py +0 -23
- clearskies/configs/authentication.py +0 -23
- clearskies/configs/authorization.py +0 -23
- clearskies/configs/boolean.py +0 -16
- clearskies/configs/boolean_or_callable.py +0 -18
- clearskies/configs/callable_config.py +0 -18
- clearskies/configs/columns.py +0 -34
- clearskies/configs/conditions.py +0 -30
- clearskies/configs/config.py +0 -24
- clearskies/configs/datetime.py +0 -18
- clearskies/configs/datetime_or_callable.py +0 -19
- clearskies/configs/endpoint.py +0 -23
- clearskies/configs/endpoint_list.py +0 -28
- clearskies/configs/float.py +0 -16
- clearskies/configs/float_or_callable.py +0 -18
- clearskies/configs/integer.py +0 -16
- clearskies/configs/integer_or_callable.py +0 -18
- clearskies/configs/joins.py +0 -30
- clearskies/configs/list_any_dict.py +0 -30
- clearskies/configs/list_any_dict_or_callable.py +0 -31
- clearskies/configs/model_class.py +0 -35
- clearskies/configs/model_column.py +0 -65
- clearskies/configs/model_columns.py +0 -56
- clearskies/configs/model_destination_name.py +0 -25
- clearskies/configs/model_to_id_column.py +0 -43
- clearskies/configs/readable_model_column.py +0 -9
- clearskies/configs/readable_model_columns.py +0 -9
- clearskies/configs/schema.py +0 -23
- clearskies/configs/searchable_model_columns.py +0 -9
- clearskies/configs/security_headers.py +0 -39
- clearskies/configs/select.py +0 -26
- clearskies/configs/select_list.py +0 -47
- clearskies/configs/string.py +0 -29
- clearskies/configs/string_dict.py +0 -32
- clearskies/configs/string_list.py +0 -32
- clearskies/configs/string_list_or_callable.py +0 -35
- clearskies/configs/string_or_callable.py +0 -18
- clearskies/configs/timedelta.py +0 -18
- clearskies/configs/timezone.py +0 -18
- clearskies/configs/url.py +0 -23
- clearskies/configs/validators.py +0 -45
- clearskies/configs/writeable_model_column.py +0 -9
- clearskies/configs/writeable_model_columns.py +0 -9
- clearskies/configurable.py +0 -76
- clearskies/contexts/__init__.py +0 -11
- clearskies/contexts/cli.py +0 -117
- clearskies/contexts/context.py +0 -98
- clearskies/contexts/wsgi.py +0 -76
- clearskies/contexts/wsgi_ref.py +0 -82
- clearskies/decorators.py +0 -33
- clearskies/di/__init__.py +0 -14
- clearskies/di/additional_config.py +0 -130
- clearskies/di/additional_config_auto_import.py +0 -17
- clearskies/di/di.py +0 -968
- clearskies/di/inject/__init__.py +0 -23
- clearskies/di/inject/by_class.py +0 -21
- clearskies/di/inject/by_name.py +0 -18
- clearskies/di/inject/di.py +0 -13
- clearskies/di/inject/environment.py +0 -14
- clearskies/di/inject/input_output.py +0 -20
- clearskies/di/inject/now.py +0 -13
- clearskies/di/inject/requests.py +0 -13
- clearskies/di/inject/secrets.py +0 -14
- clearskies/di/inject/utcnow.py +0 -13
- clearskies/di/inject/uuid.py +0 -15
- clearskies/di/injectable.py +0 -29
- clearskies/di/injectable_properties.py +0 -131
- clearskies/di/test_module/__init__.py +0 -6
- clearskies/di/test_module/another_module/__init__.py +0 -2
- clearskies/di/test_module/module_class.py +0 -5
- clearskies/end.py +0 -183
- clearskies/endpoint.py +0 -1310
- clearskies/endpoint_group.py +0 -310
- clearskies/endpoints/__init__.py +0 -23
- clearskies/endpoints/advanced_search.py +0 -526
- clearskies/endpoints/callable.py +0 -388
- clearskies/endpoints/create.py +0 -202
- clearskies/endpoints/delete.py +0 -139
- clearskies/endpoints/get.py +0 -275
- clearskies/endpoints/health_check.py +0 -181
- clearskies/endpoints/list.py +0 -573
- clearskies/endpoints/restful_api.py +0 -427
- clearskies/endpoints/simple_search.py +0 -286
- clearskies/endpoints/update.py +0 -190
- clearskies/environment.py +0 -104
- clearskies/exceptions/__init__.py +0 -17
- clearskies/exceptions/authentication.py +0 -2
- clearskies/exceptions/authorization.py +0 -2
- clearskies/exceptions/client_error.py +0 -2
- clearskies/exceptions/input_errors.py +0 -4
- clearskies/exceptions/moved_permanently.py +0 -3
- clearskies/exceptions/moved_temporarily.py +0 -3
- clearskies/exceptions/not_found.py +0 -2
- clearskies/functional/__init__.py +0 -7
- clearskies/functional/routing.py +0 -92
- clearskies/functional/string.py +0 -112
- clearskies/functional/validations.py +0 -76
- clearskies/input_outputs/__init__.py +0 -13
- clearskies/input_outputs/cli.py +0 -171
- clearskies/input_outputs/exceptions/__init__.py +0 -2
- clearskies/input_outputs/exceptions/cli_input_error.py +0 -2
- clearskies/input_outputs/exceptions/cli_not_found.py +0 -2
- clearskies/input_outputs/headers.py +0 -45
- clearskies/input_outputs/input_output.py +0 -138
- clearskies/input_outputs/programmatic.py +0 -69
- clearskies/input_outputs/py.typed +0 -0
- clearskies/input_outputs/wsgi.py +0 -77
- clearskies/model.py +0 -1922
- clearskies/py.typed +0 -0
- clearskies/query/__init__.py +0 -12
- clearskies/query/condition.py +0 -223
- clearskies/query/join.py +0 -136
- clearskies/query/query.py +0 -196
- clearskies/query/sort.py +0 -27
- clearskies/schema.py +0 -82
- clearskies/secrets/__init__.py +0 -6
- clearskies/secrets/additional_configs/__init__.py +0 -32
- clearskies/secrets/additional_configs/mysql_connection_dynamic_producer.py +0 -61
- clearskies/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +0 -160
- clearskies/secrets/akeyless.py +0 -182
- clearskies/secrets/exceptions/__init__.py +0 -1
- clearskies/secrets/exceptions/not_found.py +0 -2
- clearskies/secrets/secrets.py +0 -38
- clearskies/security_header.py +0 -15
- clearskies/security_headers/__init__.py +0 -11
- clearskies/security_headers/cache_control.py +0 -67
- clearskies/security_headers/cors.py +0 -50
- clearskies/security_headers/csp.py +0 -94
- clearskies/security_headers/hsts.py +0 -22
- clearskies/security_headers/x_content_type_options.py +0 -0
- clearskies/security_headers/x_frame_options.py +0 -0
- clearskies/test_base.py +0 -8
- clearskies/typing.py +0 -11
- clearskies/validator.py +0 -37
- clearskies/validators/__init__.py +0 -33
- clearskies/validators/after_column.py +0 -62
- clearskies/validators/before_column.py +0 -13
- clearskies/validators/in_the_future.py +0 -32
- clearskies/validators/in_the_future_at_least.py +0 -11
- clearskies/validators/in_the_future_at_most.py +0 -10
- clearskies/validators/in_the_past.py +0 -32
- clearskies/validators/in_the_past_at_least.py +0 -10
- clearskies/validators/in_the_past_at_most.py +0 -10
- clearskies/validators/maximum_length.py +0 -26
- clearskies/validators/maximum_value.py +0 -29
- clearskies/validators/minimum_length.py +0 -26
- clearskies/validators/minimum_value.py +0 -29
- clearskies/validators/required.py +0 -34
- clearskies/validators/timedelta.py +0 -59
- clearskies/validators/unique.py +0 -30
- {clear_skies-2.0.3.dist-info → clear_skies-2.0.5.dist-info/licenses}/LICENSE +0 -0
clearskies/functional/routing.py
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def match_route(expected_route, incoming_route, allow_partial=False) -> tuple[bool, dict[str, str]]:
|
|
5
|
-
"""
|
|
6
|
-
Check if two routes match, and returns the routing data if so.
|
|
7
|
-
|
|
8
|
-
A partial match happens when the beginning of the incoming route matches the expected route. It's okay for the
|
|
9
|
-
incoming route to be longer because the routing system is hierarchical, so a partial match at the beginning
|
|
10
|
-
can work. e.g.:
|
|
11
|
-
|
|
12
|
-
Expected route: `/users`
|
|
13
|
-
Incoming route: `/users/orders/5`
|
|
14
|
-
|
|
15
|
-
But note that it must fully match all route segments, so this is never a match:
|
|
16
|
-
|
|
17
|
-
Expected route: `/user`
|
|
18
|
-
Incoming route: `/users/orders/5`
|
|
19
|
-
"""
|
|
20
|
-
expected_route = expected_route.strip("/")
|
|
21
|
-
incoming_route = incoming_route.strip("/")
|
|
22
|
-
|
|
23
|
-
expected_parts = expected_route.split("/")
|
|
24
|
-
incoming_parts = incoming_route.split("/")
|
|
25
|
-
|
|
26
|
-
# quick check: if there are less parts in the incoming route than the expected route, then we can't possibly match
|
|
27
|
-
if len(incoming_parts) < len(expected_parts):
|
|
28
|
-
return (False, {})
|
|
29
|
-
# ditto the opposite, if we can't do a partial match
|
|
30
|
-
if len(expected_parts) < len(incoming_parts) and not allow_partial:
|
|
31
|
-
return (False, {})
|
|
32
|
-
|
|
33
|
-
# if we got this far then we will do a more complete match, so let's find any routing parameters
|
|
34
|
-
routing_data = {}
|
|
35
|
-
routing_parameters = extract_url_parameter_name_map(expected_route)
|
|
36
|
-
# we want it backwards
|
|
37
|
-
routing_parameters_by_index = {value: key for (key, value) in routing_parameters.items()}
|
|
38
|
-
for index in range(len(expected_parts)):
|
|
39
|
-
if index in routing_parameters_by_index:
|
|
40
|
-
if not incoming_parts[index]:
|
|
41
|
-
return (False, {})
|
|
42
|
-
routing_data[routing_parameters_by_index[index]] = incoming_parts[index]
|
|
43
|
-
else:
|
|
44
|
-
if expected_parts[index] != incoming_parts[index]:
|
|
45
|
-
return (False, {})
|
|
46
|
-
|
|
47
|
-
return (True, routing_data)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def extract_url_parameter_name_map(url: str) -> dict[str, int]:
|
|
51
|
-
"""
|
|
52
|
-
Create a map to help match URLs with routing parameters.
|
|
53
|
-
|
|
54
|
-
Routing parameters are either brace enclosed or start with colons:
|
|
55
|
-
|
|
56
|
-
```python
|
|
57
|
-
print(
|
|
58
|
-
routing.extract_url_parameter_name_map("my/path/{some_parameter}/:other_parameter/more/paths")
|
|
59
|
-
)
|
|
60
|
-
# prints {"some_parameter": 2, "other_parameter": 3}
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
Note that leading and trailing slashes are stripped, so "/my/path/{id}" and "my/path/{id}" give identical
|
|
64
|
-
parameter maps: `{"id": 2}`
|
|
65
|
-
"""
|
|
66
|
-
parameter_name_map = {}
|
|
67
|
-
path_parts = url.strip("/").split("/")
|
|
68
|
-
for index, part in enumerate(path_parts):
|
|
69
|
-
if not part:
|
|
70
|
-
continue
|
|
71
|
-
if part[0] == ":":
|
|
72
|
-
match = re.match("^:(\\w[\\w\\d_]{0,})$", part)
|
|
73
|
-
else:
|
|
74
|
-
if part[0] != "{":
|
|
75
|
-
continue
|
|
76
|
-
if part[-1] != "}":
|
|
77
|
-
raise ValueError(
|
|
78
|
-
f"Invalid route configuration for URL '{url}': section '{part}'"
|
|
79
|
-
+ " starts with a '{' but does not end with one"
|
|
80
|
-
)
|
|
81
|
-
match = re.match("^{(\\w[\\w\\d_]{0,})\\}$", part)
|
|
82
|
-
if not match:
|
|
83
|
-
raise ValueError(
|
|
84
|
-
f"Invalid route configuration for URL '{url}', section '{part}': resource identifiers must start with a letter and contain only letters, numbers, and underscores"
|
|
85
|
-
)
|
|
86
|
-
parameter_name = match.group(1)
|
|
87
|
-
if parameter_name in parameter_name_map:
|
|
88
|
-
raise ValueError(
|
|
89
|
-
f"Invalid route configuration for URL '{url}', a URL path named '{parameter_name}' appeared more than once."
|
|
90
|
-
)
|
|
91
|
-
parameter_name_map[parameter_name] = index
|
|
92
|
-
return parameter_name_map
|
clearskies/functional/string.py
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import datetime
|
|
2
|
-
import re
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
def camel_case_to_snake_case(string: str) -> str:
|
|
6
|
-
"""Convert a title/camel case string (MyString|myString) to snake case (my_string)."""
|
|
7
|
-
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", re.sub("(.)([A-Z][a-z]+)", r"\1_\2", string)).lower()
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def camel_case_to_title_case(string):
|
|
11
|
-
return camel_case_to_words(string).title().replace(" ", "")
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def camel_case_to_words(string):
|
|
15
|
-
string = re.sub("(.)([A-Z][a-z]+)", r"\1 \2", string)
|
|
16
|
-
string = re.sub("([a-z0-9])([A-Z])", r"\1 \2", string).lower()
|
|
17
|
-
return string
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def camel_case_to_nice(string):
|
|
21
|
-
return camel_case_to_words(string).title()
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def title_case_to_snake_case(string: str) -> str:
|
|
25
|
-
"""Convert a title case string (MyString) to snake case (my_string)."""
|
|
26
|
-
return camel_case_to_snake_case(string)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def title_case_to_camel_case(string: str) -> str:
|
|
30
|
-
if len(string) == 0:
|
|
31
|
-
return string
|
|
32
|
-
if len(string) == 1:
|
|
33
|
-
return string.lower()
|
|
34
|
-
return string[0].lower() + string[1:]
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def title_case_to_nice(string: str) -> str:
|
|
38
|
-
return camel_case_to_nice(string)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def snake_case_to_title_case(string: str) -> str:
|
|
42
|
-
"""
|
|
43
|
-
Convert a snake case string (my_string) to title case (MyString).
|
|
44
|
-
|
|
45
|
-
Note this is sometimes ambiguous. Consider:
|
|
46
|
-
|
|
47
|
-
TitleCase -> snake_case -> TitleCase
|
|
48
|
-
MyDbThing -> my_db_thing -> MyDbThing
|
|
49
|
-
MyDBThing -> my_db_thing -> MyDbThing
|
|
50
|
-
"""
|
|
51
|
-
words = string.lower().split("_")
|
|
52
|
-
return "".join([x.title() for x in words])
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def snake_case_to_camel_case(string: str) -> str:
|
|
56
|
-
"""
|
|
57
|
-
Convert a snake case string (my_string) to camel case (myString).
|
|
58
|
-
|
|
59
|
-
Note this is sometimes ambiguous. Consider:
|
|
60
|
-
|
|
61
|
-
camelCase -> snake_case -> camelCase
|
|
62
|
-
myDbThing -> my_db_thing -> myDbThing
|
|
63
|
-
myDBThing -> my_db_thing -> myDbThing
|
|
64
|
-
"""
|
|
65
|
-
words = string.lower().split("_")
|
|
66
|
-
return words[0] + "".join([x.title() for x in words[1:]])
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def snake_case_to_nice(string: str) -> str:
|
|
70
|
-
return camel_case_to_nice(snake_case_to_camel_case(string))
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
casings = ["camelCase", "snake_case", "TitleCase"]
|
|
74
|
-
casing_swap_map = {
|
|
75
|
-
"camelCase": {
|
|
76
|
-
"camelCase": str,
|
|
77
|
-
"snake_case": camel_case_to_snake_case,
|
|
78
|
-
"TitleCase": camel_case_to_title_case,
|
|
79
|
-
},
|
|
80
|
-
"snake_case": {
|
|
81
|
-
"camelCase": snake_case_to_camel_case,
|
|
82
|
-
"snake_case": str,
|
|
83
|
-
"TitleCase": snake_case_to_title_case,
|
|
84
|
-
},
|
|
85
|
-
"TitleCase": {
|
|
86
|
-
"camelCase": title_case_to_camel_case,
|
|
87
|
-
"snake_case": title_case_to_snake_case,
|
|
88
|
-
"TitleCase": str,
|
|
89
|
-
},
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def swap_casing(string: str, from_casing: str, to_casing: str) -> str:
|
|
94
|
-
if from_casing not in casings:
|
|
95
|
-
raise ValueError(f"Invalid casing '{from_casing}'. Must be one of '" + "', ".join(casings) + "'")
|
|
96
|
-
if to_casing not in casings:
|
|
97
|
-
raise ValueError(f"Invalid casing '{to_casing}'. Must be one of '" + "', ".join(casings) + "'")
|
|
98
|
-
return casing_swap_map[from_casing][to_casing](string) # type: ignore
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def make_plural(singular: str):
|
|
102
|
-
if singular[-1] == "y":
|
|
103
|
-
return singular[:-1] + "ies"
|
|
104
|
-
if singular[-1] == "s":
|
|
105
|
-
return singular + "es"
|
|
106
|
-
return f"{singular}s"
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def datetime_to_iso(value):
|
|
110
|
-
if not isinstance(value, datetime.date) and not isinstance(value, datetime.datetime):
|
|
111
|
-
return value
|
|
112
|
-
return value.isoformat()
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import inspect
|
|
2
|
-
from typing import Any
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
def is_model(to_check: Any) -> bool:
|
|
6
|
-
"""
|
|
7
|
-
Return True/False to denote if the given value is a model instance.
|
|
8
|
-
|
|
9
|
-
Uses ducktyping rather than checking the type, mostly to minimize the risk of circular imports
|
|
10
|
-
"""
|
|
11
|
-
if not hasattr(to_check, "destination_name"):
|
|
12
|
-
return False
|
|
13
|
-
return True
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def is_model_class(to_check: Any) -> bool:
|
|
17
|
-
"""Return True/False to denote if the given value is a model class."""
|
|
18
|
-
return inspect.isclass(to_check) and is_model(to_check)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def is_model_class_reference(to_check: Any) -> bool:
|
|
22
|
-
"""Return True/False to denote if the given value is a reference to a model class."""
|
|
23
|
-
if not hasattr(to_check, "get_model_class"):
|
|
24
|
-
return False
|
|
25
|
-
|
|
26
|
-
if inspect.isclass(to_check):
|
|
27
|
-
to_check = to_check()
|
|
28
|
-
return is_model_class(to_check.get_model_class())
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def is_model_class_or_reference(to_check: Any, raise_error_message=False, strict=True) -> bool:
|
|
32
|
-
"""
|
|
33
|
-
Return True/False to denote if the given value is a model class or a model reference.
|
|
34
|
-
|
|
35
|
-
If strict is false, then it won't check that the model reference returns a model class. This is often necessary during
|
|
36
|
-
validation by configs as, otherwise, it tends to trigger circular dependency errors.
|
|
37
|
-
"""
|
|
38
|
-
if not inspect.isclass(to_check):
|
|
39
|
-
# for references we will accept either instances or classes
|
|
40
|
-
if hasattr(to_check, "get_model_class"):
|
|
41
|
-
return is_model_class(to_check.get_model_class()) if strict else True
|
|
42
|
-
|
|
43
|
-
if raise_error_message:
|
|
44
|
-
raise TypeError(
|
|
45
|
-
f"I expected a model class or reference, but instead I received something of type '{to_check.__class__.__name__}'"
|
|
46
|
-
)
|
|
47
|
-
return False
|
|
48
|
-
|
|
49
|
-
if is_model_class(to_check):
|
|
50
|
-
return True
|
|
51
|
-
|
|
52
|
-
if hasattr(to_check, "get_model_class"):
|
|
53
|
-
if not strict:
|
|
54
|
-
return True
|
|
55
|
-
|
|
56
|
-
model_class = to_check().get_model_class()
|
|
57
|
-
if is_model_class(model_class):
|
|
58
|
-
return True
|
|
59
|
-
if raise_error_message:
|
|
60
|
-
raise TypeError(
|
|
61
|
-
f"I expected a model class or reference. I received a model reference of class '{to_check.__name__}', but when I fetched the model out of it, it gave me back something that wasn't a model. Instead, I received something of type '{model_class.__name__}'"
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
if raise_error_message:
|
|
65
|
-
raise TypeError(
|
|
66
|
-
"I expected a model class or reference, but instead I received a class that was neither. It had a type of '"
|
|
67
|
-
+ to_check.__class__.__name__
|
|
68
|
-
+ "'"
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
return False
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def is_model_or_class(to_check: Any) -> bool:
|
|
75
|
-
"""Return True/False to denote if the given value is a model instance or model class."""
|
|
76
|
-
return is_model(to_check) or is_model_class(to_check)
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
from clearskies.input_outputs.cli import Cli
|
|
2
|
-
from clearskies.input_outputs.headers import Headers
|
|
3
|
-
from clearskies.input_outputs.input_output import InputOutput
|
|
4
|
-
from clearskies.input_outputs.programmatic import Programmatic
|
|
5
|
-
from clearskies.input_outputs.wsgi import Wsgi
|
|
6
|
-
|
|
7
|
-
__all__ = [
|
|
8
|
-
"Cli",
|
|
9
|
-
"Headers",
|
|
10
|
-
"InputOutput",
|
|
11
|
-
"Programmatic",
|
|
12
|
-
"Wsgi",
|
|
13
|
-
]
|
clearskies/input_outputs/cli.py
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import sys
|
|
3
|
-
from os import isatty
|
|
4
|
-
from sys import stdin
|
|
5
|
-
|
|
6
|
-
from clearskies.input_outputs.input_output import InputOutput
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class Cli(InputOutput):
|
|
10
|
-
_args: list[str] = []
|
|
11
|
-
_has_body: bool = False
|
|
12
|
-
_body: str = ""
|
|
13
|
-
_request_method: str = ""
|
|
14
|
-
_request_headers: dict[str, str] = {}
|
|
15
|
-
|
|
16
|
-
def __init__(self):
|
|
17
|
-
self._request_headers = {}
|
|
18
|
-
self._args = []
|
|
19
|
-
self._parse_args(sys.argv)
|
|
20
|
-
super().__init__()
|
|
21
|
-
|
|
22
|
-
def respond(self, response, status_code=200):
|
|
23
|
-
if type(response) != str:
|
|
24
|
-
final = json.dumps(response)
|
|
25
|
-
else:
|
|
26
|
-
final = response
|
|
27
|
-
if status_code != 200:
|
|
28
|
-
sys.exit(final)
|
|
29
|
-
print(final)
|
|
30
|
-
|
|
31
|
-
def get_arguments(self):
|
|
32
|
-
return sys.argv
|
|
33
|
-
|
|
34
|
-
def _parse_args(self, argv):
|
|
35
|
-
tty_data = None
|
|
36
|
-
if not isatty(stdin.fileno()):
|
|
37
|
-
tty_data = sys.stdin.read().strip()
|
|
38
|
-
|
|
39
|
-
request_headers = {}
|
|
40
|
-
self._args = []
|
|
41
|
-
kwargs = {}
|
|
42
|
-
index = 0
|
|
43
|
-
# In general we will use positional arguments for routing, and kwargs for request data.
|
|
44
|
-
# If things start with a dash then they are assumed to be a kwarg. If not, then a positional argument.
|
|
45
|
-
# we don't allow for simple flags: everything is a positional argument or a key/value pair
|
|
46
|
-
# For kwargs, we'll allow for using an equal sign or not, e.g.: '--key=value' or '--key value' or '-d thing'.
|
|
47
|
-
while index < len(argv) - 1:
|
|
48
|
-
index += 1
|
|
49
|
-
|
|
50
|
-
# if we don't start with a dash then we are a positional argument
|
|
51
|
-
arg = argv[index]
|
|
52
|
-
if arg[0] != "-":
|
|
53
|
-
self._args.append(arg)
|
|
54
|
-
continue
|
|
55
|
-
|
|
56
|
-
# otherwise a kwarg
|
|
57
|
-
arg = arg.strip("-")
|
|
58
|
-
|
|
59
|
-
# if we have an equal sign in our kwarg then it's self-contained
|
|
60
|
-
if "=" in arg:
|
|
61
|
-
[key, value] = arg.split("=", 1)
|
|
62
|
-
|
|
63
|
-
# otherwise we have to grab the next argument to get the value
|
|
64
|
-
else:
|
|
65
|
-
key = arg
|
|
66
|
-
value = argv[index + 1]
|
|
67
|
-
if "-" in value:
|
|
68
|
-
raise ValueError(
|
|
69
|
-
f"Invalid clearskies cli calling sequence: found two key names next to eachother without any values: '-{arg} {value}'"
|
|
70
|
-
)
|
|
71
|
-
index += 1
|
|
72
|
-
|
|
73
|
-
if key.lower() == "h":
|
|
74
|
-
parts = value.split(":", 1)
|
|
75
|
-
if len(parts) != 2:
|
|
76
|
-
raise ValueError(
|
|
77
|
-
f"Invalid clearskies cli calling sequence: a parameter named '-H' was found, which is treated as a request header, but it didn't have the proper 'key: value' format."
|
|
78
|
-
)
|
|
79
|
-
request_headers[parts[0]] = parts[1]
|
|
80
|
-
continue
|
|
81
|
-
|
|
82
|
-
kwargs[key] = value
|
|
83
|
-
|
|
84
|
-
self._request_headers = request_headers
|
|
85
|
-
self._request_method = "GET"
|
|
86
|
-
request_method_source = ""
|
|
87
|
-
for key in ["x", "X", "request_method"]:
|
|
88
|
-
if key not in kwargs:
|
|
89
|
-
continue
|
|
90
|
-
|
|
91
|
-
if request_method_source:
|
|
92
|
-
raise ValueError(
|
|
93
|
-
f"Invalid clearskies cli calling sequence: the request method was specified via both the -{key} parameter and the -{request_method_source} parameter. To avoid ambiguity, it should only be set once."
|
|
94
|
-
)
|
|
95
|
-
self._request_method = kwargs[key]
|
|
96
|
-
del kwargs[key]
|
|
97
|
-
request_method_source = key
|
|
98
|
-
|
|
99
|
-
final_data = None
|
|
100
|
-
data_source = None
|
|
101
|
-
if tty_data:
|
|
102
|
-
final_data = tty_data
|
|
103
|
-
data_source = "piped input"
|
|
104
|
-
if kwargs.get("d"):
|
|
105
|
-
if final_data:
|
|
106
|
-
raise ValueError(
|
|
107
|
-
f"Invalid clearskies cli calling sequence: request data was sent by both the -d parameter and {data_source}. To avoid ambiguity, it should only be sent one way."
|
|
108
|
-
)
|
|
109
|
-
final_data = kwargs.get("d")
|
|
110
|
-
data_source = "the -d parameter"
|
|
111
|
-
del kwargs["d"]
|
|
112
|
-
if kwargs.get("data"):
|
|
113
|
-
if final_data:
|
|
114
|
-
raise ValueError(
|
|
115
|
-
f"Invalid calling sequence: request data was sent by both the -data parameter and {data_source}. To avoid ambiguity, it should only be sent one way."
|
|
116
|
-
)
|
|
117
|
-
final_data = kwargs.get("data")
|
|
118
|
-
data_source = "the -data parameter"
|
|
119
|
-
del kwargs["data"]
|
|
120
|
-
if final_data and len(kwargs):
|
|
121
|
-
raise ValueError(
|
|
122
|
-
f"Invalid calling sequence: extra parameters were specified after sending a body via {data_source}. To avoid ambiguity, send all data via {data_source}."
|
|
123
|
-
)
|
|
124
|
-
if not final_data and len(kwargs):
|
|
125
|
-
final_data = kwargs
|
|
126
|
-
data_source = "kwargs"
|
|
127
|
-
|
|
128
|
-
# Most of the above inputs result in a string for our final data, in which case we'll leave it as the "raw body"
|
|
129
|
-
# so that it can optionally be interpreted as JSON. If we received a bunch of kwargs though, we'll allow those to
|
|
130
|
-
# only be "read" as JSON.
|
|
131
|
-
if data_source == "kwargs":
|
|
132
|
-
self._body_as_json = final_data # type: ignore
|
|
133
|
-
self._body_loaded_as_json = True
|
|
134
|
-
self._has_body = True
|
|
135
|
-
self._body = json.dumps(final_data)
|
|
136
|
-
elif final_data:
|
|
137
|
-
self._has_body = True
|
|
138
|
-
self._body = final_data
|
|
139
|
-
|
|
140
|
-
def get_script_name(self):
|
|
141
|
-
return sys.argv[0]
|
|
142
|
-
|
|
143
|
-
def get_path_info(self):
|
|
144
|
-
return "/".join(self._args)
|
|
145
|
-
|
|
146
|
-
def get_full_path(self):
|
|
147
|
-
return self.get_path_info()
|
|
148
|
-
|
|
149
|
-
def get_request_method(self):
|
|
150
|
-
return self._request_method
|
|
151
|
-
|
|
152
|
-
def has_body(self):
|
|
153
|
-
return self._has_body
|
|
154
|
-
|
|
155
|
-
def get_body(self):
|
|
156
|
-
if not self.has_body():
|
|
157
|
-
return ""
|
|
158
|
-
|
|
159
|
-
return self._body
|
|
160
|
-
|
|
161
|
-
def context_specifics(self):
|
|
162
|
-
return {}
|
|
163
|
-
|
|
164
|
-
def get_client_ip(self):
|
|
165
|
-
return "127.0.0.1"
|
|
166
|
-
|
|
167
|
-
def get_query_string(self):
|
|
168
|
-
return ""
|
|
169
|
-
|
|
170
|
-
def get_request_headers(self):
|
|
171
|
-
return self._request_headers
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class Headers:
|
|
5
|
-
_headers: dict[str, str] = {}
|
|
6
|
-
|
|
7
|
-
def __init__(self, headers: dict[str, str] = {}):
|
|
8
|
-
self.__dict__["_headers"] = (
|
|
9
|
-
{key.upper().replace("_", "-"): value for (key, value) in headers.items()} if headers else {}
|
|
10
|
-
)
|
|
11
|
-
|
|
12
|
-
def __contains__(self, key: str):
|
|
13
|
-
return key.upper().replace("_", "-") in self._headers
|
|
14
|
-
|
|
15
|
-
def __getattr__(self, key: str) -> str | None:
|
|
16
|
-
return self._headers.get(key.upper().replace("_", "-"), None)
|
|
17
|
-
|
|
18
|
-
def __setattr__(self, key: str, value: str) -> None:
|
|
19
|
-
if not isinstance(key, str):
|
|
20
|
-
raise TypeError(
|
|
21
|
-
f"Header keys must be strings, but an object of type '{value.__class__.__name__}' was provided."
|
|
22
|
-
)
|
|
23
|
-
if not isinstance(value, str):
|
|
24
|
-
raise TypeError(
|
|
25
|
-
f"Header values must be strings, but an object of type '{value.__class__.__name__}' was provided."
|
|
26
|
-
)
|
|
27
|
-
self._headers[re.sub("\\s+", " ", key.upper().replace("_", "-"))] = re.sub("\\s+", " ", value.strip())
|
|
28
|
-
|
|
29
|
-
def get(self, key, default=None):
|
|
30
|
-
if key not in self:
|
|
31
|
-
return default
|
|
32
|
-
return self.__getattr__(key)
|
|
33
|
-
|
|
34
|
-
def keys(self) -> list[str]:
|
|
35
|
-
return list(self._headers.keys())
|
|
36
|
-
|
|
37
|
-
def values(self) -> list[str]:
|
|
38
|
-
return list(self._headers.keys())
|
|
39
|
-
|
|
40
|
-
def items(self) -> list[tuple[str]]:
|
|
41
|
-
return list(self._headers.items()) # type: ignore
|
|
42
|
-
|
|
43
|
-
def add(self, key: str, value: str) -> None:
|
|
44
|
-
"""Add a header. This expects a string with a colon separating the key and value."""
|
|
45
|
-
setattr(self, key, value)
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
from abc import ABC, abstractmethod
|
|
3
|
-
from typing import Any
|
|
4
|
-
from urllib.parse import parse_qs
|
|
5
|
-
|
|
6
|
-
import clearskies.configurable
|
|
7
|
-
import clearskies.typing
|
|
8
|
-
from clearskies.configs import AnyDict, StringDict
|
|
9
|
-
from clearskies.exceptions import ClientError
|
|
10
|
-
|
|
11
|
-
from .headers import Headers
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class InputOutput(ABC, clearskies.configurable.Configurable):
|
|
15
|
-
"""Manage the request and response to the client."""
|
|
16
|
-
|
|
17
|
-
response_headers: Headers = None # type: ignore
|
|
18
|
-
request_headers: Headers = None # type: ignore
|
|
19
|
-
query_parameters = clearskies.configs.AnyDict(default={})
|
|
20
|
-
routing_data = clearskies.configs.StringDict(default={})
|
|
21
|
-
authorization_data = clearskies.configs.AnyDict(default={})
|
|
22
|
-
|
|
23
|
-
_body_as_json: dict[str, Any] | list[Any] | None = {}
|
|
24
|
-
_body_loaded_as_json = False
|
|
25
|
-
|
|
26
|
-
def __init__(self):
|
|
27
|
-
self.response_headers = Headers()
|
|
28
|
-
self.request_headers = Headers(self.get_request_headers())
|
|
29
|
-
self.query_parameters = {key: val[0] for (key, val) in parse_qs(self.get_query_string()).items()}
|
|
30
|
-
self.authorization_data = {}
|
|
31
|
-
self.routing_data = {}
|
|
32
|
-
self.finalize_and_validate_configuration()
|
|
33
|
-
|
|
34
|
-
@abstractmethod
|
|
35
|
-
def respond(self, body: clearskies.typing.response, status_code: int = 200) -> Any:
|
|
36
|
-
"""
|
|
37
|
-
Pass along a response to the client.
|
|
38
|
-
|
|
39
|
-
Accepts a string, bytes, dictionary, or list. If a content type has not been set then it will automatically
|
|
40
|
-
be set to application/json
|
|
41
|
-
"""
|
|
42
|
-
pass
|
|
43
|
-
|
|
44
|
-
@abstractmethod
|
|
45
|
-
def get_body(self) -> str:
|
|
46
|
-
"""Return the raw body set by the client."""
|
|
47
|
-
pass
|
|
48
|
-
|
|
49
|
-
@abstractmethod
|
|
50
|
-
def has_body(self) -> bool:
|
|
51
|
-
"""Whether or not the request included a body."""
|
|
52
|
-
pass
|
|
53
|
-
|
|
54
|
-
@property
|
|
55
|
-
def request_data(self) -> dict[str, Any] | list[Any] | None:
|
|
56
|
-
"""Return the data from the request body, assuming it is JSON."""
|
|
57
|
-
if not self._body_loaded_as_json:
|
|
58
|
-
self._body_loaded_as_json = True
|
|
59
|
-
if not self.has_body():
|
|
60
|
-
self._body_as_json = None
|
|
61
|
-
else:
|
|
62
|
-
try:
|
|
63
|
-
self._body_as_json = json.loads(self.get_body())
|
|
64
|
-
except json.JSONDecodeError:
|
|
65
|
-
self._body_as_json = None
|
|
66
|
-
return self._body_as_json
|
|
67
|
-
|
|
68
|
-
@abstractmethod
|
|
69
|
-
def get_request_method(self) -> str:
|
|
70
|
-
"""Return the request method set by the client."""
|
|
71
|
-
pass
|
|
72
|
-
|
|
73
|
-
@abstractmethod
|
|
74
|
-
def get_script_name(self) -> str:
|
|
75
|
-
"""Return the script name, e.g. the path requested."""
|
|
76
|
-
pass
|
|
77
|
-
|
|
78
|
-
@abstractmethod
|
|
79
|
-
def get_path_info(self) -> str:
|
|
80
|
-
"""Return the path info for the request."""
|
|
81
|
-
pass
|
|
82
|
-
|
|
83
|
-
@abstractmethod
|
|
84
|
-
def get_query_string(self) -> str:
|
|
85
|
-
"""Return the full query string for the request (everything after the first question mark in the document URL)."""
|
|
86
|
-
pass
|
|
87
|
-
|
|
88
|
-
@abstractmethod
|
|
89
|
-
def get_client_ip(self):
|
|
90
|
-
pass
|
|
91
|
-
|
|
92
|
-
@abstractmethod
|
|
93
|
-
def get_request_headers(self) -> dict[str, str]:
|
|
94
|
-
pass
|
|
95
|
-
|
|
96
|
-
def get_full_path(self) -> str:
|
|
97
|
-
"""Return the full path requested by the client."""
|
|
98
|
-
path_info = self.get_path_info()
|
|
99
|
-
script_name = self.get_script_name()
|
|
100
|
-
if not path_info or path_info[0] != "/":
|
|
101
|
-
path_info = f"/{path_info}"
|
|
102
|
-
return f"{path_info}{script_name}".replace("//", "/")
|
|
103
|
-
|
|
104
|
-
def context_specifics(self):
|
|
105
|
-
return {}
|
|
106
|
-
|
|
107
|
-
def get_context_for_callables(self) -> dict[str, Any]:
|
|
108
|
-
"""
|
|
109
|
-
Return a dictionary with various important parts of the request that are passed along to user-defined functions.
|
|
110
|
-
|
|
111
|
-
It's common to make various aspects of an incoming request available to user-defined functions that are
|
|
112
|
-
attached to clearskies hooks everywhere. This function centralizes the definition of what aspects of
|
|
113
|
-
the reequest shouuld be passed along to callables in this case. When this is in use it typically
|
|
114
|
-
looks like this:
|
|
115
|
-
|
|
116
|
-
di.call_function(some_function, **input_output.get_context_for_callables())
|
|
117
|
-
|
|
118
|
-
And this function returns a dictionary with the following values:
|
|
119
|
-
|
|
120
|
-
| Key | Type | Ref | Value |
|
|
121
|
-
|--------------------|----------------------------------|---------------------------------|---------------------------------------------------------------------------------|
|
|
122
|
-
| routing_data | dict[str, str] | input_output.routing_data | A dictionary of data extracted from URL path parameters. |
|
|
123
|
-
| authorization_data | dict[str, Any] | input_output.authorization_data | A dictionary containing the authorization data set by the authentication method |
|
|
124
|
-
| request_data | dict[str, Any] | None | input_output.request_data | The data sent along with the request (assuming a JSON request body) |
|
|
125
|
-
| query_parameters | dict[str, Any] | input_output.query_parameters | The query parameters |
|
|
126
|
-
| request_headers | clearskies.input_outputs.Headers | input_output.request_headers | The request headers sent by the client |
|
|
127
|
-
| **routing_data | string | **input_output.routing_data | The routing data is unpacked so keys can be fetched directly |
|
|
128
|
-
"""
|
|
129
|
-
return {
|
|
130
|
-
**self.routing_data,
|
|
131
|
-
**{
|
|
132
|
-
"routing_data": self.routing_data,
|
|
133
|
-
"authorization_data": self.authorization_data,
|
|
134
|
-
"request_data": self.request_data,
|
|
135
|
-
"request_headers": self.request_headers,
|
|
136
|
-
"query_parameters": self.query_parameters,
|
|
137
|
-
},
|
|
138
|
-
}
|