clear-skies 2.0.27__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of clear-skies might be problematic. Click here for more details.

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