clear-skies 2.0.5__py3-none-any.whl → 2.0.7__py3-none-any.whl

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

Potentially problematic release.


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

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