clear-skies 1.22.31__py3-none-any.whl → 2.0.1__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 (345) hide show
  1. {clear_skies-1.22.31.dist-info → clear_skies-2.0.1.dist-info}/METADATA +12 -14
  2. clear_skies-2.0.1.dist-info/RECORD +249 -0
  3. {clear_skies-1.22.31.dist-info → clear_skies-2.0.1.dist-info}/WHEEL +1 -1
  4. clearskies/__init__.py +42 -25
  5. clearskies/action.py +7 -0
  6. clearskies/authentication/__init__.py +8 -41
  7. clearskies/authentication/authentication.py +46 -0
  8. clearskies/authentication/authorization.py +8 -9
  9. clearskies/authentication/authorization_pass_through.py +11 -9
  10. clearskies/authentication/jwks.py +133 -58
  11. clearskies/authentication/public.py +3 -38
  12. clearskies/authentication/secret_bearer.py +516 -54
  13. clearskies/autodoc/formats/oai3_json/__init__.py +1 -1
  14. clearskies/autodoc/formats/oai3_json/oai3_json.py +9 -7
  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 +4 -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 +7 -4
  24. clearskies/autodoc/request/url_parameter.py +4 -6
  25. clearskies/autodoc/request/url_path.py +4 -6
  26. clearskies/autodoc/schema/__init__.py +4 -2
  27. clearskies/autodoc/schema/array.py +5 -6
  28. clearskies/autodoc/schema/boolean.py +4 -10
  29. clearskies/autodoc/schema/date.py +0 -3
  30. clearskies/autodoc/schema/datetime.py +1 -4
  31. clearskies/autodoc/schema/double.py +0 -3
  32. clearskies/autodoc/schema/enum.py +4 -2
  33. clearskies/autodoc/schema/integer.py +4 -9
  34. clearskies/autodoc/schema/long.py +0 -3
  35. clearskies/autodoc/schema/number.py +4 -9
  36. clearskies/autodoc/schema/object.py +5 -7
  37. clearskies/autodoc/schema/password.py +0 -3
  38. clearskies/autodoc/schema/schema.py +11 -0
  39. clearskies/autodoc/schema/string.py +4 -10
  40. clearskies/backends/__init__.py +55 -20
  41. clearskies/backends/api_backend.py +1100 -284
  42. clearskies/backends/backend.py +53 -84
  43. clearskies/backends/cursor_backend.py +236 -186
  44. clearskies/backends/memory_backend.py +519 -226
  45. clearskies/backends/secrets_backend.py +75 -31
  46. clearskies/column.py +1229 -0
  47. clearskies/columns/__init__.py +71 -0
  48. clearskies/columns/audit.py +205 -0
  49. clearskies/columns/belongs_to_id.py +483 -0
  50. clearskies/columns/belongs_to_model.py +128 -0
  51. clearskies/columns/belongs_to_self.py +105 -0
  52. clearskies/columns/boolean.py +109 -0
  53. clearskies/columns/category_tree.py +275 -0
  54. clearskies/columns/category_tree_ancestors.py +51 -0
  55. clearskies/columns/category_tree_children.py +127 -0
  56. clearskies/columns/category_tree_descendants.py +48 -0
  57. clearskies/columns/created.py +94 -0
  58. clearskies/columns/created_by_authorization_data.py +116 -0
  59. clearskies/columns/created_by_header.py +99 -0
  60. clearskies/columns/created_by_ip.py +92 -0
  61. clearskies/columns/created_by_routing_data.py +96 -0
  62. clearskies/columns/created_by_user_agent.py +92 -0
  63. clearskies/columns/date.py +230 -0
  64. clearskies/columns/datetime.py +278 -0
  65. clearskies/columns/email.py +76 -0
  66. clearskies/columns/float.py +149 -0
  67. clearskies/columns/has_many.py +505 -0
  68. clearskies/columns/has_many_self.py +56 -0
  69. clearskies/columns/has_one.py +14 -0
  70. clearskies/columns/integer.py +156 -0
  71. clearskies/columns/json.py +122 -0
  72. clearskies/columns/many_to_many_ids.py +333 -0
  73. clearskies/columns/many_to_many_ids_with_data.py +270 -0
  74. clearskies/columns/many_to_many_models.py +154 -0
  75. clearskies/columns/many_to_many_pivots.py +133 -0
  76. clearskies/columns/phone.py +158 -0
  77. clearskies/columns/select.py +91 -0
  78. clearskies/columns/string.py +98 -0
  79. clearskies/columns/timestamp.py +160 -0
  80. clearskies/columns/updated.py +110 -0
  81. clearskies/columns/uuid.py +86 -0
  82. clearskies/configs/README.md +105 -0
  83. clearskies/configs/__init__.py +162 -0
  84. clearskies/configs/actions.py +43 -0
  85. clearskies/configs/any.py +13 -0
  86. clearskies/configs/any_dict.py +22 -0
  87. clearskies/configs/any_dict_or_callable.py +23 -0
  88. clearskies/configs/authentication.py +23 -0
  89. clearskies/configs/authorization.py +23 -0
  90. clearskies/configs/boolean.py +16 -0
  91. clearskies/configs/boolean_or_callable.py +18 -0
  92. clearskies/configs/callable_config.py +18 -0
  93. clearskies/configs/columns.py +34 -0
  94. clearskies/configs/conditions.py +30 -0
  95. clearskies/configs/config.py +24 -0
  96. clearskies/configs/datetime.py +18 -0
  97. clearskies/configs/datetime_or_callable.py +19 -0
  98. clearskies/configs/endpoint.py +23 -0
  99. clearskies/configs/endpoint_list.py +28 -0
  100. clearskies/configs/float.py +16 -0
  101. clearskies/configs/float_or_callable.py +18 -0
  102. clearskies/configs/integer.py +16 -0
  103. clearskies/configs/integer_or_callable.py +18 -0
  104. clearskies/configs/joins.py +30 -0
  105. clearskies/configs/list_any_dict.py +30 -0
  106. clearskies/configs/list_any_dict_or_callable.py +31 -0
  107. clearskies/configs/model_class.py +35 -0
  108. clearskies/configs/model_column.py +65 -0
  109. clearskies/configs/model_columns.py +56 -0
  110. clearskies/configs/model_destination_name.py +25 -0
  111. clearskies/configs/model_to_id_column.py +43 -0
  112. clearskies/configs/readable_model_column.py +9 -0
  113. clearskies/configs/readable_model_columns.py +9 -0
  114. clearskies/configs/schema.py +23 -0
  115. clearskies/configs/searchable_model_columns.py +9 -0
  116. clearskies/configs/security_headers.py +39 -0
  117. clearskies/configs/select.py +26 -0
  118. clearskies/configs/select_list.py +47 -0
  119. clearskies/configs/string.py +29 -0
  120. clearskies/configs/string_dict.py +32 -0
  121. clearskies/configs/string_list.py +32 -0
  122. clearskies/configs/string_list_or_callable.py +35 -0
  123. clearskies/configs/string_or_callable.py +18 -0
  124. clearskies/configs/timedelta.py +18 -0
  125. clearskies/configs/timezone.py +18 -0
  126. clearskies/configs/url.py +23 -0
  127. clearskies/configs/validators.py +45 -0
  128. clearskies/configs/writeable_model_column.py +9 -0
  129. clearskies/configs/writeable_model_columns.py +9 -0
  130. clearskies/configurable.py +76 -0
  131. clearskies/contexts/__init__.py +8 -8
  132. clearskies/contexts/cli.py +8 -41
  133. clearskies/contexts/context.py +91 -56
  134. clearskies/contexts/wsgi.py +16 -29
  135. clearskies/contexts/wsgi_ref.py +53 -0
  136. clearskies/di/__init__.py +10 -7
  137. clearskies/di/additional_config.py +115 -4
  138. clearskies/di/additional_config_auto_import.py +12 -0
  139. clearskies/di/di.py +742 -121
  140. clearskies/di/inject/__init__.py +23 -0
  141. clearskies/di/inject/by_class.py +21 -0
  142. clearskies/di/inject/by_name.py +18 -0
  143. clearskies/di/inject/di.py +13 -0
  144. clearskies/di/inject/environment.py +14 -0
  145. clearskies/di/inject/input_output.py +20 -0
  146. clearskies/di/inject/now.py +13 -0
  147. clearskies/di/inject/requests.py +13 -0
  148. clearskies/di/inject/secrets.py +14 -0
  149. clearskies/di/inject/utcnow.py +13 -0
  150. clearskies/di/inject/uuid.py +15 -0
  151. clearskies/di/injectable.py +29 -0
  152. clearskies/di/injectable_properties.py +131 -0
  153. clearskies/end.py +183 -0
  154. clearskies/endpoint.py +1310 -0
  155. clearskies/endpoint_group.py +310 -0
  156. clearskies/endpoints/__init__.py +23 -0
  157. clearskies/endpoints/advanced_search.py +526 -0
  158. clearskies/endpoints/callable.py +388 -0
  159. clearskies/endpoints/create.py +202 -0
  160. clearskies/endpoints/delete.py +139 -0
  161. clearskies/endpoints/get.py +275 -0
  162. clearskies/endpoints/health_check.py +181 -0
  163. clearskies/endpoints/list.py +573 -0
  164. clearskies/endpoints/restful_api.py +427 -0
  165. clearskies/endpoints/simple_search.py +286 -0
  166. clearskies/endpoints/update.py +190 -0
  167. clearskies/environment.py +5 -3
  168. clearskies/exceptions/__init__.py +17 -0
  169. clearskies/{handlers/exceptions/input_error.py → exceptions/input_errors.py} +1 -1
  170. clearskies/exceptions/moved_permanently.py +3 -0
  171. clearskies/exceptions/moved_temporarily.py +3 -0
  172. clearskies/exceptions/not_found.py +2 -0
  173. clearskies/functional/__init__.py +2 -2
  174. clearskies/functional/routing.py +92 -0
  175. clearskies/functional/string.py +19 -11
  176. clearskies/functional/validations.py +61 -9
  177. clearskies/input_outputs/__init__.py +9 -7
  178. clearskies/input_outputs/cli.py +130 -142
  179. clearskies/input_outputs/exceptions/__init__.py +1 -1
  180. clearskies/input_outputs/headers.py +45 -0
  181. clearskies/input_outputs/input_output.py +91 -122
  182. clearskies/input_outputs/programmatic.py +69 -0
  183. clearskies/input_outputs/wsgi.py +23 -38
  184. clearskies/model.py +984 -183
  185. clearskies/parameters_to_properties.py +31 -0
  186. clearskies/query/__init__.py +12 -0
  187. clearskies/query/condition.py +223 -0
  188. clearskies/query/join.py +136 -0
  189. clearskies/query/query.py +196 -0
  190. clearskies/query/sort.py +27 -0
  191. clearskies/schema.py +82 -0
  192. clearskies/secrets/__init__.py +3 -31
  193. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer.py +15 -4
  194. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +11 -5
  195. clearskies/secrets/akeyless.py +88 -147
  196. clearskies/secrets/secrets.py +8 -8
  197. clearskies/security_header.py +15 -0
  198. clearskies/security_headers/__init__.py +8 -8
  199. clearskies/security_headers/cache_control.py +47 -110
  200. clearskies/security_headers/cors.py +40 -95
  201. clearskies/security_headers/csp.py +76 -151
  202. clearskies/security_headers/hsts.py +14 -16
  203. clearskies/test_base.py +8 -0
  204. clearskies/typing.py +11 -0
  205. clearskies/validator.py +37 -0
  206. clearskies/validators/__init__.py +33 -0
  207. clearskies/validators/after_column.py +62 -0
  208. clearskies/validators/before_column.py +13 -0
  209. clearskies/validators/in_the_future.py +32 -0
  210. clearskies/validators/in_the_future_at_least.py +11 -0
  211. clearskies/validators/in_the_future_at_most.py +10 -0
  212. clearskies/validators/in_the_past.py +32 -0
  213. clearskies/validators/in_the_past_at_least.py +10 -0
  214. clearskies/validators/in_the_past_at_most.py +10 -0
  215. clearskies/validators/maximum_length.py +26 -0
  216. clearskies/validators/maximum_value.py +29 -0
  217. clearskies/validators/minimum_length.py +26 -0
  218. clearskies/validators/minimum_value.py +29 -0
  219. clearskies/validators/required.py +35 -0
  220. clearskies/validators/timedelta.py +59 -0
  221. clearskies/validators/unique.py +31 -0
  222. clear_skies-1.22.31.dist-info/RECORD +0 -214
  223. clearskies/application.py +0 -29
  224. clearskies/authentication/auth0_jwks.py +0 -118
  225. clearskies/authentication/auth_exception.py +0 -2
  226. clearskies/authentication/jwks_jwcrypto.py +0 -51
  227. clearskies/backends/api_get_only_backend.py +0 -48
  228. clearskies/backends/example_backend.py +0 -43
  229. clearskies/backends/file_backend.py +0 -48
  230. clearskies/backends/json_backend.py +0 -7
  231. clearskies/backends/restful_api_advanced_search_backend.py +0 -103
  232. clearskies/binding_config.py +0 -16
  233. clearskies/column_types/__init__.py +0 -203
  234. clearskies/column_types/audit.py +0 -249
  235. clearskies/column_types/belongs_to.py +0 -271
  236. clearskies/column_types/boolean.py +0 -60
  237. clearskies/column_types/category_tree.py +0 -304
  238. clearskies/column_types/column.py +0 -373
  239. clearskies/column_types/created.py +0 -26
  240. clearskies/column_types/created_by_authorization_data.py +0 -26
  241. clearskies/column_types/created_by_header.py +0 -24
  242. clearskies/column_types/created_by_ip.py +0 -17
  243. clearskies/column_types/created_by_routing_data.py +0 -25
  244. clearskies/column_types/created_by_user_agent.py +0 -17
  245. clearskies/column_types/created_micro.py +0 -26
  246. clearskies/column_types/datetime.py +0 -109
  247. clearskies/column_types/datetime_micro.py +0 -12
  248. clearskies/column_types/email.py +0 -18
  249. clearskies/column_types/float.py +0 -43
  250. clearskies/column_types/has_many.py +0 -179
  251. clearskies/column_types/has_one.py +0 -60
  252. clearskies/column_types/integer.py +0 -41
  253. clearskies/column_types/json.py +0 -25
  254. clearskies/column_types/many_to_many.py +0 -278
  255. clearskies/column_types/many_to_many_with_data.py +0 -162
  256. clearskies/column_types/phone.py +0 -48
  257. clearskies/column_types/select.py +0 -11
  258. clearskies/column_types/string.py +0 -24
  259. clearskies/column_types/timestamp.py +0 -73
  260. clearskies/column_types/updated.py +0 -26
  261. clearskies/column_types/updated_micro.py +0 -26
  262. clearskies/column_types/uuid.py +0 -25
  263. clearskies/columns.py +0 -123
  264. clearskies/condition_parser.py +0 -172
  265. clearskies/contexts/build_context.py +0 -54
  266. clearskies/contexts/convert_to_application.py +0 -190
  267. clearskies/contexts/extract_handler.py +0 -37
  268. clearskies/contexts/test.py +0 -94
  269. clearskies/decorators/__init__.py +0 -41
  270. clearskies/decorators/allow_non_json_bodies.py +0 -9
  271. clearskies/decorators/auth0_jwks.py +0 -22
  272. clearskies/decorators/authorization.py +0 -10
  273. clearskies/decorators/binding_classes.py +0 -9
  274. clearskies/decorators/binding_modules.py +0 -9
  275. clearskies/decorators/bindings.py +0 -9
  276. clearskies/decorators/create.py +0 -10
  277. clearskies/decorators/delete.py +0 -10
  278. clearskies/decorators/docs.py +0 -14
  279. clearskies/decorators/get.py +0 -10
  280. clearskies/decorators/jwks.py +0 -26
  281. clearskies/decorators/merge.py +0 -124
  282. clearskies/decorators/patch.py +0 -10
  283. clearskies/decorators/post.py +0 -10
  284. clearskies/decorators/public.py +0 -11
  285. clearskies/decorators/response_headers.py +0 -10
  286. clearskies/decorators/return_raw_response.py +0 -9
  287. clearskies/decorators/schema.py +0 -10
  288. clearskies/decorators/secret_bearer.py +0 -24
  289. clearskies/decorators/security_headers.py +0 -10
  290. clearskies/di/standard_dependencies.py +0 -151
  291. clearskies/handlers/__init__.py +0 -41
  292. clearskies/handlers/advanced_search.py +0 -271
  293. clearskies/handlers/base.py +0 -479
  294. clearskies/handlers/callable.py +0 -192
  295. clearskies/handlers/create.py +0 -35
  296. clearskies/handlers/crud_by_method.py +0 -18
  297. clearskies/handlers/database_connector.py +0 -32
  298. clearskies/handlers/delete.py +0 -61
  299. clearskies/handlers/exceptions/__init__.py +0 -5
  300. clearskies/handlers/exceptions/not_found.py +0 -3
  301. clearskies/handlers/get.py +0 -156
  302. clearskies/handlers/health_check.py +0 -59
  303. clearskies/handlers/input_processing.py +0 -79
  304. clearskies/handlers/list.py +0 -530
  305. clearskies/handlers/mygrations.py +0 -82
  306. clearskies/handlers/request_method_routing.py +0 -47
  307. clearskies/handlers/restful_api.py +0 -218
  308. clearskies/handlers/routing.py +0 -62
  309. clearskies/handlers/schema_helper.py +0 -128
  310. clearskies/handlers/simple_routing.py +0 -206
  311. clearskies/handlers/simple_routing_route.py +0 -197
  312. clearskies/handlers/simple_search.py +0 -136
  313. clearskies/handlers/update.py +0 -102
  314. clearskies/handlers/write.py +0 -193
  315. clearskies/input_requirements/__init__.py +0 -78
  316. clearskies/input_requirements/after.py +0 -36
  317. clearskies/input_requirements/before.py +0 -36
  318. clearskies/input_requirements/in_the_future_at_least.py +0 -19
  319. clearskies/input_requirements/in_the_future_at_most.py +0 -19
  320. clearskies/input_requirements/in_the_past_at_least.py +0 -19
  321. clearskies/input_requirements/in_the_past_at_most.py +0 -19
  322. clearskies/input_requirements/maximum_length.py +0 -19
  323. clearskies/input_requirements/maximum_value.py +0 -19
  324. clearskies/input_requirements/minimum_length.py +0 -22
  325. clearskies/input_requirements/minimum_value.py +0 -19
  326. clearskies/input_requirements/required.py +0 -23
  327. clearskies/input_requirements/requirement.py +0 -25
  328. clearskies/input_requirements/time_delta.py +0 -38
  329. clearskies/input_requirements/unique.py +0 -18
  330. clearskies/mocks/__init__.py +0 -7
  331. clearskies/mocks/input_output.py +0 -124
  332. clearskies/mocks/models.py +0 -142
  333. clearskies/models.py +0 -350
  334. clearskies/security_headers/base.py +0 -12
  335. clearskies/tests/simple_api/models/__init__.py +0 -2
  336. clearskies/tests/simple_api/models/status.py +0 -23
  337. clearskies/tests/simple_api/models/user.py +0 -21
  338. clearskies/tests/simple_api/users_api.py +0 -64
  339. {clear_skies-1.22.31.dist-info → clear_skies-2.0.1.dist-info}/LICENSE +0 -0
  340. /clearskies/{contexts/bash.py → autodoc/py.typed} +0 -0
  341. /clearskies/{handlers/exceptions → exceptions}/authentication.py +0 -0
  342. /clearskies/{handlers/exceptions → exceptions}/authorization.py +0 -0
  343. /clearskies/{handlers/exceptions → exceptions}/client_error.py +0 -0
  344. /clearskies/{tests/__init__.py → input_outputs/py.typed} +0 -0
  345. /clearskies/{tests/simple_api/__init__.py → py.typed} +0 -0
