clear-skies 1.22.10__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 (368) 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.22.10.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 +8 -39
  7. clearskies/authentication/authentication.py +44 -0
  8. clearskies/authentication/authorization.py +14 -8
  9. clearskies/authentication/authorization_pass_through.py +14 -10
  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 +55 -20
  41. clearskies/backends/api_backend.py +1118 -280
  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 +115 -4
  154. clearskies/di/additional_config_auto_import.py +12 -0
  155. clearskies/di/di.py +714 -125
  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 -160
  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 +1874 -193
  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.22.10.dist-info/METADATA +0 -47
  243. clear_skies-1.22.10.dist-info/RECORD +0 -213
  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 -51
  248. clearskies/backends/api_get_only_backend.py +0 -48
  249. clearskies/backends/example_backend.py +0 -43
  250. clearskies/backends/file_backend.py +0 -48
  251. clearskies/backends/json_backend.py +0 -7
  252. clearskies/backends/restful_api_advanced_search_backend.py +0 -103
  253. clearskies/binding_config.py +0 -16
  254. clearskies/column_types/__init__.py +0 -203
  255. clearskies/column_types/audit.py +0 -249
  256. clearskies/column_types/belongs_to.py +0 -271
  257. clearskies/column_types/boolean.py +0 -60
  258. clearskies/column_types/category_tree.py +0 -304
  259. clearskies/column_types/column.py +0 -373
  260. clearskies/column_types/created.py +0 -26
  261. clearskies/column_types/created_by_authorization_data.py +0 -26
  262. clearskies/column_types/created_by_header.py +0 -24
  263. clearskies/column_types/created_by_ip.py +0 -17
  264. clearskies/column_types/created_by_routing_data.py +0 -25
  265. clearskies/column_types/created_by_user_agent.py +0 -17
  266. clearskies/column_types/created_micro.py +0 -26
  267. clearskies/column_types/datetime.py +0 -109
  268. clearskies/column_types/datetime_micro.py +0 -13
  269. clearskies/column_types/email.py +0 -18
  270. clearskies/column_types/float.py +0 -43
  271. clearskies/column_types/has_many.py +0 -179
  272. clearskies/column_types/has_one.py +0 -58
  273. clearskies/column_types/integer.py +0 -41
  274. clearskies/column_types/json.py +0 -25
  275. clearskies/column_types/many_to_many.py +0 -278
  276. clearskies/column_types/many_to_many_with_data.py +0 -162
  277. clearskies/column_types/phone.py +0 -48
  278. clearskies/column_types/select.py +0 -11
  279. clearskies/column_types/string.py +0 -24
  280. clearskies/column_types/timestamp.py +0 -73
  281. clearskies/column_types/updated.py +0 -26
  282. clearskies/column_types/updated_micro.py +0 -26
  283. clearskies/column_types/uuid.py +0 -25
  284. clearskies/columns.py +0 -123
  285. clearskies/condition_parser.py +0 -172
  286. clearskies/contexts/build_context.py +0 -54
  287. clearskies/contexts/convert_to_application.py +0 -190
  288. clearskies/contexts/extract_handler.py +0 -37
  289. clearskies/contexts/test.py +0 -94
  290. clearskies/decorators/__init__.py +0 -39
  291. clearskies/decorators/auth0_jwks.py +0 -22
  292. clearskies/decorators/authorization.py +0 -10
  293. clearskies/decorators/binding_classes.py +0 -9
  294. clearskies/decorators/binding_modules.py +0 -9
  295. clearskies/decorators/bindings.py +0 -9
  296. clearskies/decorators/create.py +0 -10
  297. clearskies/decorators/delete.py +0 -10
  298. clearskies/decorators/docs.py +0 -14
  299. clearskies/decorators/get.py +0 -10
  300. clearskies/decorators/jwks.py +0 -26
  301. clearskies/decorators/merge.py +0 -124
  302. clearskies/decorators/patch.py +0 -10
  303. clearskies/decorators/post.py +0 -10
  304. clearskies/decorators/public.py +0 -11
  305. clearskies/decorators/response_headers.py +0 -10
  306. clearskies/decorators/return_raw_response.py +0 -9
  307. clearskies/decorators/schema.py +0 -10
  308. clearskies/decorators/secret_bearer.py +0 -24
  309. clearskies/decorators/security_headers.py +0 -10
  310. clearskies/di/standard_dependencies.py +0 -151
  311. clearskies/di/test_module/__init__.py +0 -6
  312. clearskies/di/test_module/another_module/__init__.py +0 -2
  313. clearskies/di/test_module/module_class.py +0 -5
  314. clearskies/handlers/__init__.py +0 -41
  315. clearskies/handlers/advanced_search.py +0 -271
  316. clearskies/handlers/base.py +0 -479
  317. clearskies/handlers/callable.py +0 -191
  318. clearskies/handlers/create.py +0 -35
  319. clearskies/handlers/crud_by_method.py +0 -18
  320. clearskies/handlers/database_connector.py +0 -32
  321. clearskies/handlers/delete.py +0 -61
  322. clearskies/handlers/exceptions/__init__.py +0 -5
  323. clearskies/handlers/exceptions/not_found.py +0 -3
  324. clearskies/handlers/get.py +0 -156
  325. clearskies/handlers/health_check.py +0 -59
  326. clearskies/handlers/input_processing.py +0 -79
  327. clearskies/handlers/list.py +0 -530
  328. clearskies/handlers/mygrations.py +0 -82
  329. clearskies/handlers/request_method_routing.py +0 -47
  330. clearskies/handlers/restful_api.py +0 -218
  331. clearskies/handlers/routing.py +0 -62
  332. clearskies/handlers/schema_helper.py +0 -128
  333. clearskies/handlers/simple_routing.py +0 -206
  334. clearskies/handlers/simple_routing_route.py +0 -192
  335. clearskies/handlers/simple_search.py +0 -136
  336. clearskies/handlers/update.py +0 -96
  337. clearskies/handlers/write.py +0 -193
  338. clearskies/input_requirements/__init__.py +0 -78
  339. clearskies/input_requirements/after.py +0 -36
  340. clearskies/input_requirements/before.py +0 -36
  341. clearskies/input_requirements/in_the_future_at_least.py +0 -19
  342. clearskies/input_requirements/in_the_future_at_most.py +0 -19
  343. clearskies/input_requirements/in_the_past_at_least.py +0 -19
  344. clearskies/input_requirements/in_the_past_at_most.py +0 -19
  345. clearskies/input_requirements/maximum_length.py +0 -19
  346. clearskies/input_requirements/maximum_value.py +0 -19
  347. clearskies/input_requirements/minimum_length.py +0 -22
  348. clearskies/input_requirements/minimum_value.py +0 -19
  349. clearskies/input_requirements/requirement.py +0 -25
  350. clearskies/input_requirements/time_delta.py +0 -38
  351. clearskies/input_requirements/unique.py +0 -18
  352. clearskies/mocks/__init__.py +0 -7
  353. clearskies/mocks/input_output.py +0 -124
  354. clearskies/mocks/models.py +0 -142
  355. clearskies/models.py +0 -350
  356. clearskies/security_headers/base.py +0 -12
  357. clearskies/tests/simple_api/models/__init__.py +0 -2
  358. clearskies/tests/simple_api/models/status.py +0 -23
  359. clearskies/tests/simple_api/models/user.py +0 -21
  360. clearskies/tests/simple_api/users_api.py +0 -64
  361. {clear_skies-1.22.10.dist-info → clear_skies-2.0.23.dist-info/licenses}/LICENSE +0 -0
  362. /clearskies/{contexts/bash.py → autodoc/py.typed} +0 -0
  363. /clearskies/{handlers/exceptions → exceptions}/authentication.py +0 -0
  364. /clearskies/{handlers/exceptions → exceptions}/authorization.py +0 -0
  365. /clearskies/{handlers/exceptions → exceptions}/client_error.py +0 -0
  366. /clearskies/{secrets/exceptions → exceptions}/not_found.py +0 -0
  367. /clearskies/{tests/__init__.py → input_outputs/py.typed} +0 -0
  368. /clearskies/{tests/simple_api/__init__.py → py.typed} +0 -0
