clear-skies 1.19.22__py3-none-any.whl → 2.0.23__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.
Files changed (362) hide show
  1. clear_skies-2.0.23.dist-info/METADATA +76 -0
  2. clear_skies-2.0.23.dist-info/RECORD +265 -0
  3. {clear_skies-1.19.22.dist-info → clear_skies-2.0.23.dist-info}/WHEEL +1 -1
  4. clearskies/__init__.py +37 -21
  5. clearskies/action.py +7 -0
  6. clearskies/authentication/__init__.py +9 -38
  7. clearskies/authentication/authentication.py +44 -0
  8. clearskies/authentication/authorization.py +14 -8
  9. clearskies/authentication/authorization_pass_through.py +22 -0
  10. clearskies/authentication/jwks.py +135 -58
  11. clearskies/authentication/public.py +3 -26
  12. clearskies/authentication/secret_bearer.py +515 -44
  13. clearskies/autodoc/formats/oai3_json/__init__.py +2 -2
  14. clearskies/autodoc/formats/oai3_json/oai3_json.py +11 -9
  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 +10 -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 +16 -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 +56 -17
  41. clearskies/backends/api_backend.py +1128 -166
  42. clearskies/backends/backend.py +54 -85
  43. clearskies/backends/cursor_backend.py +246 -191
  44. clearskies/backends/memory_backend.py +514 -208
  45. clearskies/backends/secrets_backend.py +68 -31
  46. clearskies/column.py +1221 -0
  47. clearskies/columns/__init__.py +71 -0
  48. clearskies/columns/audit.py +306 -0
  49. clearskies/columns/belongs_to_id.py +478 -0
  50. clearskies/columns/belongs_to_model.py +129 -0
  51. clearskies/columns/belongs_to_self.py +109 -0
  52. clearskies/columns/boolean.py +110 -0
  53. clearskies/columns/category_tree.py +273 -0
  54. clearskies/columns/category_tree_ancestors.py +51 -0
  55. clearskies/columns/category_tree_children.py +126 -0
  56. clearskies/columns/category_tree_descendants.py +48 -0
  57. clearskies/columns/created.py +92 -0
  58. clearskies/columns/created_by_authorization_data.py +114 -0
  59. clearskies/columns/created_by_header.py +103 -0
  60. clearskies/columns/created_by_ip.py +90 -0
  61. clearskies/columns/created_by_routing_data.py +102 -0
  62. clearskies/columns/created_by_user_agent.py +89 -0
  63. clearskies/columns/date.py +232 -0
  64. clearskies/columns/datetime.py +284 -0
  65. clearskies/columns/email.py +78 -0
  66. clearskies/columns/float.py +149 -0
  67. clearskies/columns/has_many.py +529 -0
  68. clearskies/columns/has_many_self.py +62 -0
  69. clearskies/columns/has_one.py +21 -0
  70. clearskies/columns/integer.py +158 -0
  71. clearskies/columns/json.py +126 -0
  72. clearskies/columns/many_to_many_ids.py +335 -0
  73. clearskies/columns/many_to_many_ids_with_data.py +274 -0
  74. clearskies/columns/many_to_many_models.py +156 -0
  75. clearskies/columns/many_to_many_pivots.py +132 -0
  76. clearskies/columns/phone.py +162 -0
  77. clearskies/columns/select.py +95 -0
  78. clearskies/columns/string.py +102 -0
  79. clearskies/columns/timestamp.py +164 -0
  80. clearskies/columns/updated.py +107 -0
  81. clearskies/columns/uuid.py +83 -0
  82. clearskies/configs/README.md +105 -0
  83. clearskies/configs/__init__.py +170 -0
  84. clearskies/configs/actions.py +43 -0
  85. clearskies/configs/any.py +15 -0
  86. clearskies/configs/any_dict.py +24 -0
  87. clearskies/configs/any_dict_or_callable.py +25 -0
  88. clearskies/configs/authentication.py +23 -0
  89. clearskies/configs/authorization.py +23 -0
  90. clearskies/configs/boolean.py +18 -0
  91. clearskies/configs/boolean_or_callable.py +20 -0
  92. clearskies/configs/callable_config.py +20 -0
  93. clearskies/configs/columns.py +34 -0
  94. clearskies/configs/conditions.py +30 -0
  95. clearskies/configs/config.py +26 -0
  96. clearskies/configs/datetime.py +20 -0
  97. clearskies/configs/datetime_or_callable.py +21 -0
  98. clearskies/configs/email.py +10 -0
  99. clearskies/configs/email_list.py +17 -0
  100. clearskies/configs/email_list_or_callable.py +17 -0
  101. clearskies/configs/email_or_email_list_or_callable.py +59 -0
  102. clearskies/configs/endpoint.py +23 -0
  103. clearskies/configs/endpoint_list.py +29 -0
  104. clearskies/configs/float.py +18 -0
  105. clearskies/configs/float_or_callable.py +20 -0
  106. clearskies/configs/headers.py +28 -0
  107. clearskies/configs/integer.py +18 -0
  108. clearskies/configs/integer_or_callable.py +20 -0
  109. clearskies/configs/joins.py +30 -0
  110. clearskies/configs/list_any_dict.py +32 -0
  111. clearskies/configs/list_any_dict_or_callable.py +33 -0
  112. clearskies/configs/model_class.py +35 -0
  113. clearskies/configs/model_column.py +67 -0
  114. clearskies/configs/model_columns.py +58 -0
  115. clearskies/configs/model_destination_name.py +26 -0
  116. clearskies/configs/model_to_id_column.py +45 -0
  117. clearskies/configs/readable_model_column.py +11 -0
  118. clearskies/configs/readable_model_columns.py +11 -0
  119. clearskies/configs/schema.py +23 -0
  120. clearskies/configs/searchable_model_columns.py +11 -0
  121. clearskies/configs/security_headers.py +39 -0
  122. clearskies/configs/select.py +28 -0
  123. clearskies/configs/select_list.py +49 -0
  124. clearskies/configs/string.py +31 -0
  125. clearskies/configs/string_dict.py +34 -0
  126. clearskies/configs/string_list.py +47 -0
  127. clearskies/configs/string_list_or_callable.py +48 -0
  128. clearskies/configs/string_or_callable.py +18 -0
  129. clearskies/configs/timedelta.py +20 -0
  130. clearskies/configs/timezone.py +20 -0
  131. clearskies/configs/url.py +25 -0
  132. clearskies/configs/validators.py +45 -0
  133. clearskies/configs/writeable_model_column.py +11 -0
  134. clearskies/configs/writeable_model_columns.py +11 -0
  135. clearskies/configurable.py +78 -0
  136. clearskies/contexts/__init__.py +8 -8
  137. clearskies/contexts/cli.py +129 -43
  138. clearskies/contexts/context.py +93 -56
  139. clearskies/contexts/wsgi.py +79 -33
  140. clearskies/contexts/wsgi_ref.py +87 -0
  141. clearskies/cursors/__init__.py +7 -0
  142. clearskies/cursors/cursor.py +166 -0
  143. clearskies/cursors/from_environment/__init__.py +5 -0
  144. clearskies/cursors/from_environment/mysql.py +51 -0
  145. clearskies/cursors/from_environment/postgresql.py +49 -0
  146. clearskies/cursors/from_environment/sqlite.py +35 -0
  147. clearskies/cursors/mysql.py +61 -0
  148. clearskies/cursors/postgresql.py +61 -0
  149. clearskies/cursors/sqlite.py +62 -0
  150. clearskies/decorators.py +33 -0
  151. clearskies/decorators.pyi +10 -0
  152. clearskies/di/__init__.py +11 -7
  153. clearskies/di/additional_config.py +117 -3
  154. clearskies/di/additional_config_auto_import.py +12 -0
  155. clearskies/di/di.py +717 -126
  156. clearskies/di/inject/__init__.py +23 -0
  157. clearskies/di/inject/akeyless_sdk.py +16 -0
  158. clearskies/di/inject/by_class.py +24 -0
  159. clearskies/di/inject/by_name.py +22 -0
  160. clearskies/di/inject/di.py +16 -0
  161. clearskies/di/inject/environment.py +15 -0
  162. clearskies/di/inject/input_output.py +19 -0
  163. clearskies/di/inject/now.py +16 -0
  164. clearskies/di/inject/requests.py +16 -0
  165. clearskies/di/inject/secrets.py +15 -0
  166. clearskies/di/inject/utcnow.py +16 -0
  167. clearskies/di/inject/uuid.py +16 -0
  168. clearskies/di/injectable.py +32 -0
  169. clearskies/di/injectable_properties.py +131 -0
  170. clearskies/end.py +219 -0
  171. clearskies/endpoint.py +1303 -0
  172. clearskies/endpoint_group.py +333 -0
  173. clearskies/endpoints/__init__.py +25 -0
  174. clearskies/endpoints/advanced_search.py +519 -0
  175. clearskies/endpoints/callable.py +382 -0
  176. clearskies/endpoints/create.py +201 -0
  177. clearskies/endpoints/delete.py +133 -0
  178. clearskies/endpoints/get.py +267 -0
  179. clearskies/endpoints/health_check.py +181 -0
  180. clearskies/endpoints/list.py +567 -0
  181. clearskies/endpoints/restful_api.py +417 -0
  182. clearskies/endpoints/schema.py +185 -0
  183. clearskies/endpoints/simple_search.py +279 -0
  184. clearskies/endpoints/update.py +188 -0
  185. clearskies/environment.py +7 -3
  186. clearskies/exceptions/__init__.py +19 -0
  187. clearskies/{handlers/exceptions/input_error.py → exceptions/input_errors.py} +1 -1
  188. clearskies/exceptions/missing_dependency.py +2 -0
  189. clearskies/exceptions/moved_permanently.py +3 -0
  190. clearskies/exceptions/moved_temporarily.py +3 -0
  191. clearskies/functional/__init__.py +2 -2
  192. clearskies/functional/json.py +47 -0
  193. clearskies/functional/routing.py +92 -0
  194. clearskies/functional/string.py +19 -11
  195. clearskies/functional/validations.py +61 -9
  196. clearskies/input_outputs/__init__.py +9 -7
  197. clearskies/input_outputs/cli.py +135 -152
  198. clearskies/input_outputs/exceptions/__init__.py +6 -1
  199. clearskies/input_outputs/headers.py +54 -0
  200. clearskies/input_outputs/input_output.py +77 -123
  201. clearskies/input_outputs/programmatic.py +62 -0
  202. clearskies/input_outputs/wsgi.py +36 -48
  203. clearskies/model.py +1894 -199
  204. clearskies/query/__init__.py +12 -0
  205. clearskies/query/condition.py +228 -0
  206. clearskies/query/join.py +136 -0
  207. clearskies/query/query.py +193 -0
  208. clearskies/query/sort.py +27 -0
  209. clearskies/schema.py +82 -0
  210. clearskies/secrets/__init__.py +4 -31
  211. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer.py +15 -4
  212. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +11 -5
  213. clearskies/secrets/akeyless.py +421 -155
  214. clearskies/secrets/exceptions/__init__.py +7 -1
  215. clearskies/secrets/exceptions/not_found_error.py +2 -0
  216. clearskies/secrets/exceptions/permissions_error.py +2 -0
  217. clearskies/secrets/secrets.py +12 -11
  218. clearskies/security_header.py +17 -0
  219. clearskies/security_headers/__init__.py +8 -8
  220. clearskies/security_headers/cache_control.py +47 -109
  221. clearskies/security_headers/cors.py +38 -92
  222. clearskies/security_headers/csp.py +76 -150
  223. clearskies/security_headers/hsts.py +14 -15
  224. clearskies/typing.py +11 -0
  225. clearskies/validator.py +36 -0
  226. clearskies/validators/__init__.py +33 -0
  227. clearskies/validators/after_column.py +61 -0
  228. clearskies/validators/before_column.py +15 -0
  229. clearskies/validators/in_the_future.py +29 -0
  230. clearskies/validators/in_the_future_at_least.py +13 -0
  231. clearskies/validators/in_the_future_at_most.py +12 -0
  232. clearskies/validators/in_the_past.py +29 -0
  233. clearskies/validators/in_the_past_at_least.py +12 -0
  234. clearskies/validators/in_the_past_at_most.py +12 -0
  235. clearskies/validators/maximum_length.py +25 -0
  236. clearskies/validators/maximum_value.py +28 -0
  237. clearskies/validators/minimum_length.py +25 -0
  238. clearskies/validators/minimum_value.py +28 -0
  239. clearskies/{input_requirements → validators}/required.py +18 -9
  240. clearskies/validators/timedelta.py +58 -0
  241. clearskies/validators/unique.py +28 -0
  242. clear_skies-1.19.22.dist-info/METADATA +0 -46
  243. clear_skies-1.19.22.dist-info/RECORD +0 -206
  244. clearskies/application.py +0 -29
  245. clearskies/authentication/auth0_jwks.py +0 -118
  246. clearskies/authentication/auth_exception.py +0 -2
  247. clearskies/authentication/jwks_jwcrypto.py +0 -39
  248. clearskies/backends/example_backend.py +0 -43
  249. clearskies/backends/file_backend.py +0 -48
  250. clearskies/backends/json_backend.py +0 -7
  251. clearskies/backends/restful_api_advanced_search_backend.py +0 -138
  252. clearskies/binding_config.py +0 -16
  253. clearskies/column_types/__init__.py +0 -184
  254. clearskies/column_types/audit.py +0 -235
  255. clearskies/column_types/belongs_to.py +0 -250
  256. clearskies/column_types/boolean.py +0 -60
  257. clearskies/column_types/category_tree.py +0 -226
  258. clearskies/column_types/column.py +0 -373
  259. clearskies/column_types/created.py +0 -26
  260. clearskies/column_types/created_by_authorization_data.py +0 -26
  261. clearskies/column_types/created_by_header.py +0 -24
  262. clearskies/column_types/created_by_ip.py +0 -17
  263. clearskies/column_types/created_by_routing_data.py +0 -25
  264. clearskies/column_types/created_by_user_agent.py +0 -17
  265. clearskies/column_types/created_micro.py +0 -26
  266. clearskies/column_types/datetime.py +0 -108
  267. clearskies/column_types/datetime_micro.py +0 -12
  268. clearskies/column_types/email.py +0 -18
  269. clearskies/column_types/float.py +0 -43
  270. clearskies/column_types/has_many.py +0 -139
  271. clearskies/column_types/integer.py +0 -41
  272. clearskies/column_types/json.py +0 -25
  273. clearskies/column_types/many_to_many.py +0 -278
  274. clearskies/column_types/many_to_many_with_data.py +0 -162
  275. clearskies/column_types/select.py +0 -11
  276. clearskies/column_types/string.py +0 -24
  277. clearskies/column_types/updated.py +0 -24
  278. clearskies/column_types/updated_micro.py +0 -24
  279. clearskies/column_types/uuid.py +0 -25
  280. clearskies/columns.py +0 -123
  281. clearskies/condition_parser.py +0 -172
  282. clearskies/contexts/build_context.py +0 -54
  283. clearskies/contexts/convert_to_application.py +0 -190
  284. clearskies/contexts/extract_handler.py +0 -37
  285. clearskies/contexts/test.py +0 -94
  286. clearskies/decorators/__init__.py +0 -39
  287. clearskies/decorators/auth0_jwks.py +0 -22
  288. clearskies/decorators/authorization.py +0 -10
  289. clearskies/decorators/binding_classes.py +0 -9
  290. clearskies/decorators/binding_modules.py +0 -9
  291. clearskies/decorators/bindings.py +0 -9
  292. clearskies/decorators/create.py +0 -10
  293. clearskies/decorators/delete.py +0 -10
  294. clearskies/decorators/docs.py +0 -14
  295. clearskies/decorators/get.py +0 -10
  296. clearskies/decorators/jwks.py +0 -26
  297. clearskies/decorators/merge.py +0 -124
  298. clearskies/decorators/patch.py +0 -10
  299. clearskies/decorators/post.py +0 -10
  300. clearskies/decorators/public.py +0 -11
  301. clearskies/decorators/response_headers.py +0 -10
  302. clearskies/decorators/return_raw_response.py +0 -9
  303. clearskies/decorators/schema.py +0 -10
  304. clearskies/decorators/secret_bearer.py +0 -24
  305. clearskies/decorators/security_headers.py +0 -10
  306. clearskies/di/standard_dependencies.py +0 -140
  307. clearskies/di/test_module/__init__.py +0 -6
  308. clearskies/di/test_module/another_module/__init__.py +0 -2
  309. clearskies/di/test_module/module_class.py +0 -5
  310. clearskies/handlers/__init__.py +0 -41
  311. clearskies/handlers/advanced_search.py +0 -271
  312. clearskies/handlers/base.py +0 -473
  313. clearskies/handlers/callable.py +0 -189
  314. clearskies/handlers/create.py +0 -35
  315. clearskies/handlers/crud_by_method.py +0 -18
  316. clearskies/handlers/database_connector.py +0 -32
  317. clearskies/handlers/delete.py +0 -61
  318. clearskies/handlers/exceptions/__init__.py +0 -5
  319. clearskies/handlers/exceptions/not_found.py +0 -3
  320. clearskies/handlers/get.py +0 -156
  321. clearskies/handlers/health_check.py +0 -59
  322. clearskies/handlers/input_processing.py +0 -79
  323. clearskies/handlers/list.py +0 -530
  324. clearskies/handlers/mygrations.py +0 -82
  325. clearskies/handlers/request_method_routing.py +0 -47
  326. clearskies/handlers/restful_api.py +0 -218
  327. clearskies/handlers/routing.py +0 -62
  328. clearskies/handlers/schema_helper.py +0 -128
  329. clearskies/handlers/simple_routing.py +0 -204
  330. clearskies/handlers/simple_routing_route.py +0 -192
  331. clearskies/handlers/simple_search.py +0 -136
  332. clearskies/handlers/update.py +0 -96
  333. clearskies/handlers/write.py +0 -193
  334. clearskies/input_requirements/__init__.py +0 -68
  335. clearskies/input_requirements/after.py +0 -36
  336. clearskies/input_requirements/before.py +0 -36
  337. clearskies/input_requirements/in_the_future_at_least.py +0 -19
  338. clearskies/input_requirements/in_the_future_at_most.py +0 -19
  339. clearskies/input_requirements/in_the_past_at_least.py +0 -19
  340. clearskies/input_requirements/in_the_past_at_most.py +0 -19
  341. clearskies/input_requirements/maximum_length.py +0 -19
  342. clearskies/input_requirements/minimum_length.py +0 -22
  343. clearskies/input_requirements/requirement.py +0 -25
  344. clearskies/input_requirements/time_delta.py +0 -38
  345. clearskies/input_requirements/unique.py +0 -18
  346. clearskies/mocks/__init__.py +0 -7
  347. clearskies/mocks/input_output.py +0 -124
  348. clearskies/mocks/models.py +0 -142
  349. clearskies/models.py +0 -345
  350. clearskies/security_headers/base.py +0 -12
  351. clearskies/tests/simple_api/models/__init__.py +0 -2
  352. clearskies/tests/simple_api/models/status.py +0 -23
  353. clearskies/tests/simple_api/models/user.py +0 -21
  354. clearskies/tests/simple_api/users_api.py +0 -64
  355. {clear_skies-1.19.22.dist-info → clear_skies-2.0.23.dist-info/licenses}/LICENSE +0 -0
  356. /clearskies/{contexts/bash.py → autodoc/py.typed} +0 -0
  357. /clearskies/{handlers/exceptions → exceptions}/authentication.py +0 -0
  358. /clearskies/{handlers/exceptions → exceptions}/authorization.py +0 -0
  359. /clearskies/{handlers/exceptions → exceptions}/client_error.py +0 -0
  360. /clearskies/{secrets/exceptions → exceptions}/not_found.py +0 -0
  361. /clearskies/{tests/__init__.py → input_outputs/py.typed} +0 -0
  362. /clearskies/{tests/simple_api/__init__.py → py.typed} +0 -0