@@ -1,100 +1,64 @@
1
- from abc import ABC, abstractmethod
2
- from collections import OrderedDict
3
- from ..handlers.exceptions import ClientError
4
1
  import json
2
+ from abc import ABC, abstractmethod
3
+ from typing import Any
4
+ from urllib.parse import parse_qs
5
5
 
6
+ import clearskies.configurable
7
+ import clearskies.typing
8
+ from clearskies.configs import AnyDict, StringDict
9
+ from clearskies.exceptions import ClientError
6
10
 
7
- class InputOutput(ABC):
8
- _response_headers = None
9
- _body_as_json = None
10
- _body_loaded_as_json = False
11
- _routing_data = None
12
- _authorization_data = None
13
-
14
- @abstractmethod
15
- def respond(self, body, status_code=200):
16
- pass
17
-
18
- def error(self, body):
19
- return self.respond(body, 400)
11
+ from .headers import Headers
20
12
 
21
- def success(self, body):
22
- return self.respond(body)
23
13
 
24
- def configure(self):
25
- pass
14
+ class InputOutput(ABC, clearskies.configurable.Configurable):
15
+ """Manage the request and response to the client."""
26
16
 
27
- def has_header(self, key):
28
- if self._response_headers is None:
29
- return False
30
- return key.upper() in self._response_headers
17
+ response_headers: Headers = None # type: ignore
18
+ request_headers: Headers = None # type: ignore
19
+ query_parameters = clearskies.configs.AnyDict(default={})
20
+ routing_data = clearskies.configs.StringDict(default={})
21
+ authorization_data = clearskies.configs.AnyDict(default={})
31
22
 
