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,12 @@
1
+ from clearskies.query.condition import Condition, ParsedCondition
2
+ from clearskies.query.join import Join
3
+ from clearskies.query.query import Query
4
+ from clearskies.query.sort import Sort
5
+
6
+ __all__ = [
7
+ "Condition",
8
+ "Join",
9
+ "ParsedCondition",
10
+ "Sort",
11
+ "Query",
12
+ ]
@@ -0,0 +1,228 @@
1
+ class Condition:
2
+ """
3
+ Parses a condition string, e.g. "column=value" or "table.column<=other_value".
4
+
5
+ Allowed operators: ["<=>", "!=", "<=", ">=", ">", "<", "=", "in", "is not null", "is null", "is not", "is", "like"]
6
+
7
+ NOTE: Not all backends support all operators, so make sure the condition you are building works for your backend
8
+
9
+ This is safe to use with untrusted input because it expects a stringent and easy-to-verify format. The incoming
10
+ string must be one of these patterns:
11
+
12
+ 1. [column_name][operator][value]
13
+ 2. [table_name].[column_name][operator][value]
14
+
15
+ SQL-like syntax is allowed, so:
16
+
17
+ 1. Spaces are optionally allowed around the operator.
18
+ 2. Backticks are optionally allowd around the table/column name.
19
+ 3. Single quotes are optionally allowed around the values.
20
+ 4. operators are case-insensitive.
21
+
22
+ In the case of an IN operator, the parser expects a series of comma separated values enclosed in parenthesis,
23
+ with each value optionally enclosed in single quotes. This parsing is very simple and there is not currently a way
24
+ to escape commas or single quotes.
25
+
26
+ NOTE: operators (when they are english words, of course) are always output in all upper-case.
27
+
28
+ Some examples:
29
+
30
+ ```python
31
+ condition = Condition("id=asdf-qwerty") # note: same results for: Condition("id = asdf-qwerty")
32
+ print(condition.table_name) # prints ''
33
+ print(condition.column_name) # prints 'id'
34
+ print(condition.operator) # prints '='
35
+ print(condition.values) # prints ['asdf-qwerty']
36
+ print(condition.parsed) # prints 'id=%s'
37
+
38
+ condition = Condition("orders.status_id in ('ACTIVE', 'PENDING')")
39
+ print(condition.table_name) # prints 'orders'
40
+ print(condition.column_name) # prints 'status_id'
41
+ print(condition.operator) # prints 'IN'
42
+ print(condition.values) # prints ['ACTIVE', 'PENDING']
43
+ print(condition.parsed) # prints 'status_id IN (%s, %s)'
44
+ ```
45
+ """
46
+
47
+ """
48
+ The name of the table this condition is searching on (if there is one).
49
+ """
50
+ table_name: str = ""
51
+
52
+ """
53
+ The name of the column the condition is searching.
54
+ """
55
+ column_name: str = ""
56
+
57
+ """
58
+ The operator we are searching with (e.g. '=', '<=', etc...)
59
+ """
60
+ operator: str = ""
61
+
62
+ """
63
+ The values the condition is searching for.
64
+
65
+ Note this is always a list, although most of the time there is only one value in the list. Multiple values
66
+ are only present when searching with the IN operator.
67
+ """
68
+ values: list[str] = []
69
+
70
+ """
71
+ An SQL-ready string
72
+ """
73
+ parsed: str = ""
74
+
75
+ """
76
+ The original condition string
77
+ """
78
+ _raw_condition: str = ""
79
+
80
+ """
81
+ The list of operators we can match
82
+
83
+ Note: the order is very important because this list is used to find the operator in the condition string.
84
+ As a result, the order of the operators in this list is important. The condition matching algorithm used
85
+ below will select whichever operator matches earlier in the string, but there are some operators that
86
+ start with the same characters: '<=>' and '<=', as well as 'is', 'is null', 'is not', etc... This leaves
87
+ room for ambiguity since all of these operators will match at the same location. In the event of a "tie" the
88
+ algorithm gives preference to the first matching operator. Therefore, for ambiguous operators, we put the
89
+ longer one first, which means it matches first, and so a condition with a '<=>' operator won't accidentally
90
+ match to the '<=' operator.
91
+ """
92
+ operators: list[str] = [
93
+ "<=>",
94
+ "!=",
95
+ "<=",
96
+ ">=",
97
+ ">",
98
+ "<",
99
+ "=",
100
+ "in",
101
+ "is not null",
102
+ "is null",
103
+ "is not",
104
+ "is",
105
+ "like",
106
+ "not in",
107
+ ]
108
+
109
+ operator_lengths: dict[str, int] = {
110
+ "<=>": 3,
111
+ "<=": 2,
112
+ ">=": 2,
113
+ "!=": 2,
114
+ ">": 1,
115
+ "<": 1,
116
+ "=": 1,
117
+ "in": 4,
118
+ "is not null": 12,
119
+ "is null": 8,
120
+ "is not": 8,
121
+ "is": 4,
122
+ "like": 6,
123
+ "not in": 8,
124
+ }
125
+
126
+ # some operators require spaces around them
127
+ operators_for_matching: dict[str, str] = {
128
+ "like": " like ",
129
+ "in": " in ",
130
+ "not in": " not in ",
131
+ "is not null": " is not null",
132
+ "is null": " is null",
133
+ "is": " is ",
134
+ "is not": " is not ",
135
+ }
136
+
137
+ operators_with_simple_placeholders: dict[str, bool] = {
138
+ "<=>": True,
139
+ "<=": True,
140
+ ">=": True,
141
+ "!=": True,
142
+ "=": True,
143
+ "<": True,
144
+ ">": True,
145
+ }
146
+
147
+ operators_without_placeholders: dict[str, bool] = {
148
+ "is not null": True,
149
+ "is null": True,
150
+ }
151
+
152
+ def __init__(self, condition: str):
153
+ self._raw_condition = condition
154
+ lowercase_condition = condition.lower()
155
+ self.operator = ""
156
+ matching_index = len(condition)
157
+ # figure out which operator comes earliest in the string: make sure and check all so we match the
158
+ # earliest operator so we don't get unpredictable results for things like 'age=name<=5'. We want
159
+ # our operator to **ALWAYS** match whatever comes first in the condition string.
160
+ for operator in self.operators:
161
+ try:
162
+ operator_for_match = self.operators_for_matching.get(operator, operator)
163
+ index = lowercase_condition.index(operator_for_match)
164
+ except ValueError:
165
+ continue
166
+ if index < matching_index:
167
+ matching_index = index
168
+ self.operator = operator
169
+
170
+ if not self.operator:
171
+ raise ValueError(f"No supported operators found in condition {condition}")
172
+
173
+ self.column_name = condition[:matching_index].strip().replace("`", "")
174
+ value = condition[matching_index + self.operator_lengths[self.operator] :].strip()
175
+ if value and (value[0] == "'" and value[-1] == "'"):
176
+ value = value.strip("'")
177
+ self.values = self._parse_condition_list(value) if self.operator == "in" else [value]
178
+ self.table_name = ""
179
+ if "." in self.column_name:
180
+ [self.table_name, self.column_name] = self.column_name.split(".")
181
+ column_for_parsed = f"{self.table_name}.{self.column_name}" if self.table_name else self.column_name
182
+
183
+ if self.operator in self.operators_without_placeholders:
184
+ self.values = []
185
+
186
+ self.operator = self.operator.upper()
187
+ self.parsed = self._with_placeholders(
188
+ column_for_parsed, self.operator, self.values, escape=False if self.table_name else True
189
+ )
190
+
191
+ def _parse_condition_list(self, value):
192
+ if value[0] != "(" and value[-1] != ")":
193
+ raise ValueError(f"Invalid search value {value} for condition. For IN operator use `IN (value1,value2)`")
194
+
195
+ # note: this is not very smart and will mess things up if there are single quotes/commas in the data
196
+ return list(map(lambda value: value.strip().strip("'"), value[1:-1].split(",")))
197
+
198
+ def _with_placeholders(self, column, operator, values, escape=True, escape_character="`", placeholder="%s"):
199
+ quote = escape_character if escape else ""
200
+ column = column.replace("`", "")
201
+ upper_case_operator = operator.upper()
202
+ lower_case_operator = operator.lower()
203
+ if lower_case_operator in self.operators_with_simple_placeholders:
204
+ return f"{quote}{column}{quote}{upper_case_operator}{placeholder}"
205
+ if lower_case_operator in self.operators_without_placeholders:
206
+ return f"{quote}{column}{quote} {upper_case_operator}"
207
+ if lower_case_operator == "is" or lower_case_operator == "is not" or lower_case_operator == "like":
208
+ return f"{quote}{column}{quote} {upper_case_operator} {placeholder}"
209
+ if lower_case_operator == "not in":
210
+ return f"{quote}{column}{quote} NOT IN ({', '.join([placeholder for i in range(len(values))])})"
211
+
212
+ # the only thing left is "in" which has a variable number of placeholders
213
+ return f"{quote}{column}{quote} IN ({', '.join([placeholder for i in range(len(values))])})"
214
+
215
+
216
+ class ParsedCondition(Condition):
217
+ def __init__(self, column_name: str, operator: str, values: list[str], table_name: str = ""):
218
+ self.column_name = column_name
219
+ if operator not in self.operators:
220
+ raise ValueError(f"Unknown operator '{operator}'")
221
+ self.operator = operator
222
+ self.values = values
223
+ self.table_name = table_name
224
+ column_for_parsed = f"{self.table_name}.{self.column_name}" if self.table_name else self.column_name
225
+ self.parsed = self._with_placeholders(
226
+ column_for_parsed, self.operator, self.values, escape=False if self.table_name else True
227
+ )
228
+ self._raw_condition = self.parsed
@@ -0,0 +1,136 @@
1
+ import re
2
+
3
+
4
+ class Join:
5
+ """
6
+ Parses a join clause.
7
+
8
+ Note that this expects a few very specific pattern:
9
+
10
+ 1. [TYPE] JOIN [right_table_name] ON [left_table_name].[left_column_name]=[right_table_name].[right_column_name]
11
+ 2. [TYPE] JOIN [right_table_name] AS [alias] ON [alias].[left_column_name]=[right_table_name].[right_column_name]
12
+ 3. [TYPE] JOIN [right_table_name] [alias] ON [alias].[left_column_name]=[right_table_name].[right_column_name]
13
+
14
+ NOTE: The allowed join types are ["INNER", "OUTER", "LEFT", "RIGHT"]
15
+
16
+ NOTE: backticks are optionally allowed around column and table names.
17
+
18
+ Examples:
19
+ ```python
20
+ join = Join("INNER JOIN orders ON users.id=orders.user_id")
21
+ print(f"{join.left_table_name}.{join.left_column_name}") # prints 'users.id'
22
+ print(f"{join.right_table_name}.{join.right_column_name}") # prints 'orders.user_id'
23
+ print(join.type) # prints 'INNER'
24
+ print(join.alias) # prints ''
25
+ print(join.unaliased_table_name) # prints 'orders'
26
+
27
+ join = Join("JOIN some_long_table_name AS new_table ON old_table.id=new_table.old_id")
28
+ print(f"{join.left_table_name}.{join.left_column_name}") # prints 'old_table.id'
29
+ print(f"{join.right_table_name}.{join.right_column_name}") # prints 'new_table.old_id'
30
+ print(join.type) # prints 'LEFT'
31
+ print(join.alias) # prints 'new_table'
32
+ print(join.unaliased_table_name) # prints 'some_long_table_name'
33
+ ```
34
+ """
35
+
36
+ """
37
+ The name of the table on the left side of the join
38
+ """
39
+ left_table_name: str = ""
40
+
41
+ """
42
+ The name of the column on the left side of the join
43
+ """
44
+ left_column_name: str = ""
45
+
46
+ """
47
+ The name of the table on the right side of the join
48
+ """
49
+ right_table_name: str = ""
50
+
51
+ """
52
+ The name of the column on the right side of the join
53
+ """
54
+ right_column_name: str = ""
55
+
56
+ """
57
+ The type of join (LEFT, RIGHT, INNER, OUTER)
58
+ """
59
+ join_type: str = ""
60
+
61
+ """
62
+ An alias for the joined table, if needed
63
+ """
64
+ alias: str = ""
65
+
66
+ """
67
+ The actual name of the right table, regardless of alias
68
+ """
69
+ unaliased_table_name: str = ""
70
+
71
+ """
72
+ The original join string
73
+ """
74
+ _raw_join: str = ""
75
+
76
+ """
77
+ The allowed join types
78
+ """
79
+ _allowed_types = ["INNER", "OUTER", "LEFT", "RIGHT"]
80
+
81
+ def __init__(self, join: str):
82
+ self._raw_join = join
83
+ # doing this the simple and stupid way, until that doesn't work. Yes, it is ugly.
84
+ # Splitting this into two regexps for simplicity: this one does not check for an alias
85
+ matches = re.match(
86
+ "(\\w+\\s+)?join\\s+`?([^\\s`]+)`?\\s+on\\s+`?([^`]+)`?\\.`?([^`]+)`?\\s*=\\s*`?([^`]+)`?\\.`?([^`]+)`?",
87
+ join,
88
+ re.IGNORECASE,
89
+ )
90
+ if matches:
91
+ groups = matches.groups()
92
+ alias = ""
93
+ join_type = groups[0]
94
+ right_table = groups[1]
95
+ first_table = groups[2]
96
+ first_column = groups[3]
97
+ second_table = groups[4]
98
+ second_column = groups[5]
99
+ else:
100
+ matches = re.match(
101
+ "(\\w+\\s+)?join\\s+`?([^\\s`]+)`?\\s+(as\\s+)?(\\S+)\\s+on\\s+`?([^`]+)`?\\.`?([^`]+)`?\\s*=\\s*`?([^`]+)`?\\.`?([^`]+)`?",
102
+ join,
103
+ re.IGNORECASE,
104
+ )
105
+ if not matches:
106
+ raise ValueError(f"Specified join condition, '{join}' does not appear to be a valid join statement")
107
+ groups = matches.groups()
108
+ join_type = groups[0]
109
+ right_table = groups[1]
110
+ alias = groups[3]
111
+ first_table = groups[4]
112
+ first_column = groups[5]
113
+ second_table = groups[6]
114
+ second_column = groups[7]
115
+
116
+ # which is the left table and which is the right table?
117
+ match_by = alias if alias else right_table
118
+ if first_table == match_by:
119
+ self.left_table_name = second_table
120
+ self.left_column_name = second_column
121
+ self.right_table_name = first_table
122
+ self.right_column_name = first_column
123
+ elif second_table == match_by:
124
+ self.left_table_name = first_table
125
+ self.left_column_name = first_column
126
+ self.right_table_name = second_table
127
+ self.right_column_name = second_column
128
+ else:
129
+ raise ValueError(
130
+ f"Specified join condition, '{join}' was not understandable because the joined table "
131
+ + "is not referenced in the 'on' clause"
132
+ )
133
+
134
+ self.join_type = groups[0].strip().upper() if groups[0] else "INNER"
135
+ self.alias = alias
136
+ self.unaliased_table_name = right_table
@@ -0,0 +1,193 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, Self
4
+
5
+ if TYPE_CHECKING:
6
+ from clearskies import Model
7
+ from clearskies.query import Condition, Join, Sort
8
+
9
+
10
+ class Query:
11
+ """
12
+ Track the various aspects of a query.
13
+
14
+ This is mostly just used by the Model class to keep track of a list request.
15
+ """
16
+
17
+ """
18
+ The model class
19
+ """
20
+ model_class: type[Model] = None # type: ignore
21
+
22
+ """
23
+ The list of where conditions for the query.
24
+ """
25
+ conditions: list[Condition] = []
26
+
27
+ """
28
+ The conditions, but organized by column.
29
+ """
30
+ conditions_by_column: dict[str, list[Condition]] = {}
31
+
32
+ """
33
+ Joins for the query.
34
+ """
35
+ joins: list[Join] = []
36
+
37
+ """
38
+ The sort directives for the query
39
+ """
40
+ sorts: list[Sort] = []
41
+
42
+ """
43
+ The maximum number of records to return.
44
+ """
45
+ limit: int = 0
46
+
47
+ """
48
+ Pagination information (e.g. start/next_token/etc... the details depend on the backend.
49
+ """
50
+ pagination: dict[str, Any] = {}
51
+
52
+ """
53
+ A list of select statements.
54
+ """
55
+ selects: list[str] = []
56
+
57
+ """
58
+ Whether or not to just select all columns.
59
+ """
60
+ select_all: bool = True
61
+
62
+ """
63
+ The name of the column to group by.
64
+ """
65
+ group_by = ""
66
+
67
+ def __init__(
68
+ self,
69
+ model_class: type[Model],
70
+ conditions: list[Condition] = [],
71
+ joins: list[Join] = [],
72
+ sorts: list[Sort] = [],
73
+ limit: int = 0,
74
+ group_by: str = "",
75
+ pagination: dict[str, Any] = {},
76
+ selects: list[str] = [],
77
+ select_all: bool = True,
78
+ ):
79
+ self.model_class = model_class
80
+ self.conditions = [*conditions]
81
+ self.joins = [*joins]
82
+ self.sorts = [*sorts]
83
+ self.limit = limit
84
+ self.group_by = group_by
85
+ self.pagination = {**pagination}
86
+ self.selects = [*selects]
87
+ self.select_all = select_all
88
+ self.conditions_by_column = {}
89
+ if conditions:
90
+ for condition in conditions:
91
+ if condition.column_name not in self.conditions_by_column:
92
+ self.conditions_by_column[condition.column_name] = []
93
+ self.conditions_by_column[condition.column_name].append(condition)
94
+
95
+ def as_kwargs(self):
96
+ """Return the properties of this query as a dictionary so it can be used as kwargs when creating another one."""
97
+ return {
98
+ "model_class": self.model_class,
99
+ "conditions": [*self.conditions],
100
+ "joins": [*self.joins],
101
+ "sorts": [*self.sorts],
102
+ "limit": self.limit,
103
+ "group_by": self.group_by,
104
+ "pagination": self.pagination,
105
+ "selects": [*self.selects],
106
+ "select_all": self.select_all,
107
+ }
108
+
109
+ def add_where(self, condition: Condition) -> Self:
110
+ self.validate_column(condition.column_name, "filter", table=condition.table_name)
111
+ new_kwargs = self.as_kwargs()
112
+ new_kwargs["conditions"].append(condition)
113
+ return self.__class__(**new_kwargs)
114
+
115
+ def add_join(self, join: Join) -> Self:
116
+ new_kwargs = self.as_kwargs()
117
+ new_kwargs["joins"].append(join)
118
+ return self.__class__(**new_kwargs)
119
+
120
+ def set_sort(self, sort: Sort, secondary_sort: Sort | None = None) -> Self:
121
+ self.validate_column(sort.column_name, "sort", table=sort.table_name)
122
+ new_kwargs = self.as_kwargs()
123
+ new_kwargs["sorts"] = [sort]
124
+ if secondary_sort:
125
+ new_kwargs["sorts"].append(secondary_sort)
126
+
127
+ return self.__class__(**new_kwargs)
128
+
129
+ def set_limit(self, limit: int) -> Self:
130
+ if not isinstance(limit, int):
131
+ raise TypeError(
132
+ f"The limit in a query must be of type int but I received a value of type '{limit.__class__.__name__}'"
133
+ )
134
+ return self.__class__(
135
+ **{
136
+ **self.as_kwargs(),
137
+ "limit": limit,
138
+ }
139
+ )
140
+
141
+ def set_group_by(self, column_name) -> Self:
142
+ self.validate_column(column_name, "group")
143
+ return self.__class__(
144
+ **{
145
+ **self.as_kwargs(),
146
+ "group_by": column_name,
147
+ }
148
+ )
149
+
150
+ def set_pagination(self, pagination: dict[str, Any]) -> Self:
151
+ return self.__class__(
152
+ **{
153
+ **self.as_kwargs(),
154
+ "pagination": pagination,
155
+ }
156
+ )
157
+
158
+ def add_select(self, select: str) -> Self:
159
+ new_kwargs = self.as_kwargs()
160
+ new_kwargs["selects"].append(select)
161
+ return self.__class__(**new_kwargs)
162
+
163
+ def set_select_all(self, select_all: bool) -> Self:
164
+ return self.__class__(
165
+ **{
166
+ **self.as_kwargs(),
167
+ "select_all": select_all,
168
+ }
169
+ )
170
+
171
+ def validate_column(self: Self, column_name: str, action: str, table: str | None = None) -> None:
172
+ # for now, only validate columns that belong to *our* table.
173
+ # in some cases we are explicitly told the column name
174
+ if table is not None:
175
+ # note that table may be '', in which case it is implicitly "our" table
176
+ if table != "" and table != self.model_class.destination_name():
177
+ return
178
+
179
+ # but in some cases we should check and see if it is included with the column name
180
+ column_name = column_name.replace("`", "")
181
+ if "." in column_name:
182
+ parts = column_name.split(".")
183
+ if parts[0] != self.model_class.destination_name():
184
+ return
185
+ column_name = column_name.split(".")[1]
186
+
187
+ model_columns = self.model_class.get_columns()
188
+ if column_name not in model_columns:
189
+ raise KeyError(
190
+ f"Cannot {action} by column '{column_name}' for model class {self.model_class.__name__} because this "
191
+ + "column does not exist for the model. You can suppress this error by adding a matching column "
192
+ + "to your model definition"
193
+ )
@@ -0,0 +1,27 @@
1
+ class Sort:
2
+ """Stores a sort directive."""
3
+
4
+ """
5
+ The name of the table to sort on.
6
+ """
7
+ table_name: str = ""
8
+
9
+ """
10
+ The name of the column to sort on.
11
+ """
12
+ column_name: str = ""
13
+
14
+ """
15
+ The direction to sort.
16
+ """
17
+ direction: str = ""
18
+
19
+ def __init__(self, table_name: str, column_name: str, direction: str):
20
+ if not column_name:
21
+ raise ValueError("Missing 'column_name' for sort")
22
+ direction = direction.upper().strip()
23
+ if direction != "ASC" and direction != "DESC":
24
+ raise ValueError(f"Invalid sort direction: should be ASC or DESC, not '{direction}'")
25
+ self.table_name = table_name
26
+ self.column_name = column_name
27
+ self.direction = direction
clearskies/schema.py ADDED
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import OrderedDict
4
+ from typing import TYPE_CHECKING, Self
5
+
6
+ if TYPE_CHECKING:
7
+ from clearskies import Column
8
+
9
+
10
+ class Schema:
11
+ """
12
+ Define a schema by extending and declaring columns.
13
+
14
+ ```python
15
+ from clearskies.schema import Schema
16
+ from clearskies.validators import Required, Unique
17
+
18
+ import clearskies.columns
19
+
20
+
21
+ class Person(Schema):
22
+ id = clearskies.columns.Uuid()
23
+ name = clearskies.columns.String(validators=[Required()])
24
+ date_of_birth = clearskies.columns.Datetime(validators=[Required(), InThePast()])
25
+ email = clearskies.columns.Email()
26
+ ```
27
+ """
28
+
29
+ id_column_name: str = ""
30
+ _columns: dict[str, Column] = {}
31
+
32
+ @classmethod
33
+ def destination_name(cls: type[Self]) -> str:
34
+ raise NotImplementedError()
35
+
36
+ def __init__(self):
37
+ self._data = {}
38
+
39
+ @classmethod
40
+ def get_columns(cls: type[Self], overrides={}) -> dict[str, Column]:
41
+ """
42
+ Return an ordered dictionary with the configuration for the columns.
43
+
44
+ Generally, this method is meant for internal use. It just pulls the column configuration
45
+ information out of class attributes. It doesn't return the fully prepared columns,
46
+ so you probably can't use the return value of this function. For that, see
47
+ `model.columns()`.
48
+ """
49
+ # no caching if we have overrides
50
+ if cls._columns and not overrides:
51
+ return cls._columns
52
+
53
+ overrides = {**overrides}
54
+ columns: dict[str, Column] = OrderedDict()
55
+ for attribute_name in dir(cls):
56
+ attribute = getattr(cls, attribute_name)
57
+ # use duck typing instead of isinstance to decide which attribute is a column.
58
+ # We have to do this to avoid circular imports.
59
+ if not hasattr(attribute, "from_backend") and not hasattr(attribute, "to_backend"):
60
+ continue
61
+
62
+ if attribute_name in overrides:
63
+ columns[attribute_name] = overrides[attribute_name]
64
+ del overrides[attribute_name]
65
+ columns[attribute_name] = attribute
66
+
67
+ for attribute_name, column in overrides.items():
68
+ columns[attribute_name] = column # type: ignore
69
+
70
+ if not overrides:
71
+ cls._columns = columns
72
+
73
+ # now go through and finalize everything. We have to do this after setting cls._columns, because finalization
74
+ # sometimes depends on fetching the list of columns, so if we do it before caching the answer, we may end up
75
+ # creating circular loops. I don't *think* this will cause painful side-effects, but we'll find out!
76
+ for column_name, column in cls._columns.items():
77
+ column.finalize_configuration(cls, column_name)
78
+
79
+ return columns
80
+
81
+ def __bool__(self):
82
+ return False