clear-skies 1.22.31__py3-none-any.whl → 2.0.1__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 (345) hide show
  1. {clear_skies-1.22.31.dist-info → clear_skies-2.0.1.dist-info}/METADATA +12 -14
  2. clear_skies-2.0.1.dist-info/RECORD +249 -0
  3. {clear_skies-1.22.31.dist-info → clear_skies-2.0.1.dist-info}/WHEEL +1 -1
  4. clearskies/__init__.py +42 -25
  5. clearskies/action.py +7 -0
  6. clearskies/authentication/__init__.py +8 -41
  7. clearskies/authentication/authentication.py +46 -0
  8. clearskies/authentication/authorization.py +8 -9
  9. clearskies/authentication/authorization_pass_through.py +11 -9
  10. clearskies/authentication/jwks.py +133 -58
  11. clearskies/authentication/public.py +3 -38
  12. clearskies/authentication/secret_bearer.py +516 -54
  13. clearskies/autodoc/formats/oai3_json/__init__.py +1 -1
  14. clearskies/autodoc/formats/oai3_json/oai3_json.py +9 -7
  15. clearskies/autodoc/formats/oai3_json/parameter.py +6 -3
  16. clearskies/autodoc/formats/oai3_json/request.py +7 -5
  17. clearskies/autodoc/formats/oai3_json/response.py +7 -4
  18. clearskies/autodoc/formats/oai3_json/schema/object.py +4 -1
  19. clearskies/autodoc/request/__init__.py +2 -0
  20. clearskies/autodoc/request/header.py +4 -6
  21. clearskies/autodoc/request/json_body.py +4 -6
  22. clearskies/autodoc/request/parameter.py +8 -0
  23. clearskies/autodoc/request/request.py +7 -4
  24. clearskies/autodoc/request/url_parameter.py +4 -6
  25. clearskies/autodoc/request/url_path.py +4 -6
  26. clearskies/autodoc/schema/__init__.py +4 -2
  27. clearskies/autodoc/schema/array.py +5 -6
  28. clearskies/autodoc/schema/boolean.py +4 -10
  29. clearskies/autodoc/schema/date.py +0 -3
  30. clearskies/autodoc/schema/datetime.py +1 -4
  31. clearskies/autodoc/schema/double.py +0 -3
  32. clearskies/autodoc/schema/enum.py +4 -2
  33. clearskies/autodoc/schema/integer.py +4 -9
  34. clearskies/autodoc/schema/long.py +0 -3
  35. clearskies/autodoc/schema/number.py +4 -9
  36. clearskies/autodoc/schema/object.py +5 -7
  37. clearskies/autodoc/schema/password.py +0 -3
  38. clearskies/autodoc/schema/schema.py +11 -0
  39. clearskies/autodoc/schema/string.py +4 -10
  40. clearskies/backends/__init__.py +55 -20
  41. clearskies/backends/api_backend.py +1100 -284
  42. clearskies/backends/backend.py +53 -84
  43. clearskies/backends/cursor_backend.py +236 -186
  44. clearskies/backends/memory_backend.py +519 -226
  45. clearskies/backends/secrets_backend.py +75 -31
  46. clearskies/column.py +1229 -0
  47. clearskies/columns/__init__.py +71 -0
  48. clearskies/columns/audit.py +205 -0
  49. clearskies/columns/belongs_to_id.py +483 -0
  50. clearskies/columns/belongs_to_model.py +128 -0
  51. clearskies/columns/belongs_to_self.py +105 -0
  52. clearskies/columns/boolean.py +109 -0
  53. clearskies/columns/category_tree.py +275 -0
  54. clearskies/columns/category_tree_ancestors.py +51 -0
  55. clearskies/columns/category_tree_children.py +127 -0
  56. clearskies/columns/category_tree_descendants.py +48 -0
  57. clearskies/columns/created.py +94 -0
  58. clearskies/columns/created_by_authorization_data.py +116 -0
  59. clearskies/columns/created_by_header.py +99 -0
  60. clearskies/columns/created_by_ip.py +92 -0
  61. clearskies/columns/created_by_routing_data.py +96 -0
  62. clearskies/columns/created_by_user_agent.py +92 -0
  63. clearskies/columns/date.py +230 -0
  64. clearskies/columns/datetime.py +278 -0
  65. clearskies/columns/email.py +76 -0
  66. clearskies/columns/float.py +149 -0
  67. clearskies/columns/has_many.py +505 -0
  68. clearskies/columns/has_many_self.py +56 -0
  69. clearskies/columns/has_one.py +14 -0
  70. clearskies/columns/integer.py +156 -0
  71. clearskies/columns/json.py +122 -0
  72. clearskies/columns/many_to_many_ids.py +333 -0
  73. clearskies/columns/many_to_many_ids_with_data.py +270 -0
  74. clearskies/columns/many_to_many_models.py +154 -0
  75. clearskies/columns/many_to_many_pivots.py +133 -0
  76. clearskies/columns/phone.py +158 -0
  77. clearskies/columns/select.py +91 -0
  78. clearskies/columns/string.py +98 -0
  79. clearskies/columns/timestamp.py +160 -0
  80. clearskies/columns/updated.py +110 -0
  81. clearskies/columns/uuid.py +86 -0
  82. clearskies/configs/README.md +105 -0
  83. clearskies/configs/__init__.py +162 -0
  84. clearskies/configs/actions.py +43 -0
  85. clearskies/configs/any.py +13 -0
  86. clearskies/configs/any_dict.py +22 -0
  87. clearskies/configs/any_dict_or_callable.py +23 -0
  88. clearskies/configs/authentication.py +23 -0
  89. clearskies/configs/authorization.py +23 -0
  90. clearskies/configs/boolean.py +16 -0
  91. clearskies/configs/boolean_or_callable.py +18 -0
  92. clearskies/configs/callable_config.py +18 -0
  93. clearskies/configs/columns.py +34 -0
  94. clearskies/configs/conditions.py +30 -0
  95. clearskies/configs/config.py +24 -0
  96. clearskies/configs/datetime.py +18 -0
  97. clearskies/configs/datetime_or_callable.py +19 -0
  98. clearskies/configs/endpoint.py +23 -0
  99. clearskies/configs/endpoint_list.py +28 -0
  100. clearskies/configs/float.py +16 -0
  101. clearskies/configs/float_or_callable.py +18 -0
  102. clearskies/configs/integer.py +16 -0
  103. clearskies/configs/integer_or_callable.py +18 -0
  104. clearskies/configs/joins.py +30 -0
  105. clearskies/configs/list_any_dict.py +30 -0
  106. clearskies/configs/list_any_dict_or_callable.py +31 -0
  107. clearskies/configs/model_class.py +35 -0
  108. clearskies/configs/model_column.py +65 -0
  109. clearskies/configs/model_columns.py +56 -0
  110. clearskies/configs/model_destination_name.py +25 -0
  111. clearskies/configs/model_to_id_column.py +43 -0
  112. clearskies/configs/readable_model_column.py +9 -0
  113. clearskies/configs/readable_model_columns.py +9 -0
  114. clearskies/configs/schema.py +23 -0
  115. clearskies/configs/searchable_model_columns.py +9 -0
  116. clearskies/configs/security_headers.py +39 -0
  117. clearskies/configs/select.py +26 -0
  118. clearskies/configs/select_list.py +47 -0
  119. clearskies/configs/string.py +29 -0
  120. clearskies/configs/string_dict.py +32 -0
  121. clearskies/configs/string_list.py +32 -0
  122. clearskies/configs/string_list_or_callable.py +35 -0
  123. clearskies/configs/string_or_callable.py +18 -0
  124. clearskies/configs/timedelta.py +18 -0
  125. clearskies/configs/timezone.py +18 -0
  126. clearskies/configs/url.py +23 -0
  127. clearskies/configs/validators.py +45 -0
  128. clearskies/configs/writeable_model_column.py +9 -0
  129. clearskies/configs/writeable_model_columns.py +9 -0
  130. clearskies/configurable.py +76 -0
  131. clearskies/contexts/__init__.py +8 -8
  132. clearskies/contexts/cli.py +8 -41
  133. clearskies/contexts/context.py +91 -56
  134. clearskies/contexts/wsgi.py +16 -29
  135. clearskies/contexts/wsgi_ref.py +53 -0
  136. clearskies/di/__init__.py +10 -7
  137. clearskies/di/additional_config.py +115 -4
  138. clearskies/di/additional_config_auto_import.py +12 -0
  139. clearskies/di/di.py +742 -121
  140. clearskies/di/inject/__init__.py +23 -0
  141. clearskies/di/inject/by_class.py +21 -0
  142. clearskies/di/inject/by_name.py +18 -0
  143. clearskies/di/inject/di.py +13 -0
  144. clearskies/di/inject/environment.py +14 -0
  145. clearskies/di/inject/input_output.py +20 -0
  146. clearskies/di/inject/now.py +13 -0
  147. clearskies/di/inject/requests.py +13 -0
  148. clearskies/di/inject/secrets.py +14 -0
  149. clearskies/di/inject/utcnow.py +13 -0
  150. clearskies/di/inject/uuid.py +15 -0
  151. clearskies/di/injectable.py +29 -0
  152. clearskies/di/injectable_properties.py +131 -0
  153. clearskies/end.py +183 -0
  154. clearskies/endpoint.py +1310 -0
  155. clearskies/endpoint_group.py +310 -0
  156. clearskies/endpoints/__init__.py +23 -0
  157. clearskies/endpoints/advanced_search.py +526 -0
  158. clearskies/endpoints/callable.py +388 -0
  159. clearskies/endpoints/create.py +202 -0
  160. clearskies/endpoints/delete.py +139 -0
  161. clearskies/endpoints/get.py +275 -0
  162. clearskies/endpoints/health_check.py +181 -0
  163. clearskies/endpoints/list.py +573 -0
  164. clearskies/endpoints/restful_api.py +427 -0
  165. clearskies/endpoints/simple_search.py +286 -0
  166. clearskies/endpoints/update.py +190 -0
  167. clearskies/environment.py +5 -3
  168. clearskies/exceptions/__init__.py +17 -0
  169. clearskies/{handlers/exceptions/input_error.py → exceptions/input_errors.py} +1 -1
  170. clearskies/exceptions/moved_permanently.py +3 -0
  171. clearskies/exceptions/moved_temporarily.py +3 -0
  172. clearskies/exceptions/not_found.py +2 -0
  173. clearskies/functional/__init__.py +2 -2
  174. clearskies/functional/routing.py +92 -0
  175. clearskies/functional/string.py +19 -11
  176. clearskies/functional/validations.py +61 -9
  177. clearskies/input_outputs/__init__.py +9 -7
  178. clearskies/input_outputs/cli.py +130 -142
  179. clearskies/input_outputs/exceptions/__init__.py +1 -1
  180. clearskies/input_outputs/headers.py +45 -0
  181. clearskies/input_outputs/input_output.py +91 -122
  182. clearskies/input_outputs/programmatic.py +69 -0
  183. clearskies/input_outputs/wsgi.py +23 -38
  184. clearskies/model.py +984 -183
  185. clearskies/parameters_to_properties.py +31 -0
  186. clearskies/query/__init__.py +12 -0
  187. clearskies/query/condition.py +223 -0
  188. clearskies/query/join.py +136 -0
  189. clearskies/query/query.py +196 -0
  190. clearskies/query/sort.py +27 -0
  191. clearskies/schema.py +82 -0
  192. clearskies/secrets/__init__.py +3 -31
  193. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer.py +15 -4
  194. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +11 -5
  195. clearskies/secrets/akeyless.py +88 -147
  196. clearskies/secrets/secrets.py +8 -8
  197. clearskies/security_header.py +15 -0
  198. clearskies/security_headers/__init__.py +8 -8
  199. clearskies/security_headers/cache_control.py +47 -110
  200. clearskies/security_headers/cors.py +40 -95
  201. clearskies/security_headers/csp.py +76 -151
  202. clearskies/security_headers/hsts.py +14 -16
  203. clearskies/test_base.py +8 -0
  204. clearskies/typing.py +11 -0
  205. clearskies/validator.py +37 -0
  206. clearskies/validators/__init__.py +33 -0
  207. clearskies/validators/after_column.py +62 -0
  208. clearskies/validators/before_column.py +13 -0
  209. clearskies/validators/in_the_future.py +32 -0
  210. clearskies/validators/in_the_future_at_least.py +11 -0
  211. clearskies/validators/in_the_future_at_most.py +10 -0
  212. clearskies/validators/in_the_past.py +32 -0
  213. clearskies/validators/in_the_past_at_least.py +10 -0
  214. clearskies/validators/in_the_past_at_most.py +10 -0
  215. clearskies/validators/maximum_length.py +26 -0
  216. clearskies/validators/maximum_value.py +29 -0
  217. clearskies/validators/minimum_length.py +26 -0
  218. clearskies/validators/minimum_value.py +29 -0
  219. clearskies/validators/required.py +35 -0
  220. clearskies/validators/timedelta.py +59 -0
  221. clearskies/validators/unique.py +31 -0
  222. clear_skies-1.22.31.dist-info/RECORD +0 -214
  223. clearskies/application.py +0 -29
  224. clearskies/authentication/auth0_jwks.py +0 -118
  225. clearskies/authentication/auth_exception.py +0 -2
  226. clearskies/authentication/jwks_jwcrypto.py +0 -51
  227. clearskies/backends/api_get_only_backend.py +0 -48
  228. clearskies/backends/example_backend.py +0 -43
  229. clearskies/backends/file_backend.py +0 -48
  230. clearskies/backends/json_backend.py +0 -7
  231. clearskies/backends/restful_api_advanced_search_backend.py +0 -103
  232. clearskies/binding_config.py +0 -16
  233. clearskies/column_types/__init__.py +0 -203
  234. clearskies/column_types/audit.py +0 -249
  235. clearskies/column_types/belongs_to.py +0 -271
  236. clearskies/column_types/boolean.py +0 -60
  237. clearskies/column_types/category_tree.py +0 -304
  238. clearskies/column_types/column.py +0 -373
  239. clearskies/column_types/created.py +0 -26
  240. clearskies/column_types/created_by_authorization_data.py +0 -26
  241. clearskies/column_types/created_by_header.py +0 -24
  242. clearskies/column_types/created_by_ip.py +0 -17
  243. clearskies/column_types/created_by_routing_data.py +0 -25
  244. clearskies/column_types/created_by_user_agent.py +0 -17
  245. clearskies/column_types/created_micro.py +0 -26
  246. clearskies/column_types/datetime.py +0 -109
  247. clearskies/column_types/datetime_micro.py +0 -12
  248. clearskies/column_types/email.py +0 -18
  249. clearskies/column_types/float.py +0 -43
  250. clearskies/column_types/has_many.py +0 -179
  251. clearskies/column_types/has_one.py +0 -60
  252. clearskies/column_types/integer.py +0 -41
  253. clearskies/column_types/json.py +0 -25
  254. clearskies/column_types/many_to_many.py +0 -278
  255. clearskies/column_types/many_to_many_with_data.py +0 -162
  256. clearskies/column_types/phone.py +0 -48
  257. clearskies/column_types/select.py +0 -11
  258. clearskies/column_types/string.py +0 -24
  259. clearskies/column_types/timestamp.py +0 -73
  260. clearskies/column_types/updated.py +0 -26
  261. clearskies/column_types/updated_micro.py +0 -26
  262. clearskies/column_types/uuid.py +0 -25
  263. clearskies/columns.py +0 -123
  264. clearskies/condition_parser.py +0 -172
  265. clearskies/contexts/build_context.py +0 -54
  266. clearskies/contexts/convert_to_application.py +0 -190
  267. clearskies/contexts/extract_handler.py +0 -37
  268. clearskies/contexts/test.py +0 -94
  269. clearskies/decorators/__init__.py +0 -41
  270. clearskies/decorators/allow_non_json_bodies.py +0 -9
  271. clearskies/decorators/auth0_jwks.py +0 -22
  272. clearskies/decorators/authorization.py +0 -10
  273. clearskies/decorators/binding_classes.py +0 -9
  274. clearskies/decorators/binding_modules.py +0 -9
  275. clearskies/decorators/bindings.py +0 -9
  276. clearskies/decorators/create.py +0 -10
  277. clearskies/decorators/delete.py +0 -10
  278. clearskies/decorators/docs.py +0 -14
  279. clearskies/decorators/get.py +0 -10
  280. clearskies/decorators/jwks.py +0 -26
  281. clearskies/decorators/merge.py +0 -124
  282. clearskies/decorators/patch.py +0 -10
  283. clearskies/decorators/post.py +0 -10
  284. clearskies/decorators/public.py +0 -11
  285. clearskies/decorators/response_headers.py +0 -10
  286. clearskies/decorators/return_raw_response.py +0 -9
  287. clearskies/decorators/schema.py +0 -10
  288. clearskies/decorators/secret_bearer.py +0 -24
  289. clearskies/decorators/security_headers.py +0 -10
  290. clearskies/di/standard_dependencies.py +0 -151
  291. clearskies/handlers/__init__.py +0 -41
  292. clearskies/handlers/advanced_search.py +0 -271
  293. clearskies/handlers/base.py +0 -479
  294. clearskies/handlers/callable.py +0 -192
  295. clearskies/handlers/create.py +0 -35
  296. clearskies/handlers/crud_by_method.py +0 -18
  297. clearskies/handlers/database_connector.py +0 -32
  298. clearskies/handlers/delete.py +0 -61
  299. clearskies/handlers/exceptions/__init__.py +0 -5
  300. clearskies/handlers/exceptions/not_found.py +0 -3
  301. clearskies/handlers/get.py +0 -156
  302. clearskies/handlers/health_check.py +0 -59
  303. clearskies/handlers/input_processing.py +0 -79
  304. clearskies/handlers/list.py +0 -530
  305. clearskies/handlers/mygrations.py +0 -82
  306. clearskies/handlers/request_method_routing.py +0 -47
  307. clearskies/handlers/restful_api.py +0 -218
  308. clearskies/handlers/routing.py +0 -62
  309. clearskies/handlers/schema_helper.py +0 -128
  310. clearskies/handlers/simple_routing.py +0 -206
  311. clearskies/handlers/simple_routing_route.py +0 -197
  312. clearskies/handlers/simple_search.py +0 -136
  313. clearskies/handlers/update.py +0 -102
  314. clearskies/handlers/write.py +0 -193
  315. clearskies/input_requirements/__init__.py +0 -78
  316. clearskies/input_requirements/after.py +0 -36
  317. clearskies/input_requirements/before.py +0 -36
  318. clearskies/input_requirements/in_the_future_at_least.py +0 -19
  319. clearskies/input_requirements/in_the_future_at_most.py +0 -19
  320. clearskies/input_requirements/in_the_past_at_least.py +0 -19
  321. clearskies/input_requirements/in_the_past_at_most.py +0 -19
  322. clearskies/input_requirements/maximum_length.py +0 -19
  323. clearskies/input_requirements/maximum_value.py +0 -19
  324. clearskies/input_requirements/minimum_length.py +0 -22
  325. clearskies/input_requirements/minimum_value.py +0 -19
  326. clearskies/input_requirements/required.py +0 -23
  327. clearskies/input_requirements/requirement.py +0 -25
  328. clearskies/input_requirements/time_delta.py +0 -38
  329. clearskies/input_requirements/unique.py +0 -18
  330. clearskies/mocks/__init__.py +0 -7
  331. clearskies/mocks/input_output.py +0 -124
  332. clearskies/mocks/models.py +0 -142
  333. clearskies/models.py +0 -350
  334. clearskies/security_headers/base.py +0 -12
  335. clearskies/tests/simple_api/models/__init__.py +0 -2
  336. clearskies/tests/simple_api/models/status.py +0 -23
  337. clearskies/tests/simple_api/models/user.py +0 -21
  338. clearskies/tests/simple_api/users_api.py +0 -64
  339. {clear_skies-1.22.31.dist-info → clear_skies-2.0.1.dist-info}/LICENSE +0 -0
  340. /clearskies/{contexts/bash.py → autodoc/py.typed} +0 -0
  341. /clearskies/{handlers/exceptions → exceptions}/authentication.py +0 -0
  342. /clearskies/{handlers/exceptions → exceptions}/authorization.py +0 -0
  343. /clearskies/{handlers/exceptions → exceptions}/client_error.py +0 -0
  344. /clearskies/{tests/__init__.py → input_outputs/py.typed} +0 -0
  345. /clearskies/{tests/simple_api/__init__.py → py.typed} +0 -0