32
- def set_header(self, key, value):
33
- if self._response_headers is None:
34
- self._response_headers = OrderedDict()
35
- self._response_headers[key.upper()] = value
23
+ _body_as_json: dict[str, Any] | list[Any] | None = {}
24
+ _body_loaded_as_json = False
36
25
 
37
- def clear_header(self, key):
38
- if self._response_headers is None:
39
- return
40
- if self.has_header(key):
41
- del self._response_headers[key.upper()]
26
+ def __init__(self):
27
+ self.response_headers = Headers()
28
+ self.request_headers = Headers(self.get_request_headers())
29
+ self.query_parameters = {key: val[0] for (key, val) in parse_qs(self.get_query_string()).items()}
30
+ self.authorization_data = {}
31
+ self.routing_data = {}
32
+ self.finalize_and_validate_configuration()
42
33
 
43
- def clear_headers(self, key):
44
- self._response_headers = None
34
+ @abstractmethod
35
+ def respond(self, body: clearskies.typing.response, status_code: int = 200) -> Any:
36
+ """
37
+ Pass along a response to the client.
45
38
 
46
- def set_headers(self, headers):
47
- for key, value in headers.items():
48
- self.set_header(key, value)
39
+ Accepts a string, bytes, dictionary, or list. If a content type has not been set then it will automatically
40
+ be set to application/json
41
+ """
42
+ pass
49
43
 