@@ -0,0 +1,567 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, Callable
4
+
5
+ from clearskies import authentication, autodoc, configs, decorators, exceptions
6
+ from clearskies.endpoint import Endpoint
7
+ from clearskies.functional import string
8
+
9
+ if TYPE_CHECKING:
10
+ from clearskies import Column, Model, Schema, SecurityHeader, typing
11
+ from clearskies.input_outputs import InputOutput
12
+
13
+
14
+ class List(Endpoint):
15
+ """
16
+ Create a list endpoint that fetches and returns records to the end client.
17
+
18
+ A list endpoint has four required parameters:
19
+
20
+ | Name | Value |
21
+ |----------------------------|---------------------------------------------------------------------------------------|
22
+ | `model_class` | The model class for the endpoint to use to find and return records. |
23
+ | `readable_column_names` | A list of columns from the model class that the endpoint should return to the client. |
24
+ | `sortable_column_names` | A list of columns that the client is allowed to sort by. |
25
+ | `default_sort_column_name` | The default column to sort by. |
26
+
27
+ Here's a basic working 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
+
39
+
40
+ list_users = clearskies.endpoints.List(
41
+ model_class=User,
42
+ readable_column_names=["id", "name"],
43
+ sortable_column_names=["id", "name"],
44
+ default_sort_column_name="name",
45
+ )
46
+
47
+ wsgi = clearskies.contexts.WsgiRef(
48
+ list_users,
49
+ classes=[User],
50
+ bindings={
51
+ "memory_backend_default_data": [
52
+ {
53
+ "model_class": User,
54
+ "records": [
55
+ {"id": "1-2-3-4", "name": "Bob"},
56
+ {"id": "1-2-3-5", "name": "Jane"},
57
+ {"id": "1-2-3-6", "name": "Greg"},
58
+ ],
59
+ },
60
+ ]
61
+ },
62
+ )
63
+ wsgi()
64
+ ```
65
+
66
+ You can then fetch your records:
67
+
68
+ ```bash
69
+ $ curl 'http://localhost:8080/' | jq
70
+ {
71
+ "status": "success",
72
+ "error": "",
73
+ "data": [
74
+ {"id": "1-2-3-4", "name": "Bob"},
75
+ {"id": "1-2-3-6", "name": "Greg"},
76
+ {"id": "1-2-3-5", "name": "Jane"},
77
+ ],
78
+ "pagination": {
79
+ "number_results": 3,
80
+ "limit": 50,
81
+ "next_page": {}
82
+ },
83
+ "input_errors": {}
84
+ }
85
+ ```
86
+
87
+ Pagination can be set via query parameters or the JSON body:
88
+
89
+ ```bash
90
+ $ curl 'http://localhost:8080/?sort=name&direction=desc&limit=2' | jq
91
+ {
92
+ "status": "success",
93
+ "error": "",
94
+ "data": [
95
+ {"id": "1-2-3-5", "name": "Jane"},
96
+ {"id": "1-2-3-6", "name": "Greg"},
97
+ ],
98
+ "pagination": {
99
+ "number_results": 3,
100
+ "limit": 2,
101
+ "next_page": {"start": 2}
102
+ },
103
+ "input_errors": {}
104
+ }
105
+ ```
106
+
107
+ In the response, '.pagination.next_page` is a dictionary that returns the query parameters to set in order to fetch the next page of results.
108
+ Note that the pagination method depends on the backend. The memory backend supports pagination via start/limit, while other backends may
109
+ support alternate pagination schemes. Clearskies automatically handles the difference, so it's important to use `.pagination.next_page` to fetch
110
+ the next page of results.
111
+
112
+ Use `where`, `joins`, and `group_by` to automatically adjust the query used by the list endpoint. In particular, where is a list of either
113
+ conditions (as a string) or a callable that can modify the query directly via the model class. For example:
114
+
115
+ ```python
116
+ list_users = clearskies.endpoints.List(
117
+ model_class=User,
118
+ readable_column_names=["id", "name"],
119
+ sortable_column_names=["id", "name"],
120
+ default_sort_column_name="name",
121
+ where=[User.name.equals("Jane")], # equivalent: where=["name=Jane"]
122
+ )
123
+ ```
124
+
125
+ With the above definition, the list endpoint will only ever return records with a name of "Jane". The following uses standard dependency
126
+ injection rules to execute a similar filter based on arbitrary logic required:
127
+
128
+ ```python
129
+ import datetime
130
+
131
+ list_users = clearskies.endpoints.List(
132
+ model_class=User,
133
+ readable_column_names=["id", "name"],
134
+ sortable_column_names=["id", "name"],
135
+ default_sort_column_name="name",
136
+ where=[
137
+ lambda model, now: model.where("name=Jane")
138
+ if now > datetime.datetime(2025, 1, 1)
139
+ else model
140
+ ],
141
+ )
142
+ ```
143
+
144
+ As shown in the above example, a function called in this way can request additional dependencies as needed, per the standard dependency rules.
145
+ The function needs to return the adjusted model object, which is usually as simple as returning the result of `model.where(?)`. While the
146
+ above example uses a lambda function, of course you can attach any other kind of callable - a function, a method of a class, etc...
147
+ """
148
+
149
+ """
150
+ The default column to sort by.
151
+ """
152
+ default_sort_column_name = configs.ModelColumn("model_class")
153
+
154
+ """
155
+ The default sort direction (ASC or DESC).
156
+ """
157
+ default_sort_direction = configs.Select(["ASC", "DESC"], default="ASC")
158
+
159
+ """
160
+ The number of records returned if the client doesn't specify a different number of records (default: 50).
161
+ """
162
+ default_limit = configs.Integer(default=50)
163
+
164
+ """
165
+ The maximum number of records the client is allowed to request (0 == no limit)
166
+ """
167
+ maximum_limit = configs.Integer(default=200)
168
+
169
+ """
170
+ A column to group by.
171
+ """
172
+ group_by_column_name = configs.ModelColumn("model_class")
173
+
174
+ readable_column_names = configs.ReadableModelColumns("model_class")
175
+ sortable_column_names = configs.ReadableModelColumns("model_class", allow_relationship_references=True)
176
+ searchable_column_names = configs.SearchableModelColumns("model_class", allow_relationship_references=True)
177
+
178
+ @decorators.parameters_to_properties
179
+ def __init__(
180
+ self,
181
+ model_class: type[Model],
182
+ readable_column_names: list[str],
183
+ sortable_column_names: list[str],
184
+ default_sort_column_name: str | None,
185
+ default_sort_direction: str = "ASC",
186
+ default_limit: int = 50,
187
+ maximum_limit: int = 200,
188
+ where: typing.condition | list[typing.condition] = [],
189
+ joins: typing.join | list[typing.join] = [],
190
+ url: str = "",
191
+ request_methods: list[str] = ["GET"],
192
+ response_headers: list[str | Callable[..., list[str]]] = [],
193
+ output_map: Callable[..., dict[str, Any]] | None = None,
194
+ output_schema: Schema | None = None,
195
+ column_overrides: dict[str, Column] = {},
196
+ group_by_column_name: str = "",
197
+ internal_casing: str = "snake_case",
198
+ external_casing: str = "snake_case",
199
+ security_headers: list[SecurityHeader] = [],
200
+ description: str = "",
201
+ authentication: authentication.Authentication = authentication.Public(),
202
+ authorization: authentication.Authorization = authentication.Authorization(),
203
+ ):
204
+ # 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
205
+ # just stores them in parameters, which we have already done. However, the parent does do some extra initialization stuff that we need,
206
+ # which is why we have to call the parent.
207
+ super().__init__()
208
+
209
+ @property
210
+ def searchable_columns(self) -> dict[str, Column]:
211
+ if self._searchable_columns is None:
212
+ self._searchable_columns = {name: self.columns[name] for name in self.searchable_column_names}
213
+ return self._searchable_columns
214
+
215
+ @property
216
+ def sortable_columns(self) -> dict[str, Column]:
217
+ if self._sortable_columns is None:
218
+ self._sortable_columns = {name: self.columns[name] for name in self.sortable_column_names}
219
+ return self._sortable_columns
220
+
221
+ @property
222
+ def allowed_request_keys(self) -> list[str]:
223
+ return [*["sort", "direction", "limit"], *self.searchable_column_names]
224
+
225
+ @property
226
+ def internal_request_keys(self) -> list[str]:
227
+ return ["sort", "direction", "limit"]
228
+
229
+ def handle(self, input_output: InputOutput):
230
+ model = self.fetch_model_with_base_query(input_output)
231
+ if not input_output.request_data and input_output.has_body():
232
+ raise exceptions.ClientError("Request body was not valid JSON")
233
+ if input_output.request_data and not isinstance(input_output.request_data, dict):
234
+ raise exceptions.ClientError("When present, request body must be a JSON dictionary")
235
+ request_data = self.map_input_to_internal_names(input_output.request_data) # type: ignore
236
+ query_parameters = self.map_input_to_internal_names(input_output.query_parameters)
237
+ pagination_data = {}
238
+ for key in model.allowed_pagination_keys():
239
+ if key in request_data and key in query_parameters:
240
+ original_name = self.auto_case_internal_column_name(key)
241
+ raise exceptions.ClientError(
242
+ f"Ambiguous request: key '{original_name}' is present in both the JSON body and URL data"
243
+ )
244
+ if key in request_data:
245
+ pagination_data[key] = request_data[key]
246
+ del request_data[key]
247
+ if key in query_parameters:
248
+ pagination_data[key] = query_parameters[key]
249
+ del query_parameters[key]
250
+ if request_data or query_parameters or pagination_data:
251
+ self.check_request_data(request_data, query_parameters, pagination_data)
252
+ model = self.configure_model_from_request_data(model, request_data, query_parameters, pagination_data)
253
+ if not model.get_query().limit:
254
+ model = model.limit(self.default_limit)
255
+ if not model.get_query().sorts and self.default_sort_column_name:
256
+ model = model.sort_by(
257
+ self.default_sort_column_name,
258
+ self.default_sort_direction,
259
+ model.destination_name(),
260
+ )
261
+ if self.group_by_column_name:
262
+ model = model.group_by(self.group_by_column_name)
263
+
264
+ return self.success(
265
+ input_output,
266
+ [self.model_as_json(record, input_output) for record in model],
267
+ number_results=len(model) if model.backend.can_count else None,
268
+ limit=model.get_query().limit,
269
+ next_page=model.next_page_data(),
270
+ )
271
+
272
+ def configure_model_from_request_data(
273
+ self,
274
+ model: Model,
275
+ request_data: dict[str, Any],
276
+ query_parameters: dict[str, Any],
277
+ pagination_data: dict[str, Any],
278
+ ) -> Model:
279
+ limit = int(self.from_either(request_data, query_parameters, "limit", default=self.default_limit))
280
+ model = model.limit(limit)
281
+ if pagination_data:
282
+ model = model.pagination(**pagination_data)
283
+ sort = self.from_either(request_data, query_parameters, "sort")
284
+ direction = self.from_either(request_data, query_parameters, "direction")
285
+ if sort and direction:
286
+ model = self.add_join(sort, model)
287
+ [sort_column, sort_table] = self.resolve_references_for_query(sort)
288
+ model = model.sort_by(sort_column, direction, sort_table) # type: ignore
289
+
290
+ return model
291
+
292
+ def map_input_to_internal_names(self, data: dict[str, Any]) -> dict[str, Any]:
293
+ if not data:
294
+ return {}
295
+ internal_request_keys = [*self.internal_request_keys, *self.model.allowed_pagination_keys()]
296
+ for key in internal_request_keys:
297
+ mapped_key = self.auto_case_internal_column_name(key)
298
+ if mapped_key != key and mapped_key in data:
299
+ data[key] = data[mapped_key]
300
+ del data[mapped_key]
301
+ # any non-internal fields are assumed to be column names and need to go
302
+ # through the full mapping
303
+ for key in set(self.allowed_request_keys) - set(internal_request_keys):
304
+ mapped_key = self.auto_case_column_name(key, True)
305
+ if mapped_key != key and mapped_key in data:
306
+ data[key] = data[mapped_key]
307
+ del data[mapped_key]
308
+
309
+ # finally, if we have a sort key set then convert the value to the properly cased column name
310
+ if "sort" in data:
311
+ # we can't just take the sort value and convert it to internal casing because camel/title case
312
+ # to snake_case can be ambiguous (while snake_case to camel/title is not)
313
+ sort_column_map = {}
314
+ for internal_name in self.sortable_column_names:
315
+ external_name = self.auto_case_column_name(internal_name, True)
316
+ sort_column_map[external_name] = internal_name
317
+ # sometimes the sort may be a list of directives
318
+ if isinstance(data["sort"], list):
319
+ for index, sort_entry in enumerate(data["sort"]):
320
+ if "column" not in sort_entry:
321
+ continue
322
+ if sort_entry["column"] in sort_column_map:
323
+ sort_entry["column"] = sort_column_map[sort_entry["column"]]
324
+ else:
325
+ if data["sort"] in sort_column_map:
326
+ data["sort"] = sort_column_map[data["sort"]]
327
+
328
+ return data
329
+
330
+ def check_request_data(
331
+ self, request_data: dict[str, Any], query_parameters: dict[str, Any], pagination_data: dict[str, Any]
332
+ ) -> None:
333
+ if pagination_data:
334
+ error = self.model.validate_pagination_data(pagination_data, self.auto_case_internal_column_name)
335
+ if error:
336
+ raise exceptions.ClientError(error)
337
+ for key in request_data.keys():
338
+ if key not in self.allowed_request_keys:
339
+ raise exceptions.ClientError(f"Invalid request parameter found in request body: '{key}'")
340
+ for key in query_parameters.keys():
341
+ if key not in self.allowed_request_keys:
342
+ raise exceptions.ClientError(f"Invalid request parameter found in URL data: '{key}'")
343
+ if key in request_data:
344
+ raise exceptions.ClientError(
345
+ f"Ambiguous request: '{key}' was found in both the request body and URL data"
346
+ )
347
+ self.validate_limit(request_data, query_parameters)
348
+ sort = self.from_either(request_data, query_parameters, "sort")
349
+ direction = self.from_either(request_data, query_parameters, "direction")
350
+ if sort and type(sort) != str:
351
+ raise exceptions.ClientError("Invalid request: 'sort' should be a string")
352
+ if direction and type(direction) != str:
353
+ raise exceptions.ClientError("Invalid request: 'direction' should be a string")
354
+ if sort or direction:
355
+ if (sort and not direction) or (direction and not sort):
356
+ raise exceptions.ClientError(
357
+ "You must specify 'sort' and 'direction' together in the request - not just one of them"
358
+ )
359
+ if sort not in self.sortable_column_names:
360
+ raise exceptions.ClientError(f"Invalid request: invalid sort column")
361
+ if direction.lower() not in ["asc", "desc"]:
362
+ raise exceptions.ClientError("Invalid request: direction must be 'asc' or 'desc'")
363
+ self.check_search_in_request_data(request_data, query_parameters)
364
+
365
+ def validate_limit(self, request_data: dict[str, Any], query_parameters: dict[str, Any]) -> None:
366
+ limit = self.from_either(request_data, query_parameters, "limit")
367
+ if limit is not None and type(limit) != int and type(limit) != float and type(limit) != str:
368
+ raise exceptions.ClientError("Invalid request: 'limit' should be an integer")
369
+ if limit:
370
+ try:
371
+ limit = int(limit)
372
+ except ValueError:
373
+ raise exceptions.ClientError("Invalid request: 'limit' should be an integer")
374
+ if limit:
375
+ if limit > self.maximum_limit:
376
+ raise exceptions.ClientError(f"Invalid request: 'limit' must be at most {self.maximum_limit}")
377
+ if limit < 0:
378
+ raise exceptions.ClientError(f"Invalid request: 'limit' must be positive")
379
+
380
+ def check_search_in_request_data(self, request_data: dict[str, Any], query_parameters: dict[str, Any]):
381
+ return None
382
+
383
+ def unpack_column_name_with_relationship(self, column_name: str) -> list[str]:
384
+ if "." not in column_name:
385
+ return ["", column_name]
386
+ return column_name.split(".", 1)
387
+
388
+ def resolve_references_for_query(self, column_name: str) -> list[str | None]:
389
+ """
390
+ Take the column name and returns the name and table.
391
+
392
+ If it's just a column name, we assume the table is the table for our model class.
393
+ If it's something like `belongs_to_column.column_name`, then it will find the appropriate
394
+ table reference.
395
+ """
396
+ if not column_name:
397
+ return [None, None]
398
+ [relationship_column_name, column_name] = self.unpack_column_name_with_relationship(column_name)
399
+ if not relationship_column_name:
400
+ return [self.model.destination_name(), column_name]
401
+
402
+ return [self.columns[relationship_column_name].join_table_alias(), column_name]
403
+
404
+ def add_join(self, column_name: str, model: Model) -> Model:
405
+ """
406
+ Add a join to the query for the given column name in the case where it references a column in a belongs to.
407
+
408
+ If column_name is something like `belongs_to_column.column_name`, this will add have the belongs to column
409
+ add it's typical join condition, so that further sorting/searching can work.
410
+
411
+ If column_name is empty, or doesn't contain a period, then this does nothing.
412
+ """
413
+ if not column_name:
414
+ return model
415
+ [relationship_column_name, column_name] = self.unpack_column_name_with_relationship(column_name)
416
+ if not relationship_column_name:
417
+ return model
418
+ return self.columns[relationship_column_name].add_join(model)
419
+
420
+ def from_either(self, request_data, query_parameters, key, default=None, ignore_none=True):
421
+ """Return the key from either object. Assumes it is not present in both."""
422
+ if key in request_data:
423
+ if request_data[key] is not None or not ignore_none:
424
+ return request_data[key]
425
+ if key in query_parameters:
426
+ if query_parameters[key] is not None or not ignore_none:
427
+ return query_parameters[key]
428
+ return default
429
+
430
+ def documentation(self) -> list[autodoc.request.Request]:
431
+ nice_model = string.camel_case_to_words(self.model_class.__name__)
432
+ schema_model_name = string.camel_case_to_snake_case(self.model_class.__name__)
433
+ data_schema = self.documentation_data_schema()
434
+
435
+ authentication = self.authentication
436
+ standard_error_responses = []
437
+ if not getattr(authentication, "is_public", False):
438
+ standard_error_responses.append(self.documentation_access_denied_response())
439
+ if getattr(authentication, "can_authorize", False):
440
+ standard_error_responses.append(self.documentation_unauthorized_response())
441
+
442
+ return [
443
+ autodoc.request.Request(
444
+ f"Fetch the list of current {nice_model} records",
445
+ [
446
+ self.documentation_success_response(
447
+ autodoc.schema.Array(
448
+ self.auto_case_internal_column_name("data"),
449
+ autodoc.schema.Object(nice_model, children=data_schema, model_name=schema_model_name),
450
+ ),
451
+ description=f"The matching {nice_model} records",
452
+ include_pagination=True,
453
+ ),
454
+ *standard_error_responses,
455
+ self.documentation_generic_error_response(),
456
+ ],
457
+ relative_path=self.url,
458
+ request_methods=self.request_methods,
459
+ parameters=self.documentation_request_parameters(),
460
+ root_properties={
461
+ "security": self.documentation_request_security(),
462
+ },
463
+ ),
464
+ ]
465
+
466
+ def documentation_request_parameters(self) -> list[autodoc.request.Parameter]:
467
+ return [
468
+ *self.documentation_url_pagination_parameters(),
469
+ *self.documentation_url_sort_parameters(),
470
+ *self.documentation_url_search_parameters(),
471
+ *self.documentation_json_search_parameters(),
472
+ *self.documentation_url_parameters(),
473
+ ]
474
+
475
+ def documentation_models(self) -> dict[str, autodoc.schema.Schema]:
476
+ schema_model_name = string.camel_case_to_snake_case(self.model_class.__name__)
477
+
478
+ return {
479
+ schema_model_name: autodoc.schema.Object(
480
+ self.auto_case_internal_column_name("data"),
481
+ children=self.documentation_data_schema(),
482
+ ),
483
+ }
484
+
485
+ def documentation_url_pagination_parameters(self) -> list[autodoc.request.Parameter]:
486
+ url_parameters = [
487
+ autodoc.request.URLParameter(
488
+ autodoc.schema.Integer(self.auto_case_internal_column_name("limit")),
489
+ description="The number of records to return",
490
+ ),
491
+ ]
492
+
493
+ for parameter in self.model.documentation_pagination_parameters(self.auto_case_internal_column_name):
494
+ (schema, description) = parameter
495
+ url_parameters.append(autodoc.request.URLParameter(schema, description=description))
496
+
497
+ return url_parameters # type: ignore
498
+
499
+ def documentation_url_sort_parameters(self) -> list[autodoc.request.Parameter]:
500
+ sort_columns = [self.auto_case_column_name(internal_name, True) for internal_name in self.sortable_column_names]
501
+ directions = [self.auto_case_column_name(internal_name, True) for internal_name in ["asc", "desc"]]
502
+
503
+ return [
504
+ autodoc.request.URLParameter(
505
+ autodoc.schema.Enum(
506
+ self.auto_case_internal_column_name("sort"),
507
+ sort_columns,
508
+ autodoc.schema.String(self.auto_case_internal_column_name("sort")),
509
+ example=self.auto_case_column_name("name", True),
510
+ ),
511
+ description=f"Column to sort by",
512
+ ),
513
+ autodoc.request.URLParameter(
514
+ autodoc.schema.Enum(
515
+ self.auto_case_internal_column_name("direction"),
516
+ directions,
517
+ autodoc.schema.String(self.auto_case_internal_column_name("direction")),
518
+ example=self.auto_case_column_name("asc", True),
519
+ ),
520
+ description=f"Direction to sort",
521
+ ),
522
+ ]
523
+
524
+ def documentation_json_pagination_parameters(self) -> list[autodoc.request.Parameter]:
525
+ json_parameters = [
526
+ autodoc.request.JSONBody(
527
+ autodoc.schema.Integer(self.auto_case_internal_column_name("limit")),
528
+ description="The number of records to return",
529
+ ),
530
+ ]
531
+
532
+ for parameter in self.model.documentation_pagination_parameters(self.auto_case_internal_column_name):
533
+ (schema, description) = parameter
534
+ json_parameters.append(autodoc.request.JSONBody(schema, description=description))
535
+
536
+ return json_parameters # type: ignore
537
+
538
+ def documentation_json_sort_parameters(self) -> list[autodoc.request.Parameter]:
539
+ sort_columns = [self.auto_case_column_name(internal_name, True) for internal_name in self.sortable_column_names]
540
+ directions = [self.auto_case_column_name(internal_name, True) for internal_name in ["asc", "desc"]]
541
+
542
+ return [
543
+ autodoc.request.JSONBody(
544
+ autodoc.schema.Enum(
545
+ self.auto_case_internal_column_name("sort"),
546
+ sort_columns,
547
+ autodoc.schema.String(self.auto_case_internal_column_name("sort")),
548
+ example=self.auto_case_column_name("name", True),
549
+ ),
550
+ description=f"Column to sort by",
551
+ ),
552
+ autodoc.request.JSONBody(
553
+ autodoc.schema.Enum(
554
+ self.auto_case_internal_column_name("direction"),
555
+ directions,
556
+ autodoc.schema.String(self.auto_case_internal_column_name("direction")),
557
+ example=self.auto_case_column_name("asc", True),
558
+ ),
559
+ description=f"Direction to sort",
560
+ ),
561
+ ]
562
+
563
+ def documentation_url_search_parameters(self) -> list[autodoc.request.Parameter]:
564
+ return []
565
+
566
+ def documentation_json_search_parameters(self) -> list[autodoc.request.Parameter]:
567
+ return []