@@ -0,0 +1,519 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ from clearskies import exceptions
6
+ from clearskies.endpoints.simple_search import SimpleSearch
7
+
8
+ if TYPE_CHECKING:
9
+ from clearskies import Model, autodoc
10
+
11
+
12
+ class AdvancedSearch(SimpleSearch):
13
+ """
14
+ An endpoint that grants the client extensive control over searching and filtering.
15
+
16
+ Rather than accepting URL parameters (like the SimpleSearch endpoint), this endpoint accepts a JSON POST
17
+ body. Search conditions are specified as a list of dictionaries containing `column`, `operator`, and
18
+ `value`. It also accepts up to two sort directives. Of course, while this endpoint supports arbitrary
19
+ searching, it won't work if the backend itself doesn't support it. The following is the list of allowed
20
+ keys in the JSON body:
21
+
22
+ | Name | Type | Description | Example |
23
+ |-------|----------------------|----------------------------------------------------------------------------|---------|
24
+ | sort | list[dict[str, str]] | A list of sort directives containing `column` and `direction` | `{"sort": [ {"column": "age", "direction": "desc} ] }` |
25
+ | limit | int | The number of records to return | `{"limit": `100`}` |
26
+ | where | list[dict[str, Any]] | A list of conditions containing `column`, `operator`, and `value` | `{"where": [ {"column": "age", "operator": ">", "value": 10} ] }` |
27
+ | * | str, int | Pagination information. The key name and value type depend on the backend | `{"start": 100}` |
28
+
29
+ Here's an example making use of the AdvancedSearch endpoint:
30
+
31
+ ```python
32
+ import clearskies
33
+
34
+
35
+ class Company(clearskies.Model):
36
+ id_column_name = "id"
37
+ backend = clearskies.backends.MemoryBackend()
38
+
39
+ id = clearskies.columns.Uuid()
40
+ name = clearskies.columns.String()
41
+
42
+
43
+ class User(clearskies.Model):
44
+ id_column_name = "id"
45
+ backend = clearskies.backends.MemoryBackend()
46
+
47
+ id = clearskies.columns.Uuid()
48
+ name = clearskies.columns.String()
49
+ username = clearskies.columns.String()
50
+ age = clearskies.columns.Integer()
51
+ company_id = clearskies.columns.BelongsToId(Company, readable_parent_columns=["id", "name"])
52
+ company = clearskies.columns.BelongsToModel("company_id")
53
+
54
+
55
+ wsgi = clearskies.contexts.WsgiRef(
56
+ clearskies.endpoints.AdvancedSearch(
57
+ model_class=User,
58
+ readable_column_names=["id", "name", "username", "age", "company"],
59
+ sortable_column_names=["name", "username", "age", "company.name"],
60
+ searchable_column_names=["id", "name", "username", "age", "company_id", "company.name"],
61
+ default_sort_column_name="name",
62
+ ),
63
+ bindings={
64
+ "memory_backend_default_data": [
65
+ {
66
+ "model_class": Company,
67
+ "records": [
68
+ {"id": "5-5-5-5", "name": "Bob's Widgets"},
69
+ {"id": "3-3-3-3", "name": "New Venture"},
70
+ {"id": "7-7-7-7", "name": "Jane's Cool Stuff"},
71
+ ],
72
+ },
73
+ {
74
+ "model_class": User,
75
+ "records": [
76
+ {
77
+ "id": "1-2-3-4",
78
+ "name": "Bob Brown",
79
+ "username": "bobbrown",
80
+ "age": 18,
81
+ "company_id": "5-5-5-5",
82
+ },
83
+ {
84
+ "id": "1-2-3-5",
85
+ "name": "Jane Doe",
86
+ "username": "janedoe",
87
+ "age": 52,
88
+ "company_id": "7-7-7-7",
89
+ },
90
+ {
91
+ "id": "1-2-3-6",
92
+ "name": "Greg",
93
+ "username": "greg",
94
+ "age": 37,
95
+ "company_id": "7-7-7-7",
96
+ },
97
+ {
98
+ "id": "1-2-3-7",
99
+ "name": "Curious George",
100
+ "username": "curious",
101
+ "age": 7,
102
+ "company_id": "3-3-3-3",
103
+ },
104
+ ],
105
+ },
106
+ ],
107
+ },
108
+ )
109
+ wsgi()
110
+ ```
111
+
112
+ If you invoke the endpoint without any additional data, it will simply list all records:
113
+
114
+ ```bash
115
+ $ curl 'http://localhost:8080/' | jq
116
+ {
117
+ "status": "success",
118
+ "error": "",
119
+ "data": [
120
+ {
121
+ "id": "1-2-3-4",
122
+ "name": "Bob Brown",
123
+ "username": "bobbrown",
124
+ "age": 18,
125
+ "company": {
126
+ "id": "5-5-5-5",
127
+ "name": "Bob's Widgets"
128
+ }
129
+ },
130
+ {
131
+ "id": "1-2-3-7",
132
+ "name": "Curious George",
133
+ "username": "curious",
134
+ "age": 7,
135
+ "company": {
136
+ "id": "3-3-3-3",
137
+ "name": "New Venture"
138
+ }
139
+ },
140
+ {
141
+ "id": "1-2-3-6",
142
+ "name": "Greg",
143
+ "username": "greg",
144
+ "age": 37,
145
+ "company": {
146
+ "id": "7-7-7-7",
147
+ "name": "Jane's Cool Stuff"
148
+ }
149
+ },
150
+ {
151
+ "id": "1-2-3-5",
152
+ "name": "Jane Doe",
153
+ "username": "janedoe",
154
+ "age": 52,
155
+ "company": {
156
+ "id": "7-7-7-7",
157
+ "name": "Jane's Cool Stuff"
158
+ }
159
+ }
160
+ ],
161
+ "pagination": {
162
+ "number_results": 4,
163
+ "limit": 50,
164
+ "next_page": {}
165
+ },
166
+ "input_errors": {}
167
+ }
168
+ ```
169
+
170
+ Of course you can also sort and paginate. Keep in mind that pagination is backend-dependent:
171
+
172
+ ```bash
173
+ $ curl 'http://localhost:8080/' -d '{"sort":[ {"column": "name", "direction": "desc"} ], "limit": 2, "start": 1}' | jq
174
+ {
175
+ "status": "success",
176
+ "error": "",
177
+ "data": [
178
+ {
179
+ "id": "1-2-3-6",
180
+ "name": "Greg",
181
+ "username": "greg",
182
+ "age": 37,
183
+ "company": {
184
+ "id": "7-7-7-7",
185
+ "name": "Jane's Cool Stuff"
186
+ }
187
+ },
188
+ {
189
+ "id": "1-2-3-7",
190
+ "name": "Curious George",
191
+ "username": "curious",
192
+ "age": 7,
193
+ "company": {
194
+ "id": "3-3-3-3",
195
+ "name": "New Venture"
196
+ }
197
+ }
198
+ ],
199
+ "pagination": {
200
+ "number_results": 4,
201
+ "limit": 2,
202
+ "next_page": {
203
+ "start": 3
204
+ }
205
+ },
206
+ "input_errors": {}
207
+ }
208
+
209
+ ```
210
+
211
+ Note that sorting on columns in related models is done via the syntax `relationship_column.column_name`. These
212
+ must be listed as such in the list of sortable/searchable columns, and then you use the same name to sort/search
213
+ by them:
214
+
215
+ ```bash
216
+ $ curl 'http://localhost:8080/' -d '{"sort":[ {"column": "company.name", "direction": "desc"}, {"column": "age", "direction": "asc"} ]}' | jq
217
+ {
218
+ "status": "success",
219
+ "error": "",
220
+ "data": [
221
+ {
222
+ "id": "1-2-3-7",
223
+ "name": "Curious George",
224
+ "username": "curious",
225
+ "age": 7,
226
+ "company": {
227
+ "id": "3-3-3-3",
228
+ "name": "New Venture"
229
+ }
230
+ },
231
+ {
232
+ "id": "1-2-3-6",
233
+ "name": "Greg",
234
+ "username": "greg",
235
+ "age": 37,
236
+ "company": {
237
+ "id": "7-7-7-7",
238
+ "name": "Jane's Cool Stuff"
239
+ }
240
+ },
241
+ {
242
+ "id": "1-2-3-5",
243
+ "name": "Jane Doe",
244
+ "username": "janedoe",
245
+ "age": 52,
246
+ "company": {
247
+ "id": "7-7-7-7",
248
+ "name": "Jane's Cool Stuff"
249
+ }
250
+ },
251
+ {
252
+ "id": "1-2-3-4",
253
+ "name": "Bob Brown",
254
+ "username": "bobbrown",
255
+ "age": 18,
256
+ "company": {
257
+ "id": "5-5-5-5",
258
+ "name": "Bob's Widgets"
259
+ }
260
+ }
261
+ ],
262
+ "pagination": {
263
+ "number_results": 4,
264
+ "limit": 50,
265
+ "next_page": {}
266
+ },
267
+ "input_errors": {}
268
+ }
269
+
270
+ ```
271
+
272
+ And finally searching:
273
+
274
+ ```bash
275
+ $ curl 'http://localhost:8080/' -d '{"where":[ {"column": "age", "operator": "<=", "value": 37}, {"column": "username", "operator": "in", "value": ["curious", "greg"]} ]}' | jq
276
+ {
277
+ "status": "success",
278
+ "error": "",
279
+ "data": [
280
+ {
281
+ "id": "1-2-3-7",
282
+ "name": "Curious George",
283
+ "username": "curious",
284
+ "age": 7,
285
+ "company": {
286
+ "id": "3-3-3-3",
287
+ "name": "New Venture"
288
+ }
289
+ },
290
+ {
291
+ "id": "1-2-3-6",
292
+ "name": "Greg",
293
+ "username": "greg",
294
+ "age": 37,
295
+ "company": {
296
+ "id": "7-7-7-7",
297
+ "name": "Jane's Cool Stuff"
298
+ }
299
+ }
300
+ ],
301
+ "pagination": {
302
+ "number_results": 2,
303
+ "limit": 50,
304
+ "next_page": {}
305
+ },
306
+ "input_errors": {}
307
+ }
308
+
309
+ ```
310
+
311
+ In terms of the allowed search operators, the standard list of operators is:
312
+
313
+ * `<=>`
314
+ * `!=`
315
+ * `<=`
316
+ * `>=`
317
+ * `>`
318
+ * `<`
319
+ * `=`
320
+ * `in`
321
+ * `is not null`
322
+ * `is null`
323
+ * `is not`
324
+ * `is`
325
+ * `like`
326
+
327
+ Although not all operators are supported by all columns. You can use `%` with the `LIKE` operator
328
+ to perform a wildcard search.
329
+
330
+ """
331
+
332
+ @property
333
+ def allowed_request_keys(self) -> list[str]:
334
+ return self.internal_request_keys
335
+
336
+ @property
337
+ def internal_request_keys(self) -> list[str]:
338
+ return ["sort", "limit", "where"]
339
+
340
+ def check_request_data(
341
+ self, request_data: dict[str, Any], query_parameters: dict[str, Any], pagination_data: dict[str, Any]
342
+ ) -> None:
343
+ if pagination_data:
344
+ error = self.model.validate_pagination_data(pagination_data, self.auto_case_internal_column_name)
345
+ if error:
346
+ raise exceptions.ClientError(error)
347
+ if query_parameters:
348
+ raise exceptions.ClientError("Query parameters were found but are not supported.")
349
+ for key in request_data.keys():
350
+ if key not in self.allowed_request_keys:
351
+ raise exceptions.ClientError(
352
+ f"Invalid request parameter found in request body: '{key}'. Expected parameters: "
353
+ + ", ".join([self.auto_case_internal_column_name(key) for key in self.allowed_request_keys])
354
+ )
355
+ self.validate_limit(request_data, {})
356
+ sort_key_name = self.auto_case_internal_column_name("sort")
357
+ sort = request_data.get(sort_key_name, [])
358
+ if not isinstance(sort, list):
359
+ raise exceptions.ClientError(
360
+ f"'{sort_key_name}' property in request body should be a list, but I found a value of type "
361
+ + sort.__class__.__name
362
+ )
363
+ if sort:
364
+ column_key_name = self.auto_case_internal_column_name("column")
365
+ direction_key_name = self.auto_case_internal_column_name("direction")
366
+ for index, sort_entry in enumerate(sort):
367
+ if not isinstance(sort_entry, dict):
368
+ raise exceptions.ClientError(
369
+ f"'{sort_key_name}' should be a list of dictionaries, but entry #{index + 1} is a value of type '{sort_entry.__class__.__name}', not a dict"
370
+ )
371
+ for key_name in [column_key_name, direction_key_name]:
372
+ if not sort_entry.get(key_name):
373
+ raise exceptions.ClientError(
374
+ f"Each entry in the sort list should contain both '{column_key_name}' and '{direction_key_name}' but entry #{index + 1} is missing '{key_name}'"
375
+ )
376
+ if not isinstance(sort_entry[key_name], str):
377
+ raise exceptions.ClientError(
378
+ f"{key_name}' must be a string, but for entry #{index + 1} it is a value of type "
379
+ + sort_entry[key_name].__class__.__name__
380
+ )
381
+ if sort_entry[direction_key_name].lower() not in ["asc", "desc"]:
382
+ raise exceptions.ClientError(
383
+ f"{direction_key_name}' must be either 'ASC' or 'DESC', but a different value was found for entry #{index + 1}"
384
+ )
385
+ if self.auto_case_column_name(sort_entry[column_key_name], False) not in self.sortable_column_names:
386
+ raise exceptions.ClientError(
387
+ f"Invalid sort column for entry #{index + 1}. Allowed values are: "
388
+ + ", ".join(
389
+ [
390
+ self.auto_case_column_name(column_name, False)
391
+ for column_name in self.sortable_column_names
392
+ ]
393
+ )
394
+ )
395
+ where_key_name = self.auto_case_internal_column_name("where")
396
+ where = request_data.get(where_key_name, [])
397
+ if not isinstance(where, list):
398
+ raise exceptions.ClientError(
399
+ f"'{where_key_name}' property in request body should be a list, but I found a value of type "
400
+ + where.__class__.__name
401
+ )
402
+ if where:
403
+ column_key_name = self.auto_case_internal_column_name("column")
404
+ operator_key_name = self.auto_case_internal_column_name("operator")
405
+ value_key_name = self.auto_case_internal_column_name("value")
406
+ for index, where_entry in enumerate(where):
407
+ if not isinstance(where_entry, dict):
408
+ raise exceptions.ClientError(
409
+ f"'{where_key_name}' should be a list of dictionaries, but entry #{index + 1} is a value of type '{where_entry.__class__.__name}', not a dict"
410
+ )
411
+ for key_name in [column_key_name, operator_key_name, value_key_name]:
412
+ if key_name not in where_entry:
413
+ raise exceptions.ClientError(
414
+ f"Each entry in the where list should contain '{column_key_name}', '{operator_key_name}', and '{value_key_name}', but entry #{index + 1} is missing '{key_name}'"
415
+ )
416
+ if key_name != value_key_name and not isinstance(where_entry[key_name], str):
417
+ raise exceptions.ClientError(
418
+ f"{key_name}' must be a string, but for entry #{index + 1} it is a value of type "
419
+ + sort_entry[key_name].__class__.__name__
420
+ )
421
+ if where_entry[column_key_name] not in self.searchable_column_names:
422
+ raise exceptions.ClientError(
423
+ f"Invalid where column for entry #{index + 1}. Allowed values are: "
424
+ + ", ".join(
425
+ [
426
+ self.auto_case_column_name(column_name, True)
427
+ for column_name in self.searchable_column_names
428
+ ]
429
+ )
430
+ )
431
+ [relationship_column_name, column_name] = self.unpack_column_name_with_relationship(
432
+ self.auto_case_column_name(where_entry[column_key_name], False),
433
+ )
434
+ operator = where_entry[operator_key_name].lower()
435
+ value = where_entry[value_key_name]
436
+ error_allowed_operators = None
437
+ if relationship_column_name:
438
+ column = self.columns[relationship_column_name]
439
+ if not column.is_allowed_search_operator(operator, relationship_reference=column_name):
440
+ error_allowed_operators = column.allowed_search_operators(
441
+ relationship_reference=column_name
442
+ )
443
+ else:
444
+ error = column.check_search_value(
445
+ value if operator != "in" else value[0],
446
+ where_entry[operator_key_name],
447
+ relationship_reference=column_name,
448
+ )
449
+ else:
450
+ column = self.columns[column_name]
451
+ if not column.is_allowed_search_operator(operator):
452
+ error_allowed_operators = column.allowed_search_operators()
453
+ else:
454
+ error = column.check_search_value(
455
+ value if operator != "in" else value[0], where_entry[operator_key_name]
456
+ )
457
+ if error_allowed_operators:
458
+ raise exceptions.ClientError(
459
+ f"Invalid operator for entry #{index + 1}. Allowed operators are: "
460
+ + ", ".join(column.allowed_search_operators(relationship_reference=column_name))
461
+ )
462
+ if error:
463
+ raise exceptions.ClientError(f"Invalid search value for entry #{index + 1}: {error}")
464
+
465
+ def configure_model_from_request_data(
466
+ self,
467
+ model: Model,
468
+ request_data: dict[str, Any],
469
+ query_parameters: dict[str, Any],
470
+ pagination_data: dict[str, Any],
471
+ ) -> Model:
472
+ if pagination_data:
473
+ model = model.pagination(**pagination_data)
474
+ sort = request_data.get(self.auto_case_internal_column_name("sort"), [])
475
+ if sort:
476
+ column_key_name = self.auto_case_internal_column_name("column")
477
+ direction_key_name = self.auto_case_internal_column_name("direction")
478
+ model = self.add_join(sort[0][column_key_name], model)
479
+ [primary_table_name, primary_column_name] = self.resolve_references_for_query(sort[0][column_key_name])
480
+ primary_direction = sort[0][direction_key_name]
481
+
482
+ if len(sort) > 1:
483
+ [secondary_table_name, secondary_column_name] = self.resolve_references_for_query(
484
+ sort[1][column_key_name]
485
+ )
486
+ secondary_direction = sort[1][direction_key_name]
487
+ else:
488
+ secondary_column_name = ""
489
+ secondary_direction = ""
490
+ secondary_table_name = ""
491
+ model = model.sort_by(
492
+ primary_column_name if primary_column_name else "",
493
+ primary_direction if primary_direction else "",
494
+ primary_table_name=primary_table_name if primary_table_name else "",
495
+ secondary_column_name=secondary_column_name if secondary_column_name else "",
496
+ secondary_direction=secondary_direction if secondary_direction else "",
497
+ secondary_table_name=secondary_table_name if secondary_table_name else "",
498
+ )
499
+ if request_data.get("limit"):
500
+ model = model.limit(request_data["limit"])
501
+
502
+ for where in request_data.get(self.auto_case_internal_column_name("where"), []):
503
+ raw_column_name = self.auto_case_column_name(where[self.auto_case_internal_column_name("column")], False)
504
+ [relationship_column_name, column_name] = self.unpack_column_name_with_relationship(raw_column_name)
505
+ operator = where[self.auto_case_internal_column_name("operator")].lower()
506
+ value = where[self.auto_case_internal_column_name("value")]
507
+
508
+ model = self.add_join(raw_column_name, model)
509
+ if relationship_column_name:
510
+ model = self.columns[relationship_column_name].add_search(
511
+ model, value, operator=operator, relationship_reference=column_name
512
+ )
513
+ else:
514
+ model = self.columns[column_name].add_search(model, value, operator=operator)
515
+
516
+ return model
517
+
518
+ def documentation_url_search_parameters(self) -> list[autodoc.request.Parameter]:
519
+ return []