50
44
  @abstractmethod
51
- def get_body(self):
45
+ def get_body(self) -> str:
46
+ """Return the raw body set by the client."""
52
47
  pass
53
48
 
54
49
  @abstractmethod
55
- def has_body(self):
50
+ def has_body(self) -> bool:
51
+ """Whether or not the request included a body."""
56
52
  pass
57
53
 
58
- def routing_data(self):
59
- return self._routing_data if self._routing_data is not None else {}
60
-
61
- def set_routing_data(self, data):
62
- self._routing_data = data
63
-
64
- def add_routing_data(self, key, value=None):
65
- if self._routing_data is None:
66
- self._routing_data = {}
67
- if type(key) == dict:
68
- self._routing_data = {**self._routing_data, **key}
69
- else:
70
- self._routing_data[key] = value
71
-
72
- def request_data(self, required=True, allow_non_json_bodies=False):
73
- request_data = self.json_body(False, allow_non_json_bodies=allow_non_json_bodies)
74
- if not request_data:
75
- if self.has_body() and not allow_non_json_bodies:
76
- raise ClientError("Request body was not valid JSON")
77
- request_data = {}
78
- return request_data
79
-
80
- def json_body(self, required=True, allow_non_json_bodies=False):
81
- json = self._get_json_body()
82
- # if we get None then either the body was not JSON or was empty.
83
- # If it is required then we have an exception either way. If it is not required
84
- # then we have an exception if a body was provided but it was not JSON. We can check for this
85
- # if json is None and there is an actual request body. If json is none, the body is empty,
86
- # and it was not required, then we can just return None
87
- if json is None:
88
- if required or (self.has_body() and not allow_non_json_bodies):
89
- raise ClientError("Request body was not valid JSON")
90
- return json
91
-
92
- def _get_json_body(self):
54
+ @property
55
+ def request_data(self) -> dict[str, Any] | list[Any] | None:
56
+ """Return the data from the request body, assuming it is JSON."""
93
57
  if not self._body_loaded_as_json:
