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.

Files changed (252) hide show
  1. {clear_skies-2.0.5.dist-info → clear_skies-2.0.7.dist-info}/METADATA +1 -1
  2. clear_skies-2.0.7.dist-info/RECORD +251 -0
  3. clearskies/__init__.py +61 -0
  4. clearskies/action.py +7 -0
  5. clearskies/authentication/__init__.py +15 -0
  6. clearskies/authentication/authentication.py +46 -0
  7. clearskies/authentication/authorization.py +16 -0
  8. clearskies/authentication/authorization_pass_through.py +20 -0
  9. clearskies/authentication/jwks.py +163 -0
  10. clearskies/authentication/public.py +5 -0
  11. clearskies/authentication/secret_bearer.py +553 -0
  12. clearskies/autodoc/__init__.py +8 -0
  13. clearskies/autodoc/formats/__init__.py +5 -0
  14. clearskies/autodoc/formats/oai3_json/__init__.py +7 -0
  15. clearskies/autodoc/formats/oai3_json/oai3_json.py +87 -0
  16. clearskies/autodoc/formats/oai3_json/oai3_schema_resolver.py +15 -0
  17. clearskies/autodoc/formats/oai3_json/parameter.py +35 -0
  18. clearskies/autodoc/formats/oai3_json/request.py +68 -0
  19. clearskies/autodoc/formats/oai3_json/response.py +28 -0
  20. clearskies/autodoc/formats/oai3_json/schema/__init__.py +11 -0
  21. clearskies/autodoc/formats/oai3_json/schema/array.py +9 -0
  22. clearskies/autodoc/formats/oai3_json/schema/default.py +13 -0
  23. clearskies/autodoc/formats/oai3_json/schema/enum.py +7 -0
  24. clearskies/autodoc/formats/oai3_json/schema/object.py +35 -0
  25. clearskies/autodoc/formats/oai3_json/test.json +1985 -0
  26. clearskies/autodoc/py.typed +0 -0
  27. clearskies/autodoc/request/__init__.py +15 -0
  28. clearskies/autodoc/request/header.py +6 -0
  29. clearskies/autodoc/request/json_body.py +6 -0
  30. clearskies/autodoc/request/parameter.py +8 -0
  31. clearskies/autodoc/request/request.py +47 -0
  32. clearskies/autodoc/request/url_parameter.py +6 -0
  33. clearskies/autodoc/request/url_path.py +6 -0
  34. clearskies/autodoc/response/__init__.py +5 -0
  35. clearskies/autodoc/response/response.py +9 -0
  36. clearskies/autodoc/schema/__init__.py +31 -0
  37. clearskies/autodoc/schema/array.py +10 -0
  38. clearskies/autodoc/schema/base64.py +8 -0
  39. clearskies/autodoc/schema/boolean.py +5 -0
  40. clearskies/autodoc/schema/date.py +5 -0
  41. clearskies/autodoc/schema/datetime.py +5 -0
  42. clearskies/autodoc/schema/double.py +5 -0
  43. clearskies/autodoc/schema/enum.py +17 -0
  44. clearskies/autodoc/schema/integer.py +6 -0
  45. clearskies/autodoc/schema/long.py +5 -0
  46. clearskies/autodoc/schema/number.py +6 -0
  47. clearskies/autodoc/schema/object.py +13 -0
  48. clearskies/autodoc/schema/password.py +5 -0
  49. clearskies/autodoc/schema/schema.py +11 -0
  50. clearskies/autodoc/schema/string.py +5 -0
  51. clearskies/backends/__init__.py +65 -0
  52. clearskies/backends/api_backend.py +1178 -0
  53. clearskies/backends/backend.py +136 -0
  54. clearskies/backends/cursor_backend.py +335 -0
  55. clearskies/backends/memory_backend.py +797 -0
  56. clearskies/backends/secrets_backend.py +106 -0
  57. clearskies/column.py +1233 -0
  58. clearskies/columns/__init__.py +71 -0
  59. clearskies/columns/audit.py +206 -0
  60. clearskies/columns/belongs_to_id.py +483 -0
  61. clearskies/columns/belongs_to_model.py +132 -0
  62. clearskies/columns/belongs_to_self.py +105 -0
  63. clearskies/columns/boolean.py +113 -0
  64. clearskies/columns/category_tree.py +275 -0
  65. clearskies/columns/category_tree_ancestors.py +51 -0
  66. clearskies/columns/category_tree_children.py +127 -0
  67. clearskies/columns/category_tree_descendants.py +48 -0
  68. clearskies/columns/created.py +95 -0
  69. clearskies/columns/created_by_authorization_data.py +116 -0
  70. clearskies/columns/created_by_header.py +99 -0
  71. clearskies/columns/created_by_ip.py +92 -0
  72. clearskies/columns/created_by_routing_data.py +97 -0
  73. clearskies/columns/created_by_user_agent.py +92 -0
  74. clearskies/columns/date.py +234 -0
  75. clearskies/columns/datetime.py +282 -0
  76. clearskies/columns/email.py +76 -0
  77. clearskies/columns/float.py +153 -0
  78. clearskies/columns/has_many.py +505 -0
  79. clearskies/columns/has_many_self.py +56 -0
  80. clearskies/columns/has_one.py +14 -0
  81. clearskies/columns/integer.py +160 -0
  82. clearskies/columns/json.py +128 -0
  83. clearskies/columns/many_to_many_ids.py +337 -0
  84. clearskies/columns/many_to_many_ids_with_data.py +274 -0
  85. clearskies/columns/many_to_many_models.py +158 -0
  86. clearskies/columns/many_to_many_pivots.py +134 -0
  87. clearskies/columns/phone.py +159 -0
  88. clearskies/columns/select.py +92 -0
  89. clearskies/columns/string.py +102 -0
  90. clearskies/columns/timestamp.py +164 -0
  91. clearskies/columns/updated.py +110 -0
  92. clearskies/columns/uuid.py +86 -0
  93. clearskies/configs/README.md +105 -0
  94. clearskies/configs/__init__.py +162 -0
  95. clearskies/configs/actions.py +43 -0
  96. clearskies/configs/any.py +13 -0
  97. clearskies/configs/any_dict.py +22 -0
  98. clearskies/configs/any_dict_or_callable.py +23 -0
  99. clearskies/configs/authentication.py +23 -0
  100. clearskies/configs/authorization.py +23 -0
  101. clearskies/configs/boolean.py +16 -0
  102. clearskies/configs/boolean_or_callable.py +18 -0
  103. clearskies/configs/callable_config.py +18 -0
  104. clearskies/configs/columns.py +34 -0
  105. clearskies/configs/conditions.py +30 -0
  106. clearskies/configs/config.py +24 -0
  107. clearskies/configs/datetime.py +18 -0
  108. clearskies/configs/datetime_or_callable.py +19 -0
  109. clearskies/configs/endpoint.py +23 -0
  110. clearskies/configs/endpoint_list.py +29 -0
  111. clearskies/configs/float.py +16 -0
  112. clearskies/configs/float_or_callable.py +18 -0
  113. clearskies/configs/integer.py +16 -0
  114. clearskies/configs/integer_or_callable.py +18 -0
  115. clearskies/configs/joins.py +30 -0
  116. clearskies/configs/list_any_dict.py +30 -0
  117. clearskies/configs/list_any_dict_or_callable.py +31 -0
  118. clearskies/configs/model_class.py +35 -0
  119. clearskies/configs/model_column.py +65 -0
  120. clearskies/configs/model_columns.py +56 -0
  121. clearskies/configs/model_destination_name.py +25 -0
  122. clearskies/configs/model_to_id_column.py +43 -0
  123. clearskies/configs/readable_model_column.py +9 -0
  124. clearskies/configs/readable_model_columns.py +9 -0
  125. clearskies/configs/schema.py +23 -0
  126. clearskies/configs/searchable_model_columns.py +9 -0
  127. clearskies/configs/security_headers.py +39 -0
  128. clearskies/configs/select.py +26 -0
  129. clearskies/configs/select_list.py +47 -0
  130. clearskies/configs/string.py +29 -0
  131. clearskies/configs/string_dict.py +32 -0
  132. clearskies/configs/string_list.py +32 -0
  133. clearskies/configs/string_list_or_callable.py +35 -0
  134. clearskies/configs/string_or_callable.py +18 -0
  135. clearskies/configs/timedelta.py +18 -0
  136. clearskies/configs/timezone.py +18 -0
  137. clearskies/configs/url.py +23 -0
  138. clearskies/configs/validators.py +45 -0
  139. clearskies/configs/writeable_model_column.py +9 -0
  140. clearskies/configs/writeable_model_columns.py +9 -0
  141. clearskies/configurable.py +76 -0
  142. clearskies/contexts/__init__.py +11 -0
  143. clearskies/contexts/cli.py +117 -0
  144. clearskies/contexts/context.py +98 -0
  145. clearskies/contexts/wsgi.py +76 -0
  146. clearskies/contexts/wsgi_ref.py +82 -0
  147. clearskies/decorators.py +33 -0
  148. clearskies/di/__init__.py +15 -0
  149. clearskies/di/additional_config.py +130 -0
  150. clearskies/di/additional_config_auto_import.py +17 -0
  151. clearskies/di/di.py +973 -0
  152. clearskies/di/inject/__init__.py +23 -0
  153. clearskies/di/inject/by_class.py +21 -0
  154. clearskies/di/inject/by_name.py +18 -0
  155. clearskies/di/inject/di.py +13 -0
  156. clearskies/di/inject/environment.py +14 -0
  157. clearskies/di/inject/input_output.py +20 -0
  158. clearskies/di/inject/now.py +13 -0
  159. clearskies/di/inject/requests.py +13 -0
  160. clearskies/di/inject/secrets.py +14 -0
  161. clearskies/di/inject/utcnow.py +13 -0
  162. clearskies/di/inject/uuid.py +15 -0
  163. clearskies/di/injectable.py +29 -0
  164. clearskies/di/injectable_properties.py +131 -0
  165. clearskies/di/test_module/__init__.py +6 -0
  166. clearskies/di/test_module/another_module/__init__.py +2 -0
  167. clearskies/di/test_module/module_class.py +5 -0
  168. clearskies/end.py +183 -0
  169. clearskies/endpoint.py +1314 -0
  170. clearskies/endpoint_group.py +336 -0
  171. clearskies/endpoints/__init__.py +25 -0
  172. clearskies/endpoints/advanced_search.py +526 -0
  173. clearskies/endpoints/callable.py +388 -0
  174. clearskies/endpoints/create.py +205 -0
  175. clearskies/endpoints/delete.py +139 -0
  176. clearskies/endpoints/get.py +271 -0
  177. clearskies/endpoints/health_check.py +183 -0
  178. clearskies/endpoints/list.py +574 -0
  179. clearskies/endpoints/restful_api.py +427 -0
  180. clearskies/endpoints/schema.py +189 -0
  181. clearskies/endpoints/simple_search.py +286 -0
  182. clearskies/endpoints/update.py +193 -0
  183. clearskies/environment.py +104 -0
  184. clearskies/exceptions/__init__.py +19 -0
  185. clearskies/exceptions/authentication.py +2 -0
  186. clearskies/exceptions/authorization.py +2 -0
  187. clearskies/exceptions/client_error.py +2 -0
  188. clearskies/exceptions/input_errors.py +4 -0
  189. clearskies/exceptions/missing_dependency.py +2 -0
  190. clearskies/exceptions/moved_permanently.py +3 -0
  191. clearskies/exceptions/moved_temporarily.py +3 -0
  192. clearskies/exceptions/not_found.py +2 -0
  193. clearskies/functional/__init__.py +7 -0
  194. clearskies/functional/routing.py +92 -0
  195. clearskies/functional/string.py +112 -0
  196. clearskies/functional/validations.py +76 -0
  197. clearskies/input_outputs/__init__.py +13 -0
  198. clearskies/input_outputs/cli.py +171 -0
  199. clearskies/input_outputs/exceptions/__init__.py +2 -0
  200. clearskies/input_outputs/exceptions/cli_input_error.py +2 -0
  201. clearskies/input_outputs/exceptions/cli_not_found.py +2 -0
  202. clearskies/input_outputs/headers.py +45 -0
  203. clearskies/input_outputs/input_output.py +138 -0
  204. clearskies/input_outputs/programmatic.py +69 -0
  205. clearskies/input_outputs/py.typed +0 -0
  206. clearskies/input_outputs/wsgi.py +77 -0
  207. clearskies/model.py +1922 -0
  208. clearskies/py.typed +0 -0
  209. clearskies/query/__init__.py +12 -0
  210. clearskies/query/condition.py +223 -0
  211. clearskies/query/join.py +136 -0
  212. clearskies/query/query.py +196 -0
  213. clearskies/query/sort.py +27 -0
  214. clearskies/schema.py +82 -0
  215. clearskies/secrets/__init__.py +6 -0
  216. clearskies/secrets/additional_configs/__init__.py +32 -0
  217. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer.py +61 -0
  218. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +160 -0
  219. clearskies/secrets/akeyless.py +182 -0
  220. clearskies/secrets/exceptions/__init__.py +1 -0
  221. clearskies/secrets/exceptions/not_found.py +2 -0
  222. clearskies/secrets/secrets.py +38 -0
  223. clearskies/security_header.py +15 -0
  224. clearskies/security_headers/__init__.py +11 -0
  225. clearskies/security_headers/cache_control.py +67 -0
  226. clearskies/security_headers/cors.py +50 -0
  227. clearskies/security_headers/csp.py +94 -0
  228. clearskies/security_headers/hsts.py +22 -0
  229. clearskies/security_headers/x_content_type_options.py +0 -0
  230. clearskies/security_headers/x_frame_options.py +0 -0
  231. clearskies/test_base.py +8 -0
  232. clearskies/typing.py +11 -0
  233. clearskies/validator.py +37 -0
  234. clearskies/validators/__init__.py +33 -0
  235. clearskies/validators/after_column.py +62 -0
  236. clearskies/validators/before_column.py +13 -0
  237. clearskies/validators/in_the_future.py +32 -0
  238. clearskies/validators/in_the_future_at_least.py +11 -0
  239. clearskies/validators/in_the_future_at_most.py +10 -0
  240. clearskies/validators/in_the_past.py +32 -0
  241. clearskies/validators/in_the_past_at_least.py +10 -0
  242. clearskies/validators/in_the_past_at_most.py +10 -0
  243. clearskies/validators/maximum_length.py +26 -0
  244. clearskies/validators/maximum_value.py +29 -0
  245. clearskies/validators/minimum_length.py +26 -0
  246. clearskies/validators/minimum_value.py +29 -0
  247. clearskies/validators/required.py +34 -0
  248. clearskies/validators/timedelta.py +59 -0
  249. clearskies/validators/unique.py +30 -0
  250. clear_skies-2.0.5.dist-info/RECORD +0 -4
  251. {clear_skies-2.0.5.dist-info → clear_skies-2.0.7.dist-info}/WHEEL +0 -0
  252. {clear_skies-2.0.5.dist-info → clear_skies-2.0.7.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,139 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from collections import OrderedDict
5
+ from typing import TYPE_CHECKING, Any, Callable, Type
6
+
7
+ import clearskies.configs
8
+ import clearskies.exceptions
9
+ from clearskies import authentication, autodoc, typing
10
+ from clearskies.endpoints.get import Get
11
+ from clearskies.functional import routing, string
12
+ from clearskies.input_outputs import InputOutput
13
+
14
+ if TYPE_CHECKING:
15
+ from clearskies import SecurityHeader
16
+ from clearskies.model import Model, Schema
17
+
18
+
19
+ class Delete(Get):
20
+ """
21
+ An endpoint that deletes a single record by id (or other unique column).
22
+
23
+ This endpoint is intended to delete a single record. You have to provide a model class and (optionally) the name
24
+ of the column that it should use to lookup the matching record. If you don't specifically tell it what column to
25
+ use to lookup the record, it will assume that you want to use the id column of the model. Finally, you must
26
+ declare a route parameter with a matching column name: the delete endpoint will then fetch the desired record id
27
+ out of the URL path. The default request method is DELETE. Here's a simple example:
28
+
29
+ ```python
30
+ import clearskies
31
+
32
+
33
+ class User(clearskies.Model):
34
+ id_column_name = "id"
35
+ backend = clearskies.backends.MemoryBackend()
36
+ id = clearskies.columns.Uuid()
37
+ name = clearskies.columns.String()
38
+ username = clearskies.columns.String()
39
+
40
+
41
+ wsgi = clearskies.contexts.WsgiRef(
42
+ clearskies.endpoints.Delete(
43
+ model_class=User,
44
+ url="/{id}",
45
+ ),
46
+ bindings={
47
+ "memory_backend_default_data": [
48
+ {
49
+ "model_class": User,
50
+ "records": [
51
+ {"id": "1-2-3-4", "name": "Bob Brown", "username": "bobbrown"},
52
+ {"id": "1-2-3-5", "name": "Jane Doe", "username": "janedoe"},
53
+ {"id": "1-2-3-6", "name": "Greg", "username": "greg"},
54
+ ],
55
+ },
56
+ ],
57
+ },
58
+ )
59
+ wsgi()
60
+ ```
61
+
62
+ And when invoked:
63
+
64
+ ```bash
65
+ $ curl 'http://localhost:8080/1-2-3-4' -X DELETE | jq
66
+ {
67
+ "status": "success",
68
+ "error": "",
69
+ "data": {},
70
+ "pagination": {},
71
+ "input_errors": {}
72
+ }
73
+ ```
74
+ """
75
+
76
+ @clearskies.decorators.parameters_to_properties
77
+ def __init__(
78
+ self,
79
+ model_class: type[Model],
80
+ url: str,
81
+ record_lookup_column_name: str | None = None,
82
+ response_headers: list[str | Callable[..., list[str]]] = [],
83
+ request_methods: list[str] = ["DELETE"],
84
+ internal_casing: str = "snake_case",
85
+ external_casing: str = "snake_case",
86
+ security_headers: list[SecurityHeader] = [],
87
+ description: str = "",
88
+ where: typing.condition | list[typing.condition] = [],
89
+ joins: typing.join | list[typing.join] = [],
90
+ authentication: authentication.Authentication = authentication.Public(),
91
+ authorization: authentication.Authorization = authentication.Authorization(),
92
+ ):
93
+ # see comment in clearskies.endpoints.Create.__init__
94
+ self.request_methods = request_methods
95
+
96
+ # 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
97
+ # just stores them in parameters, which we have already done. However, the parent does do some extra initialization stuff that we need,
98
+ # which is why we have to call the parent.
99
+ super().__init__(model_class, url, [model_class.id_column_name])
100
+
101
+ def handle(self, input_output: InputOutput) -> Any:
102
+ model = self.fetch_model(input_output)
103
+ model.delete()
104
+ return self.success(input_output, {})
105
+
106
+ def documentation(self) -> list[autodoc.request.Request]:
107
+ output_autodoc = (autodoc.schema.Object(self.auto_case_internal_column_name("data"), children={}),)
108
+
109
+ authentication = self.authentication
110
+ standard_error_responses = [self.documentation_input_error_response()]
111
+ if not getattr(authentication, "is_public", False):
112
+ standard_error_responses.append(self.documentation_access_denied_response())
113
+ if getattr(authentication, "can_authorize", False):
114
+ standard_error_responses.append(self.documentation_unauthorized_response())
115
+
116
+ return [
117
+ autodoc.request.Request(
118
+ self.description,
119
+ [
120
+ self.documentation_success_response(
121
+ output_autodoc, # type: ignore
122
+ description=self.description,
123
+ ),
124
+ *standard_error_responses,
125
+ self.documentation_generic_error_response(),
126
+ ],
127
+ relative_path=self.url,
128
+ request_methods=self.request_methods,
129
+ parameters=[
130
+ *self.documentation_url_parameters(),
131
+ ],
132
+ root_properties={
133
+ "security": self.documentation_request_security(),
134
+ },
135
+ ),
136
+ ]
137
+
138
+ def documentation_models(self) -> dict[str, autodoc.schema.Schema]:
139
+ return {}
@@ -0,0 +1,271 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from collections import OrderedDict
5
+ from typing import TYPE_CHECKING, Any, Callable, Type
6
+
7
+ import clearskies.configs
8
+ import clearskies.exceptions
9
+ from clearskies import authentication, autodoc, typing
10
+ from clearskies.authentication import Authentication, Authorization
11
+ from clearskies.endpoint import Endpoint
12
+ from clearskies.functional import routing, string
13
+ from clearskies.input_outputs import InputOutput
14
+
15
+ if TYPE_CHECKING:
16
+ from clearskies import Column, Schema, SecurityHeader
17
+ from clearskies.model import Model
18
+
19
+
20
+ class Get(Endpoint):
21
+ """
22
+ An endpoint that fetches a single record by id (or other unique column).
23
+
24
+ This endpoint is intended to return a single record. You have to provide a model class, the list of columns
25
+ to return, and (optionally) the name of the column that it should use to lookup the matching record. If you don't
26
+ specifically tell it what column to use to lookup the record, it will assume that you want to use the id column
27
+ of the model. Finally, you must declare a route parameter with a matching column name: the get endpoint will then
28
+ fetch the desired record id out of the URL path. Here's a simple example:
29
+
30
+ ```python
31
+ import clearskies
32
+
33
+
34
+ class User(clearskies.Model):
35
+ id_column_name = "id"
36
+ backend = clearskies.backends.MemoryBackend()
37
+ id = clearskies.columns.Uuid()
38
+ name = clearskies.columns.String()
39
+ username = clearskies.columns.String()
40
+
41
+
42
+ wsgi = clearskies.contexts.WsgiRef(
43
+ clearskies.endpoints.Get(
44
+ model_class=User,
45
+ url="/{id}",
46
+ readable_column_names=["id", "name", "username"],
47
+ ),
48
+ bindings={
49
+ "memory_backend_default_data": [
50
+ {
51
+ "model_class": User,
52
+ "records": [
53
+ {"id": "1-2-3-4", "name": "Bob Brown", "username": "bobbrown"},
54
+ {"id": "1-2-3-5", "name": "Jane Doe", "username": "janedoe"},
55
+ {"id": "1-2-3-6", "name": "Greg", "username": "greg"},
56
+ ],
57
+ },
58
+ ],
59
+ },
60
+ )
61
+ wsgi()
62
+ ```
63
+
64
+ And when invoked:
65
+
66
+ ```bash
67
+ $ curl 'http://localhost:8080/1-2-3-4' | jq
68
+ {
69
+ "status": "success",
70
+ "error": "",
71
+ "data": {
72
+ "id": "1-2-3-4",
73
+ "name": "Bob Brown",
74
+ "username": "bobbrown"
75
+ },
76
+ "pagination": {},
77
+ "input_errors": {}
78
+ }
79
+
80
+ $ curl 'http://localhost:8080/1-2-3-5' | jq
81
+ {
82
+ "status": "success",
83
+ "error": "",
84
+ "data": {
85
+ "id": "1-2-3-5",
86
+ "name": "Jane Doe",
87
+ "username": "janedoe"
88
+ },
89
+ "pagination": {},
90
+ "input_errors": {}
91
+ }
92
+
93
+ $ curl 'http://localhost:8080/notauser' | jq
94
+ {
95
+ "status": "client_error",
96
+ "error": "Not Found",
97
+ "data": [],
98
+ "pagination": {},
99
+ "input_errors": {}
100
+ }
101
+ ```
102
+ """
103
+
104
+ """
105
+ Specify the name of the column that should be used to look up the record.
106
+
107
+ If not specified, it will default to the id column name. There must be a matching route parameter in the URL.
108
+
109
+ ```python
110
+ import clearskies
111
+
112
+ class User(clearskies.Model):
113
+ id_column_name = "id"
114
+ backend = clearskies.backends.MemoryBackend()
115
+ id = clearskies.columns.Uuid()
116
+ name = clearskies.columns.String()
117
+ username = clearskies.columns.String()
118
+
119
+ wsgi = clearskies.contexts.WsgiRef(
120
+ clearskies.endpoints.Get(
121
+ model_class=User,
122
+ url="/{username}",
123
+ readable_column_names=["id", "name", "username"],
124
+ record_lookup_column_name="username",
125
+ ),
126
+ bindings={
127
+ "memory_backend_default_data": [
128
+ {
129
+ "model_class": User,
130
+ "records": [
131
+ {"id": "1-2-3-4", "name": "Bob Brown", "username": "bobbrown"},
132
+ {"id": "1-2-3-5", "name": "Jane Doe", "username": "janedoe"},
133
+ {"id": "1-2-3-6", "name": "Greg", "username": "greg"},
134
+ ],
135
+ },
136
+ ],
137
+ },
138
+ )
139
+ wsgi()
140
+ ```
141
+
142
+ Note that `record_lookup_column_name` is set to `username` and we similarly changed the route from
143
+ `/{id}` to `/{username}`. We then invoke it with the username rather than the id:
144
+
145
+ ```bash
146
+ $ curl 'http://localhost:8080/janedoe' | jq
147
+ {
148
+ "status": "success",
149
+ "error": "",
150
+ "data": {
151
+ "id": "1-2-3-5",
152
+ "name": "Jane Doe",
153
+ "username": "janedoe"
154
+ },
155
+ "pagination": {},
156
+ "input_errors": {}
157
+ }
158
+ ```
159
+ """
160
+ record_lookup_column_name = clearskies.configs.ReadableModelColumn("model_class", default=None)
161
+
162
+ @clearskies.decorators.parameters_to_properties
163
+ def __init__(
164
+ self,
165
+ model_class: type[Model],
166
+ url: str,
167
+ readable_column_names: list[str],
168
+ record_lookup_column_name: str | None = None,
169
+ request_methods: list[str] = ["GET"],
170
+ response_headers: list[str | Callable[..., list[str]]] = [],
171
+ output_map: Callable[..., dict[str, Any]] | None = None,
172
+ output_schema: Schema | None = None,
173
+ column_overrides: dict[str, Column] = {},
174
+ internal_casing: str = "snake_case",
175
+ external_casing: str = "snake_case",
176
+ security_headers: list[SecurityHeader] = [],
177
+ description: str = "",
178
+ where: typing.condition | list[typing.condition] = [],
179
+ joins: typing.join | list[typing.join] = [],
180
+ authentication: Authentication = authentication.Public(),
181
+ authorization: Authorization = authentication.Authorization(),
182
+ ):
183
+ try:
184
+ # we will set the value for this if it isn't already set, and the easiest way is to just fetch it and see if it blows up
185
+ self.record_lookup_column_name
186
+ except:
187
+ self.record_lookup_column_name = self.model_class.id_column_name
188
+
189
+ # 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
190
+ # just stores them in parameters, which we have already done. However, the parent does do some extra initialization stuff that we need,
191
+ # which is why we have to call the parent.
192
+ super().__init__()
193
+
194
+ route_parameters = routing.extract_url_parameter_name_map(url)
195
+ if self.record_lookup_column_name not in route_parameters:
196
+ raise KeyError(
197
+ f"Configuration error for {self.__class__.__name__} endpoint: record_lookup_column_name is set to '{self.record_lookup_column_name}' but no matching routing parameter is found"
198
+ )
199
+
200
+ def get_model_id(self, input_output: InputOutput) -> str:
201
+ routing_data = input_output.routing_data
202
+ if self.record_lookup_column_name in routing_data:
203
+ return routing_data[self.record_lookup_column_name]
204
+ raise KeyError(
205
+ f"I didn't receive the ID in my routing data. I am probably misconfigured. My record_lookup_column_name is '{self.record_lookup_column_name}' and my route is {self.url}"
206
+ )
207
+
208
+ def fetch_model(self, input_output: InputOutput) -> Model:
209
+ lookup_column_value = self.get_model_id(input_output)
210
+ model = self.fetch_model_with_base_query(input_output).find(
211
+ self.record_lookup_column_name + "=" + lookup_column_value
212
+ )
213
+ if not model:
214
+ raise clearskies.exceptions.NotFound("Not Found")
215
+ return model
216
+
217
+ def handle(self, input_output: InputOutput) -> Any:
218
+ model = self.fetch_model(input_output)
219
+ return self.success(input_output, self.model_as_json(model, input_output))
220
+
221
+ def documentation(self) -> list[autodoc.request.Request]:
222
+ output_schema = self.model_class
223
+ nice_model = string.camel_case_to_words(output_schema.__name__)
224
+
225
+ schema_model_name = string.camel_case_to_snake_case(output_schema.__name__)
226
+ output_data_schema = self.documentation_data_schema(output_schema, self.readable_column_names)
227
+ output_autodoc = (
228
+ autodoc.schema.Object(
229
+ self.auto_case_internal_column_name("data"), children=output_data_schema, model_name=schema_model_name
230
+ ),
231
+ )
232
+
233
+ authentication = self.authentication
234
+ standard_error_responses = []
235
+ if not getattr(authentication, "is_public", False):
236
+ standard_error_responses.append(self.documentation_access_denied_response())
237
+ if getattr(authentication, "can_authorize", False):
238
+ standard_error_responses.append(self.documentation_unauthorized_response())
239
+
240
+ return [
241
+ autodoc.request.Request(
242
+ self.description,
243
+ [
244
+ self.documentation_success_response(
245
+ output_autodoc, # type: ignore
246
+ description=self.description,
247
+ ),
248
+ *standard_error_responses,
249
+ self.documentation_generic_error_response(),
250
+ ],
251
+ relative_path=self.url,
252
+ request_methods=self.request_methods,
253
+ parameters=[
254
+ *self.documentation_url_parameters(),
255
+ ],
256
+ root_properties={
257
+ "security": self.documentation_request_security(),
258
+ },
259
+ ),
260
+ ]
261
+
262
+ def documentation_models(self) -> dict[str, autodoc.schema.Schema]:
263
+ output_schema = self.output_schema if self.output_schema else self.model_class
264
+ schema_model_name = string.camel_case_to_snake_case(output_schema.__name__)
265
+
266
+ return {
267
+ schema_model_name: autodoc.schema.Object(
268
+ self.auto_case_internal_column_name("data"),
269
+ children=self.documentation_data_schema(output_schema, self.readable_column_names),
270
+ ),
271
+ }
@@ -0,0 +1,183 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from typing import Any, Callable, Type
5
+
6
+ import clearskies.configs
7
+ import clearskies.exceptions
8
+ from clearskies import autodoc, typing
9
+ from clearskies.endpoint import Endpoint
10
+ from clearskies.functional import routing, string
11
+ from clearskies.input_outputs import InputOutput
12
+
13
+
14
+ class HealthCheck(Endpoint):
15
+ """
16
+ An endpoint that returns 200/500 to denote if backend services are functional.
17
+
18
+ You can provide dependency injection names, classes, or a callable. When invoked, this endpoint
19
+ will build/call all of them and, as long as none raise any exceptions, return a 200.
20
+
21
+ HealthCheck endpoints are always public and ignore authentication/authorization settings.
22
+
23
+ If you don't provide any configuration to the endpoint, it will always succeed:
24
+
25
+ ```python
26
+ import clearskies
27
+
28
+ wsgi = clearskies.contexts.WsgiRef(
29
+ clearskies.endpoints.HealthCheck(),
30
+ )
31
+ wsgi()
32
+ ```
33
+
34
+ which when invoked:
35
+
36
+ ```bash
37
+ $ curl 'http://localhost:8080' | jq
38
+ {
39
+ "status": "success",
40
+ "error": "",
41
+ "data": {},
42
+ "pagination": {},
43
+ "input_errors": {}
44
+ }
45
+ ```
46
+
47
+ This example demonstrates a failed healthcheck by requesting the cursor (which attempts to connect to the database).
48
+ Since no database has been setup/configured, it always fails:
49
+
50
+ ```python
51
+ import clearskies
52
+
53
+ wsgi = clearskies.contexts.WsgiRef(
54
+ clearskies.endpoints.HealthCheck(
55
+ dependency_injection_names=["cursor"],
56
+ ),
57
+ )
58
+ wsgi()
59
+ ```
60
+
61
+ And when invoked returns:
62
+
63
+ ```bash
64
+ $ curl 'http://localhost:8080' | jq
65
+ {
66
+ "status": "failure",
67
+ "error": "",
68
+ "data": {},
69
+ "pagination": {},
70
+ "input_errors": {}
71
+ }
72
+ ```
73
+
74
+ with a status code of 500.
75
+ """
76
+
77
+ """
78
+ A list of dependency injection names that should be fetched when the healthcheck endpoint is invoked.
79
+
80
+ If any exceptions are raised when building the dependency injection parameters, the health check will return
81
+ failure.
82
+ """
83
+ dependency_injection_names = clearskies.configs.StringList(default=[])
84
+
85
+ """
86
+ A list of classes to build with the dependency injection system.
87
+
88
+ The If any exceptions are raised when building the classes, then the healthcheck will return a failure.
89
+ In the following example, since the class-to-build requests the cursor, and we don't have a reachable
90
+ database configured,
91
+
92
+ ```python
93
+ import clearskies
94
+
95
+ class MyClass:
96
+ def __init__(self, cursor):
97
+ pass
98
+
99
+ wsgi = clearskies.contexts.WsgiRef(
100
+ clearskies.endpoints.HealthCheck(
101
+ classes_to_build=[MyClass],
102
+ ),
103
+ )
104
+ wsgi()
105
+ ```
106
+ """
107
+ classes_to_build = clearskies.configs.Any(default=[])
108
+
109
+ """
110
+ A list of callables to invoke.
111
+
112
+ Your callables can request any dependency injection names. If any exceptions are raised, the healthcheck will
113
+ return a failure. The return value from the function is ignored. In this example we request the cursor from
114
+ the dependency injection system, which will call the healthcheck to fail since we don't have a database setup
115
+ and configured:
116
+
117
+ ```python
118
+ import clearskies
119
+
120
+ def my_function(cursor):
121
+ pass
122
+
123
+ wsgi = clearskies.contexts.WsgiRef(
124
+ clearskies.endpoints.HealthCheck(
125
+ callables=[my_function],
126
+ ),
127
+ )
128
+ wsgi()
129
+ ```
130
+ """
131
+ callables = clearskies.configs.Any(default=[])
132
+
133
+ @clearskies.decorators.parameters_to_properties
134
+ def __init__(
135
+ self,
136
+ dependency_injection_names: list[str] = [],
137
+ classes_to_build: list[type] = [],
138
+ callables: list[Callable] = [],
139
+ description: str = "",
140
+ url: str = "",
141
+ request_methods: list[str] = ["GET"],
142
+ ):
143
+ # 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
144
+ # just stores them in parameters, which we have already done. However, the parent does do some extra initialization stuff that we need,
145
+ # which is why we have to call the parent.
146
+ super().__init__()
147
+
148
+ def handle(self, input_output: InputOutput) -> Any:
149
+ try:
150
+ for name in self.dependency_injection_names:
151
+ self.di.build_from_name(name)
152
+
153
+ for class_to_build in self.classes_to_build:
154
+ self.di.build_class(class_to_build)
155
+
156
+ for thing_to_call in self.callables:
157
+ self.di.call_function(thing_to_call)
158
+ except:
159
+ return self.failure(input_output)
160
+
161
+ return self.success(input_output, {})
162
+
163
+ def documentation(self) -> list[autodoc.request.Request]:
164
+ output_schema = self.model_class
165
+ output_autodoc = (autodoc.schema.Object(self.auto_case_internal_column_name("data"), children=[]),)
166
+
167
+ description = self.description if self.description else "Health Check"
168
+ return [
169
+ autodoc.request.Request(
170
+ description,
171
+ [
172
+ self.documentation_success_response(
173
+ output_autodoc, # type: ignore
174
+ description=description,
175
+ ),
176
+ ],
177
+ relative_path=self.url,
178
+ request_methods=self.request_methods,
179
+ parameters=[
180
+ *self.documentation_url_parameters(),
181
+ ],
182
+ ),
183
+ ]