@@ -0,0 +1,286 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from collections import OrderedDict
5
+ from typing import TYPE_CHECKING, Any, Callable
6
+
7
+ import clearskies.configs
8
+ import clearskies.exceptions
9
+ from clearskies import authentication, autodoc, typing
10
+ from clearskies.endpoints.list import List
11
+ from clearskies.functional import string
12
+ from clearskies.input_outputs import InputOutput
13
+
14
+ if TYPE_CHECKING:
15
+ from clearskies import Column, Schema, SecurityHeader
16
+ from clearskies.model import Model
17
+
18
+
19
+ class SimpleSearch(List):
20
+ """
21
+ Create an endpoint that supports searching by exact values via url/JSON parameters.
22
+
23
+ This acts exactly like the list endpoint but additionally grants the client the ability to search records
24
+ via URL parameters or JSON POST body parameters. You just have to specify which columns are searchable.
25
+
26
+ In the following example we tell the `SimpleSearch` endpoint that we want it to return records from the
27
+ `Student` model, return `id`, `name`, and `grade` in the results, and allow the user to search by
28
+ `name` and `grade`. We also seed the memory backend with data so the endpoint has something to return:
29
+
30
+ ```python
31
+ import clearskies
32
+
33
+
34
+ class Student(clearskies.Model):
35
+ backend = clearskies.backends.MemoryBackend()
36
+ id_column_name = "id"
37
+
38
+ id = clearskies.columns.Uuid()
39
+ name = clearskies.columns.String()
40
+ grade = clearskies.columns.Integer()
41
+
42
+
43
+ wsgi = clearskies.contexts.WsgiRef(
44
+ clearskies.endpoints.SimpleSearch(
45
+ Student,
46
+ readable_column_names=["id", "name", "grade"],
47
+ sortable_column_names=["name", "grade"],
48
+ searchable_column_names=["name", "grade"],
49
+ default_sort_column_name="name",
50
+ ),
51
+ bindings={
52
+ "memory_backend_default_data": [
53
+ {
54
+ "model_class": Student,
55
+ "records": [
56
+ {"id": "1-2-3-4", "name": "Bob", "grade": 5},
57
+ {"id": "1-2-3-5", "name": "Jane", "grade": 3},
58
+ {"id": "1-2-3-6", "name": "Greg", "grade": 3},
59
+ {"id": "1-2-3-7", "name": "Bob", "grade": 2},
60
+ ],
61
+ },
62
+ ],
63
+ },
64
+ )
65
+ wsgi()
66
+ ```
67
+
68
+ Here is the basic operation of the endpoint itself, without any search parameters, in which case it behaves
69
+ identically to the list endpoint:
70
+
71
+ ```bash
72
+ $ curl 'http://localhost:8080' | jq
73
+ {
74
+ "status": "success",
75
+ "error": "",
76
+ "data": [
77
+ {
78
+ "id": "1-2-3-4",
79
+ "name": "Bob",
80
+ "grade": 5
81
+ },
82
+ {
83
+ "id": "1-2-3-7",
84
+ "name": "Bob",
85
+ "grade": 2
86
+ },
87
+ {
88
+ "id": "1-2-3-6",
89
+ "name": "Greg",
90
+ "grade": 3
91
+ },
92
+ {
93
+ "id": "1-2-3-5",
94
+ "name": "Jane",
95
+ "grade": 3
96
+ }
97
+ ],
98
+ "pagination": {},
99
+ "input_errors": {}
100
+ }
101
+ ```
102
+
103
+ We can then search on name via the `name` URL parameter:
104
+
105
+ ```bash
106
+ $ curl 'http://localhost:8080?name=Bob' | jq
107
+ {
108
+ "status": "success",
109
+ "error": "",
110
+ "data": [
111
+ {
112
+ "id": "1-2-3-4",
113
+ "name": "Bob",
114
+ "grade": 5
115
+ },
116
+ {
117
+ "id": "1-2-3-7",
118
+ "name": "Bob",
119
+ "grade": 2
120
+ }
121
+ ],
122
+ "pagination": {},
123
+ "input_errors": {}
124
+ }
125
+ ```
126
+
127
+ and multiple search terms are allowed:
128
+
129
+ ```bash
130
+ $ curl 'http://localhost:8080?name=Bob&grade=2' | jq
131
+ {
132
+ "status": "success",
133
+ "error": "",
134
+ "data": [
135
+ {
136
+ "id": "1-2-3-7",
137
+ "name": "Bob",
138
+ "grade": 2
139
+ }
140
+ ],
141
+ "pagination": {},
142
+ "input_errors": {}
143
+ }
144
+ ```
145
+
146
+ Pagination and sorting work just like with the list endpoint:
147
+
148
+ ```bash
149
+ $ curl 'http://localhost:8080?sort=grade&direction=desc&limit=2' | jq
150
+ {
151
+ "status": "success",
152
+ "error": "",
153
+ "data": [
154
+ {
155
+ "id": "1-2-3-4",
156
+ "name": "Bob",
157
+ "grade": 5
158
+ },
159
+ {
160
+ "id": "1-2-3-5",
161
+ "name": "Jane",
162
+ "grade": 3
163
+ }
164
+ ],
165
+ "pagination": {
166
+ "number_results": 4,
167
+ "limit": 2,
168
+ "next_page": {
169
+ "start": 2
170
+ }
171
+ },
172
+ "input_errors": {}
173
+ }
174
+
175
+ $ curl 'http://localhost:8080?sort=grade&direction=desc&limit=2&start=2' | jq
176
+ {
177
+ "status": "success",
178
+ "error": "",
179
+ "data": [
180
+ {
181
+ "id": "1-2-3-6",
182
+ "name": "Greg",
183
+ "grade": 3
184
+ },
185
+ {
186
+ "id": "1-2-3-7",
187
+ "name": "Bob",
188
+ "grade": 2
189
+ }
190
+ ],
191
+ "pagination": {},
192
+ "input_errors": {}
193
+ }
194
+ ```
195
+ """
196
+
197
+ @clearskies.parameters_to_properties.parameters_to_properties
198
+ def __init__(
199
+ self,
200
+ model_class: type[Model],
201
+ readable_column_names: list[str],
202
+ sortable_column_names: list[str],
203
+ searchable_column_names: list[str],
204
+ default_sort_column_name: str,
205
+ default_sort_direction: str = "ASC",
206
+ default_limit: int = 50,
207
+ maximum_limit: int = 200,
208
+ where: typing.condition | list[typing.condition] = [],
209
+ joins: typing.join | list[typing.join] = [],
210
+ url: str = "",
211
+ request_methods: list[str] = ["GET", "POST", "QUERY"],
212
+ response_headers: list[str | Callable[..., list[str]]] = [],
213
+ output_map: Callable[..., dict[str, Any]] | None = None,
214
+ output_schema: Schema | None = None,
215
+ column_overrides: dict[str, Column] = {},
216
+ internal_casing: str = "snake_case",
217
+ external_casing: str = "snake_case",
218
+ security_headers: list[SecurityHeader] = [],
219
+ description: str = "",
220
+ authentication: authentication.Authentication = authentication.Public(),
221
+ authorization: authentication.Authorization = authentication.Authorization(),
222
+ ):
223
+ self.request_methods = request_methods
224
+
225
+ # 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
226
+ # just stores them in parameters, which we have already done. However, the parent does do some extra initialization stuff that we need,
227
+ # which is why we have to call the parent.
228
+ super().__init__(model_class, readable_column_names, sortable_column_names, default_sort_column_name)
229
+
230
+ def check_search_in_request_data(self, request_data: dict[str, Any], query_parameters: dict[str, Any]) -> None:
231
+ for input_source_label, input_data in [("request body", request_data), ("URL data", query_parameters)]:
232
+ for column_name, value in input_data.items():
233
+ if column_name in self.allowed_request_keys and column_name not in self.searchable_column_names:
234
+ continue
235
+ if column_name not in self.searchable_column_names:
236
+ raise clearskies.exceptions.ClientError(
237
+ f"Invalid request parameter found in {input_source_label}: '{column_name}'"
238
+ )
239
+ [relationship_column_name, final_column_name] = self.unpack_column_name_with_relationship(column_name)
240
+ column_to_check = relationship_column_name if relationship_column_name else final_column_name
241
+ value_error = self.searchable_columns[column_to_check].check_search_value(
242
+ value, relationship_reference=final_column_name
243
+ )
244
+ if value_error:
245
+ raise clearskies.exceptions.InputErrors({column_name: value_error})
246
+
247
+ def configure_model_from_request_data(
248
+ self,
249
+ model: Model,
250
+ request_data: dict[str, Any],
251
+ query_parameters: dict[str, Any],
252
+ pagination_data: dict[str, Any],
253
+ ) -> Model:
254
+ model = super().configure_model_from_request_data(
255
+ model,
256
+ request_data,
257
+ query_parameters,
258
+ pagination_data,
259
+ )
260
+
261
+ for input_source in [request_data, query_parameters]:
262
+ for column_name, value in input_source.items():
263
+ if column_name not in self.searchable_column_names:
264
+ continue
265
+
266
+ model = self.add_join(column_name, model)
267
+ [relationship_column_name, column_name] = self.unpack_column_name_with_relationship(column_name)
268
+ if relationship_column_name:
269
+ self.columns[relationship_column_name].add_search(model, value, relationship_reference=column_name)
270
+ else:
271
+ model = self.columns[column_name].add_search(model, value, operator="=")
272
+
273
+ return model
274
+
275
+ def documentation_url_search_parameters(self) -> list[autodoc.request.Parameter]:
276
+ docs = []
277
+ for column in self._get_searchable_columns().values():
278
+ column_doc = column.documentation()
279
+ column_doc.name = self.auto_case_internal_column_name(column_doc.name)
280
+ docs.append(
281
+ autodoc.request.URLParameter(
282
+ column_doc,
283
+ description=f"Search by {column_doc.name} (via exact match)",
284
+ )
285
+ )
286
+ return docs # type: ignore
@@ -0,0 +1,190 @@
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 Column, Model, Schema
17
+
18
+
19
+ class Update(Get):
20
+ """
21
+ An endpoint to update a record.
22
+
23
+ This endpoint handles update operations. As with the `Get` endpoint, it will lookup the record by taking
24
+ the record id (or any other unique column you specify) out of the URL and then will fetch that record
25
+ using the model class. Then, it will use the model and list of writeable column names to validate the
26
+ incoming user input. The default request method is `PATCH`. If everything checks out, it will then
27
+ update the record.
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(validators=[clearskies.validators.Required()])
39
+
40
+
41
+ wsgi = clearskies.contexts.WsgiRef(
42
+ clearskies.endpoints.Update(
43
+ model_class=User,
44
+ url="/{id}",
45
+ readable_column_names=["id", "name", "username"],
46
+ writeable_column_names=["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' -X PATCH -d '{"name": "Bobby Brown", "username": "bobbybrown"}' | jq
68
+ {
69
+ "status": "success",
70
+ "error": "",
71
+ "data": {
72
+ "id": "1-2-3-4",
73
+ "name": "Bobby Brown",
74
+ "username": "bobbybrown"
75
+ },
76
+ "pagination": {},
77
+ "input_errors": {}
78
+ }
79
+
80
+ $ curl 'http://localhost:8080/1-2-3-5' -X PATCH -d '{"name": 12345, "username": ""}' | jq
81
+ {
82
+ "status": "input_errors",
83
+ "error": "",
84
+ "data": [],
85
+ "pagination": {},
86
+ "input_errors": {
87
+ "name": "value should be a string",
88
+ "username": "'username' is required."
89
+ }
90
+ }
91
+ """
92
+
93
+ @clearskies.parameters_to_properties.parameters_to_properties
94
+ def __init__(
95
+ self,
96
+ model_class: type[Model],
97
+ url: str,
98
+ writeable_column_names: list[str],
99
+ readable_column_names: list[str],
100
+ record_lookup_column_name: str | None = None,
101
+ input_validation_callable: Callable | None = None,
102
+ request_methods: list[str] = ["PATCH"],
103
+ response_headers: list[str | Callable[..., list[str]]] = [],
104
+ output_map: Callable[..., dict[str, Any]] | None = None,
105
+ output_schema: Schema | None = None,
106
+ column_overrides: dict[str, Column] = {},
107
+ internal_casing: str = "snake_case",
108
+ external_casing: str = "snake_case",
109
+ security_headers: list[SecurityHeader] = [],
110
+ description: str = "",
111
+ where: typing.condition | list[typing.condition] = [],
112
+ joins: typing.join | list[typing.join] = [],
113
+ authentication: authentication.Authentication = authentication.Public(),
114
+ authorization: authentication.Authorization = authentication.Authorization(),
115
+ ):
116
+ # see comment in clearskies.endpoints.Create.__init__
117
+ self.request_methods = request_methods
118
+
119
+ # 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
120
+ # just stores them in parameters, which we have already done. However, the parent does do some extra initialization stuff that we need,
121
+ # which is why we have to call the parent.
122
+ super().__init__(model_class, url, readable_column_names)
123
+
124
+ def handle(self, input_output: InputOutput) -> Any:
125
+ request_data = self.get_request_data(input_output)
126
+ if not request_data and input_output.has_body():
127
+ raise clearskies.exceptions.ClientError("Request body was not valid JSON")
128
+ model = self.fetch_model(input_output)
129
+ self.validate_input_against_schema(request_data, input_output, model)
130
+ model.save(request_data)
131
+ return self.success(input_output, self.model_as_json(model, input_output))
132
+
133
+ def documentation(self) -> list[autodoc.request.Request]:
134
+ output_schema = self.model_class
135
+ nice_model = string.camel_case_to_words(output_schema.__name__)
136
+
137
+ schema_model_name = string.camel_case_to_snake_case(output_schema.__name__)
138
+ output_data_schema = self.documentation_data_schema(output_schema, self.readable_column_names)
139
+ output_autodoc = (
140
+ autodoc.schema.Object(
141
+ self.auto_case_internal_column_name("data"), children=output_data_schema, model_name=schema_model_name
142
+ ),
143
+ )
144
+
145
+ authentication = self.authentication
146
+ standard_error_responses = [self.documentation_input_error_response()]
147
+ if not getattr(authentication, "is_public", False):
148
+ standard_error_responses.append(self.documentation_access_denied_response())
149
+ if getattr(authentication, "can_authorize", False):
150
+ standard_error_responses.append(self.documentation_unauthorized_response())
151
+
152
+ return [
153
+ autodoc.request.Request(
154
+ self.description,
155
+ [
156
+ self.documentation_success_response(
157
+ output_autodoc, # type: ignore
158
+ description=self.description,
159
+ ),
160
+ *standard_error_responses,
161
+ self.documentation_generic_error_response(),
162
+ ],
163
+ relative_path=self.url,
164
+ request_methods=self.request_methods,
165
+ parameters=[
166
+ *self.documentation_request_parameters(),
167
+ *self.standard_url_parameters(),
168
+ ],
169
+ root_properties={
170
+ "security": self.documentation_request_security(),
171
+ },
172
+ ),
173
+ ]
174
+
175
+ def documentation_request_parameters(self) -> list[autodoc.request.Parameter]:
176
+ return [
177
+ *self.standard_json_request_parameters(self.model_class),
178
+ *self.standard_url_request_parameters(),
179
+ ]
180
+
181
+ def documentation_models(self) -> dict[str, autodoc.schema.Schema]:
182
+ output_schema = self.output_schema if self.output_schema else self.model_class
183
+ schema_model_name = string.camel_case_to_snake_case(output_schema.__name__)
184
+
185
+ return {
186
+ schema_model_name: autodoc.schema.Object(
187
+ self.auto_case_internal_column_name("data"),
188
+ children=self.documentation_data_schema(output_schema, self.readable_column_names),
189
+ ),
190
+ }
clearskies/environment.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import os.path
2
+ from typing import Any
2
3
 