94
- if self.get_body() is None:
58
+ self._body_loaded_as_json = True
59
+ if not self.has_body():
95
60
  self._body_as_json = None
96
61
  else:
97
- self._body_loaded_as_json = True
98
62
  try:
99
63
  self._body_as_json = json.loads(self.get_body())
100
64
  except json.JSONDecodeError:
@@ -102,68 +66,73 @@ class InputOutput(ABC):
102
66
  return self._body_as_json
103
67
 
104
68
  @abstractmethod
105
- def get_request_method(self):
106
- pass
107
-
108
- @abstractmethod
109
- def get_script_name(self):
110
- pass
111
-
112
- @abstractmethod
113
- def get_path_info(self):
114
- pass
115
-
116
- def get_full_path(self):
117
- path_info = self.get_path_info()
118
- script_name = self.get_script_name()
119
- if not path_info or path_info[0] != "/":
120
- path_info = f"/{path_info}"
121
- return f"{path_info}{script_name}".replace("//", "/")
122
-
123
- @abstractmethod
124
- def get_query_string(self):
69
+ def get_request_method(self) -> str:
70
+ """Return the request method set by the client."""
125
71
  pass
126
72
 
127
73
  @abstractmethod
128
- def get_content_type(self):
74
+ def get_script_name(self) -> str:
75
+ """Return the script name, e.g. the path requested."""
129
76
  pass
