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
@@ -1,192 +0,0 @@
1
- import logging
2
- import urllib.parse
3
- import re
4
- import json
5
- from ..autodoc.request import URLPath
6
- from ..autodoc.schema import String
7
- from . import simple_routing
8
-
9
- logger = logging.getLogger(__name__)
10
-
11
-
12
- class SimpleRoutingRoute:
13
- _di = None
14
- _handler = None
15
- _methods = None
16
- _path = None
17
- _path_parts = None
18
- _resource_paths = None
19
- _routes_to_simple_routing = False
20
- _bindings = None
21
- _path_parameter_with_slashes = None
22
- _has_sub_paths = None
23
-
24
- def __init__(self, di):
25
- self._di = di
26
-
27
- def configure(
28
- self,
29
- handler_class,
30
- handler_config,
31
- path=None,
32
- methods=None,
33
- authentication=None,
34
- response_headers=None,
35
- security_headers=None,
36
- path_parameter_with_slashes=None,
37
- bindings=None,
38
- has_sub_paths=True,
39
- ):
40
- if authentication is not None and not handler_config.get("authentication"):
41
- handler_config["authentication"] = authentication
42
- response_headers = response_headers if response_headers is not None else {}
43
- if "response_headers" in handler_config:
44
- if type(handler_config["response_headers"]) != dict:
45
- raise ValueError("Invalid configuration: 'response_headers' must be a dictionary")
46
- response_headers = {**response_headers, **handler_config["response_headers"]}
47
- self._path = path
48
- if handler_config.get("base_url"):
49
- self._path = path.rstrip("/") + "/" + handler_config.get("base_url").lstrip("/")
50
- self._path_parts = self._path.strip("/").split("/") if self._path is not None else []
51
- self._resource_paths = self._extract_resource_paths(self._path_parts)
52
- self._bindings = bindings if bindings else {}
53
- self._path_parameter_with_slashes = path_parameter_with_slashes if path_parameter_with_slashes else []
54
- self._has_sub_paths = has_sub_paths
55
- if methods is not None:
56
- self._methods = [methods.upper()] if isinstance(methods, str) else [met.upper() for met in methods]
57
- sub_handler_config = {
58
- **handler_config,
59
- **{
60
- "base_url": ("/" + path.strip("/")) if path is not None else "/",
61
- },
62
- }
63
- if response_headers:
64
- sub_handler_config["response_headers"] = response_headers
65
- security_headers = security_headers if security_headers is not None else []
66
- if "security_headers" in handler_config:
67
- security_headers = [*security_headers, *handler_config["security_headers"]]
68
- sub_handler_config["security_headers"] = security_headers
69
- self._handler = self._di.build(handler_class, cache=False)
70
- self._handler.configure(sub_handler_config)
71
- self._routes_to_simple_routing = issubclass(handler_class, simple_routing.SimpleRouting)
72
-
73
- def _extract_resource_paths(self, path_parts):
74
- resource_paths = {}
75
- for index, part in enumerate(path_parts):
76
- if not part:
77
- continue
78
- if part[0] != "{":
79
- continue
80
- if part[-1] != "}":
81
- raise ValueError(
82
- f"Invalid route configuration for URL '{path}': section '{part}'"
83
- + " starts with a '{' but does not end with one"
84
- )
85
- match = re.match("{(\\w[\\w\\d_]{0,})\\}", part)
86
- if not match:
87
- raise ValueError(
88
- f"Invalid route configuration for URL '{path}', section '{part}': resource identifiers must start with a letter and contain only letters, numbers, and underscores"
89
- )
90
- resource_paths[index] = match.group(1)
91
- return resource_paths
92
-
93
- def matches(self, full_path, request_method, is_cors=False):
94
- """Returns None if the route doesn't match, or a dictionary with route data for a match.
95
-
96
- You can't just match true/false against the return value, because of the route matches
97
- but has no route data, it returns an empty dictionary. Check explicitly for None
98
- to understand if there was no route match at all.
99
- """
100
- # if we're routing to a simple router then defer to it
101
- incoming = f"Incoming request: [{request_method}] {full_path}. Check against route with url '{self._path}'. Results: "
102
- if not self._methods:
103
- incoming += " configured for any method except OPTIONS"
104
- elif isinstance(self._methods, str):
105
- incoming += f" with method '{self._methods}'"
106
- else:
107
- incoming += " with any of the following methods: " + ", ".join(self._methods)
108
- if self._routes_to_simple_routing:
109
- return self._handler.can_handle(full_path, request_method, is_cors=is_cors)
110
- # If we're routing for CORS then ignore the request method (since it won't match)
111
- if not is_cors and self._methods is not None and request_method not in self._methods:
112
- logger.debug(
113
- f"{incoming} Skipped because this route is not specifically configured for CORS, and this is an OPTIONS request."
114
- )
115
- return None
116
- if self._resource_paths:
117
- results = self._resource_path_match(full_path, self._path_parts, self._resource_paths)
118
- if not results:
119
- logger.debug(f"{incoming} Not a match.")
120
- else:
121
- logger.debug(f"{incoming} Matched and extracted route data: " + json.dumps(results))
122
- return results
123
- if self._path is not None:
124
- full_path = full_path.strip("/")
125
- my_path = self._path.strip("/")
126
- my_path_length = len(my_path)
127
- full_path_length = len(full_path)
128
- if my_path_length > full_path_length:
129
- logger.debug(f"{incoming} Not a match. I'm too long to bother checking.")
130
- return None
131
- if full_path[:my_path_length] != my_path:
132
- logger.debug(f"{incoming} Not a match. Our prefixes just don't match.")
133
- return None
134
- if not self._has_sub_paths and full_path_length > my_path_length:
135
- logger.debug(f"{incoming} Not a match. It's a partial match but I'm not allowed to do that.")
136
- return None
137
- # make sure we don't get confused by partial matches. `user` should match `user/` and `user/5`,
138
- # but it shouldn't match `users/`
139
- if full_path_length > my_path_length and full_path[my_path_length] != "/" and my_path != "":
140
- logger.debug(f"{incoming} Not a match. I only partially matched the URL but not as a sub-directory.")
141
- return None
142
- logger.debug(f"{incoming} Match!")
143
- return {}
144
-
145
- def _resource_path_match(self, requested_path, path_parts, resource_paths):
146
- """Returns None if the route doesn't match, or a dictionary with route data for the match."""
147
- requested_parts = requested_path.strip("/").split("/")
148
- route_data = {}
149
- path_length = len(path_parts)
150
- # it's okay if the requested path is longer than the configured path, since there may
151
- # be sub-routes that we don't know about. However, we won't ever have a match if
152
- # the requested path is shorter than the configured path.
153
- if len(requested_parts) < path_length:
154
- return None
155
- for index in range(path_length):
156
- if index in resource_paths:
157
- if resource_paths[index] in self._path_parameter_with_slashes:
158
- route_data[resource_paths[index]] = urllib.parse.unquote("/".join(requested_parts[index:]))
159
- else:
160
- route_data[resource_paths[index]] = urllib.parse.unquote(requested_parts[index])
161
- else:
162
- if requested_parts[index] != path_parts[index]:
163
- return None
164
- return route_data
165
-
166
- def __call__(self, input_output):
167
- # including calling parameters that came from the route matching
168
- return self._handler(input_output)
169
-
170
- def cors(self, input_output):
171
- # including calling parameters that came from the route matching
172
- return self._handler.cors(input_output)
173
-
174
- def documentation(self):
175
- docs = []
176
- for doc in self._handler.documentation():
177
- if self._methods is not None:
178
- doc.set_request_methods(self._methods)
179
-
180
- # do we have any resource paths to document?
181
- for path_name in self._resource_paths.values():
182
- description = f"The {path_name} to show results for"
183
- doc.add_parameter(URLPath(String(path_name), description=description, required=True))
184
-
185
- docs.append(doc)
186
- return docs
187
-
188
- def documentation_models(self):
189
- return self._handler.documentation_models()
190
-
191
- def documentation_security_schemes(self):
192
- return self._handler.documentation_security_schemes()
@@ -1,136 +0,0 @@
1
- from .list import List
2
- from .. import autodoc
3
- from ..functional import string
4
-
5
-
6
- class SimpleSearch(List):
7
- search_control_columns = ["sort", "direction", "limit"]
8
-
9
- @property
10
- def allowed_request_keys(self):
11
- return [
12
- *self.search_control_columns,
13
- # the list comprehension seems unnecessary, but that is because we require searchable
14
- # columns to be an iterable, but that doesn't guarantee that it converts automatically
15
- # into a list.
16
- *[key for key in self.configuration("searchable_columns")],
17
- ]
18
-
19
- def check_search_in_request_data(self, request_data, query_parameters):
20
- for input_source_label, input_data in [("request body", request_data), ("URL data", query_parameters)]:
21
- for column_name, value in input_data.items():
22
- if column_name in self.search_control_columns:
23
- continue
24
- if column_name not in self.configuration("searchable_columns"):
25
- return f"Invalid request. An invalid search column, '{column_name}', was found in the {input_source_label}"
26
- [column_name, relationship_reference] = self._unpack_column_name_with_reference(column_name)
27
- value_error = self._columns[column_name].check_search_value(
28
- value, relationship_reference=relationship_reference
29
- )
30
- if value_error:
31
- return (
32
- f"Invalid request. {value_error} for search column '{column_name}' in the {input_source_label}"
33
- )
34
-
35
- def configure_models_from_request_data(self, models, request_data, query_parameters, pagination_data):
36
- [models, limit] = super().configure_models_from_request_data(
37
- models, request_data, query_parameters, pagination_data
38
- )
39
- # we can play fast and loose with the possiblity of duplicate keys because our input checking already
40
- # disallows that
41
- for input_source in [request_data, query_parameters]:
42
- for column_name, value in input_source.items():
43
- if column_name in self.search_control_columns:
44
- continue
45
- if column_name == "id":
46
- column_name = self.id_column_name
47
- models = self._add_join(column_name, models)
48
- [column_name, relationship_reference] = self._unpack_column_name_with_reference(column_name)
49
- column = self._columns[column_name]
50
- models = column.add_search(models, value, relationship_reference=relationship_reference)
51
-
52
- return [models, limit]
53
-
54
- def _check_configuration(self, configuration):
55
- super()._check_configuration(configuration)
56
- self._check_columns_in_configuration(configuration, "searchable_columns")
57
-
58
- def _documentation_request(self, request_method, parameters):
59
- nice_model = string.camel_case_to_words(self._model.__class__.__name__)
60
- schema_model_name = string.camel_case_to_snake_case(self._model.__class__.__name__)
61
- data_schema = self.documentation_data_schema()
62
-
63
- authentication = self.configuration("authentication")
64
- standard_error_responses = []
65
- if not getattr(authentication, "is_public", False):
66
- standard_error_responses.append(self.documentation_access_denied_response())
67
- if getattr(authentication, "can_authorize", False):
68
- standard_error_responses.append(self.documentation_unauthorized_response())
69
-
70
- return autodoc.request.Request(
71
- f"Fetch the list of current {nice_model} records",
72
- [
73
- self.documentation_success_response(
74
- autodoc.schema.Array(
75
- self.auto_case_internal_column_name("data"),
76
- autodoc.schema.Object(nice_model, children=data_schema, model_name=schema_model_name),
77
- ),
78
- description=f"The matching {nice_model} records",
79
- include_pagination=True,
80
- ),
81
- *standard_error_responses,
82
- self.documentation_generic_error_response(),
83
- ],
84
- relative_path=self.configuration("base_url"),
85
- request_methods=request_method,
86
- parameters=parameters,
87
- root_properties={
88
- "security": self.documentation_request_security(),
89
- },
90
- )
91
-
92
- def documentation(self):
93
- return [
94
- self._documentation_request(
95
- "GET",
96
- [
97
- *self.documentation_url_pagination_parameters(),
98
- *self.documentation_url_sort_parameters(),
99
- *self.documentation_url_search_parameters(),
100
- ],
101
- ),
102
- self._documentation_request(
103
- "POST",
104
- [
105
- *self.documentation_url_pagination_parameters(),
106
- *self.documentation_url_sort_parameters(),
107
- *self.documentation_json_search_parameters(),
108
- ],
109
- ),
110
- ]
111
-
112
- def documentation_url_search_parameters(self):
113
- docs = []
114
- for column in self._get_searchable_columns().values():
115
- column_doc = column.documentation()
116
- column_doc.name = self.auto_case_internal_column_name(column_doc.name)
117
- docs.append(
118
- autodoc.request.URLParameter(
119
- column_doc,
120
- description=f"Search by {column_doc.name} (via exact match)",
121
- )
122
- )
123
- return docs
124
-
125
- def documentation_json_search_parameters(self):
126
- docs = []
127
- for column in self._get_searchable_columns().values():
128
- column_doc = column.documentation()
129
- column_doc.name = self.auto_case_internal_column_name(column_doc.name)
130
- docs.append(
131
- autodoc.request.JSONBody(
132
- column_doc,
133
- description=f"Search by {column_doc.name} (via exact match)",
134
- )
135
- )
136
- return docs
@@ -1,96 +0,0 @@
1
- from .write import Write
2
- from .exceptions import InputError
3
- from collections import OrderedDict
4
- from ..functional import string
5
-
6
-
7
- class Update(Write):
8
- def __init__(self, di):
9
- super().__init__(di)
10
-
11
- _configuration_defaults = {
12
- "model": None,
13
- "model_class": None,
14
- "columns": None,
15
- "column_overrides": None,
16
- "writeable_columns": None,
17
- "readable_columns": None,
18
- "output_map": None,
19
- "where": [],
20
- "input_error_callable": None,
21
- "include_id_in_path": False,
22
- }
23
-
24
- def get_model_id(self, input_output, input_data):
25
- routing_data = input_output.routing_data()
26
- if self.id_column_name in routing_data:
27
- return routing_data[self.id_column_name]
28
- if "id" in routing_data:
29
- return routing_data["id"]
30
- raise ValueError("I didn't receive the ID in my routing data. I am probably misconfigured.")
31
-
32
- def handle(self, input_output):
33
- input_data = self.request_data(input_output)
34
- model_id = self.get_model_id(input_output, input_data)
35
- if not model_id:
36
- return self.error(input_output, "Not Found", 404)
37
- id_column_name = self.id_column_name
38
- models = self._model.where(f"{id_column_name}={model_id}")
39
- for where in self.configuration("where"):
40
- if type(where) == str:
41
- models = models.where(where)
42
- else:
43
- models = self._di.call_function(
44
- where, models=models, input_output=input_output, routing_data=input_output.routing_data()
45
- )
46
- models = models.where_for_request(
47
- models,
48
- input_output.routing_data(),
49
- input_output.get_authorization_data(),
50
- input_output,
51
- overrides=self.configuration("column_overrides"),
52
- )
53
- authorization = self._configuration.get("authorization", None)
54
- if authorization and hasattr(authorization, "filter_models"):
55
- models = authorization.filter_models(models, input_output.get_authorization_data(), input_output)
56
- model = models.first()
57
- if not model.exists:
58
- return self.error(input_output, "Not Found", 404)
59
- if "id" in input_data:
60
- del input_data["id"]
61
-
62
- input_errors = {
63
- **self._extra_column_errors(input_data),
64
- **self._find_input_errors(model, input_data, input_output),
65
- }
66
- if input_errors:
67
- raise InputError(input_errors)
68
- model.save(input_data, columns=self._get_writeable_columns())
69
-
70
- return self.success(input_output, self._model_as_json(model, input_output))
71
-
72
- def _check_configuration(self, configuration):
73
- super()._check_configuration(configuration)
74
- error_prefix = "Configuration error for %s:" % (self.__class__.__name__)
75
- if "where" in configuration:
76
- if not hasattr(configuration["where"], "__iter__") or type(configuration["where"]) == str:
77
- raise ValueError(
78
- f"{error_prefix} 'where' should be an iterable of coditions or callables "
79
- + ", not "
80
- + str(type(configuration["where"])),
81
- )
82
- for index, where in enumerate(configuration["where"]):
83
- if type(where) != str and not callable(where):
84
- raise ValueError(
85
- f"{error_prefix} 'where' entry should be a string with a condition or a callable that filters models "
86
- + f", but entry #{index+1} is neither of these",
87
- )
88
-
89
- def documentation(self):
90
- nice_model = string.camel_case_to_words(self._model.__class__.__name__)
91
- id_label = "id" if self.configuration("id_column_name") else self.id_column_name
92
- return self._documentation(
93
- description="Update the " + nice_model + " with an " + id_label + " of {" + id_label + "}",
94
- response_description=f"The updated {nice_model}",
95
- include_id_in_path=self.configuration("include_id_in_path"),
96
- )
@@ -1,193 +0,0 @@
1
- from .base import Base
2
- from .input_processing import InputProcessing
3
- from .exceptions import InputError
4
- from collections import OrderedDict
5
- from abc import abstractmethod
6
- from .. import autodoc
7
- from ..functional import string
8
- import inspect
9
-
10
-
11
- class Write(Base, InputProcessing):
12
- _di = None
13
- _model = None
14
- _columns = None
15
- _authentication = None
16
- _writeable_columns = None
17
- _readable_columns = None
18
-
19
- _configuration_defaults = {
20
- "model": None,
21
- "model_class": None,
22
- "columns": None,
23
- "column_overrides": None,
24
- "writeable_columns": None,
25
- "output_map": None,
26
- "readable_columns": None,
27
- "input_error_callable": None,
28
- }
29
-
30
- def __init__(self, di):
31
- super().__init__(di)
32
-
33
- @abstractmethod
34
- def handle(self, input_output):
35
- pass
36
-
37
- def _check_configuration(self, configuration):
38
- super()._check_configuration(configuration)
39
- error_prefix = "Configuration error for %s:" % (self.__class__.__name__)
40
- has_model_class = ("model_class" in configuration) and configuration["model_class"] is not None
41
- has_model = ("model" in configuration) and configuration["model"] is not None
42
- if not has_model and not has_model_class:
43
- raise KeyError(f"{error_prefix} you must specify 'model' or 'model_class'")
44
- if has_model and has_model_class:
45
- raise KeyError(f"{error_prefix} you specified both 'model' and 'model_class', but can only provide one")
46
- if has_model and inspect.isclass(configuration["model"]):
47
- raise ValueError(
48
- "{error_prefix} you must provide a model instance in the 'model' configuration setting, but a class was provided instead"
49
- )
50
- if "input_error_callable" in configuration and not callable(configuration.get("input_error_callable")):
51
- raise ValueError(
52
- "{error_prefix} you must provide a callable for the 'input_error_callable' configuration but the provided value is not callable"
53
- )
54
- self._model = self._di.build(configuration["model_class"]) if has_model_class else configuration["model"]
55
- self._columns = self._model.columns(overrides=configuration.get("column_overrides"))
56
- has_columns = "columns" in configuration and configuration["columns"] is not None
57
- has_writeable = "writeable_columns" in configuration and configuration["writeable_columns"] is not None
58
- has_readable = "readable_columns" in configuration and configuration["readable_columns"] is not None
59
- if not has_columns and not has_writeable:
60
- raise KeyError(f"{error_prefix} you must specify 'columns' OR 'writeable_columns'")
61
- if not has_columns and not has_readable:
62
- raise KeyError(f"{error_prefix} you must specify 'columns' OR 'readable_columns'")
63
- if has_columns and has_writeable:
64
- raise KeyError(f"{error_prefix} you must specify 'columns' OR 'writeable_columns', not both")
65
- if has_columns and has_readable:
66
- raise KeyError(f"{error_prefix} you must specify 'columns' OR 'readable_columns', not both")
67
- if has_writeable and not has_readable:
68
- raise KeyError(f"{error_prefix} you must specify 'readable_columns' if you specify 'writeable_columns'")
69
- if has_readable and not has_writeable:
70
- raise KeyError(f"{error_prefix} you must specify 'writeable_columns' if you specify 'readable_columns'")
71
-
72
- for config_name in ["columns", "writeable_columns", "readable_columns"]:
73
- if config_name not in configuration or configuration[config_name] is not None:
74
- continue
75
- if hasattr(configuration[config_name], "__iter__"):
76
- continue
77
- raise ValueError(
78
- f"{error_prefix} '{config_name}' should be a list of column names "
79
- + f", not {str(type(configuration[config_name]))}"
80
- )
81
-
82
- if has_columns and not configuration["columns"]:
83
- raise KeyError(f"{error_prefix} you must specify at least one column for 'columns'")
84
- if has_writeable and not configuration["writeable_columns"]:
85
- raise KeyError(f"{error_prefix} you must specify at least one column for 'writeable_columns'")
86
- if has_readable and not configuration["readable_columns"]:
87
- raise KeyError(f"{error_prefix} you must specify at least one column for 'readable_columns'")
88
- writeable_columns = configuration["writeable_columns"] if has_writeable else configuration["columns"]
89
- for column_name in writeable_columns:
90
- if column_name not in self._columns:
91
- raise KeyError(f"{error_prefix} specified writeable column '{column_name}' does not exist")
92
- if not self._columns[column_name].is_writeable:
93
- raise KeyError(f"{error_prefix} specified writeable column '{column_name}' is not writeable")
94
- readable_columns = configuration["readable_columns"] if has_readable else configuration["columns"]
95
- for column_name in readable_columns:
96
- if column_name not in self._columns:
97
- raise KeyError(f"{error_prefix} specified readable column '{column_name}' does not exist")
98
-
99
- def _get_rw_columns(self, rw_type):
100
- column_names = self.configuration("columns")
101
- if column_names is None:
102
- column_names = self.configuration(f"{rw_type}_columns")
103
- wr_columns = OrderedDict()
104
- for column_name in column_names:
105
- if column_name not in self._columns:
106
- class_name = self.__class__.__name__
107
- model_class = self._model.__class__.__name__
108
- raise ValueError(
109
- f"Configuration error for {self.__class__.__name__}: handler was configured with {rw_type} "
110
- + f"column '{column_name}' but this column doesn't exist for model {model_class}"
111
- )
112
- wr_columns[column_name] = self._columns[column_name]
113
- return wr_columns
114
-
115
- def _get_readable_columns(self):
116
- if self._readable_columns is None:
117
- self._readable_columns = self._get_rw_columns("readable")
118
- return self._readable_columns
119
-
120
- def documentation_models(self):
121
- schema_model_name = string.camel_case_to_snake_case(self._model.__class__.__name__)
122
-
123
- return {
124
- schema_model_name: autodoc.schema.Object(
125
- "data",
126
- children=self.documentation_data_schema(),
127
- ),
128
- }
129
-
130
- def _documentation(self, description="", response_description="", include_id_in_path=False):
131
- nice_model = string.camel_case_to_words(self._model.__class__.__name__)
132
- data_schema = self.documentation_data_schema()
133
- schema_model_name = string.camel_case_to_snake_case(self._model.__class__.__name__)
134
-
135
- authentication = self.configuration("authentication")
136
- standard_error_responses = [
137
- self.documentation_input_error_response(),
138
- ]
139
- if not getattr(authentication, "is_public", False):
140
- standard_error_responses.append(self.documentation_access_denied_response())
141
- if getattr(authentication, "can_authorize", False):
142
- standard_error_responses.append(self.documentation_unauthorized_response())
143
-
144
- id_label = "id" if self.configuration("id_column_name") else self.id_column_name
145
-
146
- url = self.configuration("base_url")
147
- if include_id_in_path:
148
- url = url.rstrip("/") + "/{" + id_label + "}"
149
-
150
- return [
151
- autodoc.request.Request(
152
- description,
153
- [
154
- self.documentation_success_response(
155
- autodoc.schema.Object(
156
- self.auto_case_internal_column_name("data"),
157
- children=data_schema,
158
- model_name=schema_model_name,
159
- ),
160
- description=description,
161
- ),
162
- *standard_error_responses,
163
- self.documentation_not_found(),
164
- ],
165
- relative_path=url,
166
- parameters=[
167
- *self.documentation_write_parameters(nice_model, include_id_in_path=include_id_in_path),
168
- ],
169
- root_properties={
170
- "security": self.documentation_request_security(),
171
- },
172
- )
173
- ]
174
-
175
- def documentation_write_parameters(self, model_name, include_id_in_path=False):
176
- id_label = "id" if self.configuration("id_column_name") else self.id_column_name
177
- parameters = [
178
- autodoc.request.JSONBody(
179
- column.documentation(name=self.auto_case_column_name(column.name, True)),
180
- description=f"Set '{column.name}' for the {model_name}",
181
- required=column.is_required,
182
- )
183
- for column in self._get_writeable_columns().values()
184
- ]
185
- if include_id_in_path:
186
- parameters.append(
187
- autodoc.request.URLPath(
188
- autodoc.schema.String(id_label),
189
- description=f"The {id_label} of the record in question.",
190
- required=True,
191
- )
192
- )
193
- return parameters