3
4
 
4
5
  class Environment:
@@ -14,8 +15,9 @@ class Environment:
14
15
  is assumed to be a string.
15
16
  """
16
17
 
17
- _env_file_config = None
18
- _resolved_values = None
18
+ _env_file_config: dict[str, Any] = None # type: ignore
19
+ _resolved_values: dict[str, Any] = {}
20
+ os_environ: dict[str, Any] = {}
19
21
 
20
22
  def __init__(self, env_file_path, os_environ, secrets):
21
23
  self._env_file_path = env_file_path
@@ -23,7 +25,7 @@ class Environment:
23
25
  self.secrets = secrets
24
26
  self._resolved_values = {}
25
27
 
26
- def get(self, name, silent=False):
28
+ def get(self, name, silent=False) -> Any:
27
29
  self._load_env_file()
28
30
  if name in self.os_environ:
29
31
  return self.resolve_value(self.os_environ[name])
@@ -0,0 +1,17 @@
1
+ from clearskies.exceptions.authentication import Authentication
2
+ from clearskies.exceptions.authorization import Authorization
3
+ from clearskies.exceptions.client_error import ClientError
4
+ from clearskies.exceptions.input_errors import InputErrors
5
+ from clearskies.exceptions.moved_permanently import MovedPermanently
6
+ from clearskies.exceptions.moved_temporarily import MovedTemporarily
7
+ from clearskies.exceptions.not_found import NotFound
8
+
9
+ __all__ = [
10
+ "Authentication",
11
+ "Authorization",
12
+ "ClientError",
13
+ "InputErrors",
14
+ "MovedPermanently",
15
+ "MovedTemporarily",
16
+ "NotFound",
17
+ ]
@@ -1,4 +1,4 @@
1
- class InputError(Exception):
1
+ class InputErrors(Exception):
2
2
  def __init__(self, errors):
3
3
  super().__init__(self, "Input Error")
4
4
  self.errors = errors
@@ -0,0 +1,3 @@
1
+ class MovedPermanently(Exception):
2
+ def __init__(self, location):
3
+ super().__init__(self, location)
@@ -0,0 +1,3 @@
1
+ class MovedTemporarily(Exception):
2
+ def __init__(self, location):
3
+ super().__init__(self, location)
@@ -0,0 +1,2 @@
1
+ class NotFound(Exception):
2
+ pass
@@ -1,7 +1,7 @@
1
- from . import string
2
- from . import validations
1
+ from . import routing, string, validations
3
2
 
4
3
  __all__ = [
4
+ "routing",
5
5
  "string",
6
6
  "validations",
7
7
  ]
@@ -0,0 +1,92 @@
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