130
77
 
131
78
  @abstractmethod
132
- def get_protocol(self):
79
+ def get_path_info(self) -> str:
80
+ """Return the path info for the request."""
133
81
  pass
134
82
 
135
83
  @abstractmethod
136
- def has_request_header(self, header_name):
84
+ def get_query_string(self) -> str:
85
+ """Return the full query string for the request (everything after the first question mark in the document URL)."""
137
86
  pass
138
87
 
139
88
  @abstractmethod
140
- def get_request_header(self, header_name, silent=True):
89
+ def get_client_ip(self):
141
90
  pass
142
91
 
143
- def _parse_query_parameters(self):
144
- if self._query_parameters is None:
145
- self._query_parameters = {
146
- key: val[0] if len(val) == 1 else val
147
- for (key, val) in urllib.parse.parse_qs(self.get_query_string()).items()
148
- }
149
-
150
- def get_query_parameter(self, key):
151
- self._parse_query_parameters()
152
- return self._query_parameters[key] if key in self._query_parameters else None
153
-
154
- def get_query_parameters(self):
155
- self._parse_query_parameters()
156
- return self._query_parameters
157
-
158
92
  @abstractmethod
159
- def get_client_ip(self):
93
+ def get_request_headers(self) -> dict[str, str]:
160
94
  pass
161
95
 
162
- def set_authorization_data(self, data):
163
- self._authorization_data = data
164
-
165
- def get_authorization_data(self):
166
- return self._authorization_data if self._authorization_data else {}
96
+ def get_full_path(self) -> str:
97
+ """Return the full path requested by the client."""
98
+ path_info = self.get_path_info()
99
+ script_name = self.get_script_name()
100
+ if not path_info or path_info[0] != "/":
101
+ path_info = f"/{path_info}"
102
+ return f"{path_info}{script_name}".replace("//", "/")
167
103
 
168
104
  def context_specifics(self):
169
105
  return {}
106
+
107
+ def get_context_for_callables(self) -> dict[str, Any]:
108
+ """
109
+ Return a dictionary with various important parts of the request that are passed along to user-defined functions.
110
+
111
+ It's common to make various aspects of an incoming request available to user-defined functions that are
112
+ attached to clearskies hooks everywhere. This function centralizes the definition of what aspects of
113
+ the reequest shouuld be passed along to callables in this case. When this is in use it typically
114
+ looks like this:
115
+
116
+ di.call_function(some_function, **input_output.get_context_for_callables())
117
+
118
+ And this function returns a dictionary with the following values:
119
+
120
+ | Key | Type | Ref | Value |
121
+ |--------------------|----------------------------------|---------------------------------|---------------------------------------------------------------------------------|
122
+ | routing_data | dict[str, str] | input_output.routing_data | A dictionary of data extracted from URL path parameters. |
123
+ | authorization_data | dict[str, Any] | input_output.authorization_data | A dictionary containing the authorization data set by the authentication method |
124
+ | request_data | dict[str, Any] | None | input_output.request_data | The data sent along with the request (assuming a JSON request body) |
125
+ | query_parameters | dict[str, Any] | input_output.query_parameters | The query parameters |
126
+ | request_headers | clearskies.input_outputs.Headers | input_output.request_headers | The request headers sent by the client |
127
+ | **routing_data | string | **input_output.routing_data | The routing data is unpacked so keys can be fetched directly |
128
+ """
129
+ return {
130
+ **self.routing_data,
131
+ **{
132
+ "routing_data": self.routing_data,
133
+ "authorization_data": self.authorization_data,
134
+ "request_data": self.request_data,
135
+ "request_headers": self.request_headers,
136
+ "query_parameters": self.query_parameters,
137
+ },
138
+ }
@@ -0,0 +1,69 @@
1
+ from typing import Any
2
+
3
+ from clearskies.input_outputs.input_output import InputOutput
4
+
5
+ from .headers import Headers
6
+
7
+
8
+ class Programmatic(InputOutput):
9
+ _body: str | dict[str, Any] | list[Any] = ""
10
+ _request_method: str = ""
11
+ _request_headers: dict[str, Any] = {}
12
+ url: str = ""
13
+
14
+ def __init__(
15
+ self,
16
+ url: str = "",
17
+ request_method: str = "GET",
18
+ body: str | dict[str, Any] | list[Any] = "",
19
+ query_parameters: dict[str, Any] = {},
20
+ request_headers: dict[str, str] = {},
21
+ ):
22
+ self.url = url
23
+ self._request_headers = {**request_headers}
24
+ self._body_loaded_as_json = True
25
+ self._body_as_json = None
26
+ self._request_method = request_method
27
+ if body:
28
+ self._body = body
29
+ if isinstance(body, dict) or isinstance(body, list):
30
+ self._body_as_json = body
31
+
32
+ super().__init__()
33
+ self.query_parameters = {**query_parameters}
34
+
35
+ def respond(self, response, status_code=200):
36
+ return (status_code, response, self.response_headers)
37
+
38
+ def get_script_name(self):
39
+ return self.url
40
+
41
+ def get_path_info(self):
42
+ return self.url
43
+
44
+ def get_full_path(self):
45
+ return self.url
46
+
47
+ def get_request_method(self):
48
+ return self._request_method
49
+
50
+ def has_body(self):
51
+ return bool(self._body)
52
+
53
+ def get_body(self):
54
+ if not self.has_body():
55
+ return ""
56
+
57
+ return self._body
58
+
59
+ def context_specifics(self):
60
+ return {}
61
+
62
+ def get_client_ip(self):
63
+ return "127.0.0.1"
64
+
65
+ def get_query_string(self):
66
+ return ""
67
+
68
+ def get_request_headers(self):
69
+ return self._request_headers
@@ -1,14 +1,16 @@
1
- from .input_output import InputOutput
2
- import urllib, urllib.parse
3
1
  import json
2
+ import urllib
3
+ import urllib.parse
4
+ from typing import Callable
5
+
6
+ from .input_output import InputOutput
4
7
 
5
8
 
6
- class WSGI(InputOutput):
7
- _environment = None
8
- _start_response = None
9
- _request_headers = None
10
- _cached_body = None
11
- _query_parameters = None
9
+ class Wsgi(InputOutput):
10
+ _environment: dict[str, str] = {}
11
+ _start_response: Callable = None # type: ignore
12
+ _request_headers: dict[str, str] = {}
13
+ _cached_body: str | None = None
12
14
 
13
15
  def __init__(self, environment, start_response):
14
16
  self._environment = environment
@@ -17,15 +19,16 @@ class WSGI(InputOutput):
17
19
  for key, value in self._environment.items():
18
20
  if key.upper()[0:5] == "HTTP_":
19
21
  self._request_headers[key[5:].lower()] = value
22
+ super().__init__()
20
23
 
21
24
  def _from_environment(self, key):
22
25
  return self._environment[key] if key in self._environment else ""
23
26
 
24
27
  def respond(self, body, status_code=200):
25
- if not self.has_header("content-type"):
26
- self.set_header("content-type", "application/json; charset=UTF-8")
28
+ if "content-type" not in self.response_headers:
29
+ self.response_headers.content_type = "application/json; charset=UTF-8"
27
30
 
28
- self._start_response(f"{status_code} Ok", [header for header in self._response_headers.items()])
31
+ self._start_response(f"{status_code} Ok", [header for header in self.response_headers.items()]) # type: ignore
29
32
  if type(body) == bytes:
30
33
  final_body = body
31
34
  elif type(body) == str:
@@ -35,11 +38,15 @@ class WSGI(InputOutput):
35
38
  return [final_body]
36
39
 
37
40
  def has_body(self):
38
- return bool(self.get_body())
41
+ return bool(self._from_environment("CONTENT_LENGTH"))
39
42
 
40
43
  def get_body(self):
41
44
  if self._cached_body is None:
42
- self._cached_body = self._from_environment("wsgi.input").read().decode("utf-8")
45
+ self._cached_body = (
46
+ self._from_environment("wsgi.input").read(int(self._from_environment("CONTENT_LENGTH"))).decode("utf-8")
47
+ if self._from_environment("CONTENT_LENGTH")
48
+ else ""
49
+ )
43
50
  return self._cached_body
44
51
 
45
52
  def get_request_method(self):
@@ -60,33 +67,11 @@ class WSGI(InputOutput):
60
67
  def get_protocol(self):
61
68
  return self._from_environment("wsgi.url_scheme").lower()
62
69
 
63
- def has_request_header(self, header_name):
64
- return header_name.lower() in self._request_headers
65
-
66
- def get_request_header(self, header_name, silent=False):
67
- if not header_name.lower() in self._request_headers:
68
- if not silent:
69
- raise KeyError(f"HTTP header '{header_name}' was not found in request")
70
- return ""
71
- return self._request_headers[header_name.lower()]
72
-
73
- def _parse_query_parameters(self):
74
- if self._query_parameters is None:
75
- self._query_parameters = {
76
- key: val[0] if len(val) == 1 else val
77
- for (key, val) in urllib.parse.parse_qs(self.get_query_string()).items()
78
- }
79
-
80
- def get_query_parameter(self, key):
81
- self._parse_query_parameters()
82
- return self._query_parameters[key] if key in self._query_parameters else []
83
-
84
- def get_query_parameters(self):
85
- self._parse_query_parameters()
86
- return self._query_parameters
87
-
88
70
  def context_specifics(self):
89
71
  return {"wsgi_environment": self._environment}
90
72
 
91
73
  def get_client_ip(self):
92
74
  return self._environment.get("REMOTE_ADDR")
75
+
76
+ def get_request_headers(self):
77
+ return self._request_headers