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,16 +1,14 @@
1
- import re
2
1
  import datetime
2
+ import re
3
3
 
4
4
 
5
5
  def camel_case_to_snake_case(string: str) -> str:
6
- """
7
- Converts a title/camel case string (MyString|myString) to snake case (my_string)
8
- """
6
+ """Convert a title/camel case string (MyString|myString) to snake case (my_string)."""
9
7
  return re.sub("([a-z0-9])([A-Z])", r"\1_\2", re.sub("(.)([A-Z][a-z]+)", r"\1_\2", string)).lower()
10
8
 
11
9
 
12
10
  def camel_case_to_title_case(string):
13
- return camel_case_to_words.title()
11
+ return camel_case_to_words(string).title().replace(" ", "")
14
12
 
15
13
 
16
14
  def camel_case_to_words(string):
@@ -19,10 +17,12 @@ def camel_case_to_words(string):
19
17
  return string
20
18
 
21
19
 
20
+ def camel_case_to_nice(string):
21
+ return camel_case_to_words(string).title()
22
+
23
+
22
24
  def title_case_to_snake_case(string: str) -> str:
23
- """
24
- Converts a title case string (MyString) to snake case (my_string)
25
- """
25
+ """Convert a title case string (MyString) to snake case (my_string)."""
26
26
  return camel_case_to_snake_case(string)
27
27
 
28
28
 
@@ -34,9 +34,13 @@ def title_case_to_camel_case(string: str) -> str:
34
34
  return string[0].lower() + string[1:]
35
35
 
36
36
 
37
+ def title_case_to_nice(string: str) -> str:
38
+ return camel_case_to_nice(string)
39
+
40
+
37
41
  def snake_case_to_title_case(string: str) -> str:
38
42
  """
39
- Converts a snake case string (my_string) to title case (MyString)
43
+ Convert a snake case string (my_string) to title case (MyString).
40
44
 
41
45
  Note this is sometimes ambiguous. Consider:
42
46
 
@@ -50,7 +54,7 @@ def snake_case_to_title_case(string: str) -> str:
50
54
 
51
55
  def snake_case_to_camel_case(string: str) -> str:
52
56
  """
53
- Converts a snake case string (my_string) to camel case (myString)
57
+ Convert a snake case string (my_string) to camel case (myString).
54
58
 
55
59
  Note this is sometimes ambiguous. Consider:
56
60
 
@@ -62,6 +66,10 @@ def snake_case_to_camel_case(string: str) -> str:
62
66
  return words[0] + "".join([x.title() for x in words[1:]])
63
67
 
64
68
 
69
+ def snake_case_to_nice(string: str) -> str:
70
+ return camel_case_to_nice(snake_case_to_camel_case(string))
71
+
72
+
65
73
  casings = ["camelCase", "snake_case", "TitleCase"]
66
74
  casing_swap_map = {
67
75
  "camelCase": {
@@ -87,7 +95,7 @@ def swap_casing(string: str, from_casing: str, to_casing: str) -> str:
87
95
  raise ValueError(f"Invalid casing '{from_casing}'. Must be one of '" + "', ".join(casings) + "'")
88
96
  if to_casing not in casings:
89
97
  raise ValueError(f"Invalid casing '{to_casing}'. Must be one of '" + "', ".join(casings) + "'")
90
- return casing_swap_map[from_casing][to_casing](string)
98
+ return casing_swap_map[from_casing][to_casing](string) # type: ignore
91
99
 
92
100
 
93
101
  def make_plural(singular: str):
@@ -1,24 +1,76 @@
1
- from typing import Any
2
- from .. import model
3
1
  import inspect
2
+ from typing import Any
4
3
 
5
4
 
6
5
  def is_model(to_check: Any) -> bool:
7
6
  """
8
- Returns True/False to denote if the given value is a model instance
7
+ Return True/False to denote if the given value is a model instance.
8
+
9
+ Uses ducktyping rather than checking the type, mostly to minimize the risk of circular imports
9
10
  """
10
- return isinstance(to_check, model.Model)
11
+ if not hasattr(to_check, "destination_name"):
12
+ return False
13
+ return True
11
14
 
12
15
 
13
16
  def is_model_class(to_check: Any) -> bool:
17
+ """Return True/False to denote if the given value is a model class."""
18
+ return inspect.isclass(to_check) and is_model(to_check)
19
+
20
+
21
+ def is_model_class_reference(to_check: Any) -> bool:
22
+ """Return True/False to denote if the given value is a reference to a model class."""
23
+ if not hasattr(to_check, "get_model_class"):
24
+ return False
25
+
26
+ if inspect.isclass(to_check):
27
+ to_check = to_check()
28
+ return is_model_class(to_check.get_model_class())
29
+
30
+
31
+ def is_model_class_or_reference(to_check: Any, raise_error_message=False, strict=True) -> bool:
14
32
  """
15
- Returns True/False to denote if the given value is a model class
33
+ Return True/False to denote if the given value is a model class or a model reference.
34
+
35
+ If strict is false, then it won't check that the model reference returns a model class. This is often necessary during
36
+ validation by configs as, otherwise, it tends to trigger circular dependency errors.
16
37
  """
17
- return inspect.isclass(to_check) and issubclass(to_check, model.Model)
38
+ if not inspect.isclass(to_check):
39
+ # for references we will accept either instances or classes
40
+ if hasattr(to_check, "get_model_class"):
41
+ return is_model_class(to_check.get_model_class()) if strict else True
42
+
43
+ if raise_error_message:
44
+ raise TypeError(
45
+ f"I expected a model class or reference, but instead I received something of type '{to_check.__class__.__name__}'"
46
+ )
47
+ return False
48
+
49
+ if is_model_class(to_check):
50
+ return True
51
+
52
+ if hasattr(to_check, "get_model_class"):
53
+ if not strict:
54
+ return True
55
+
56
+ model_class = to_check().get_model_class()
57
+ if is_model_class(model_class):
58
+ return True
59
+ if raise_error_message:
60
+ raise TypeError(
61
+ f"I expected a model class or reference. I received a model reference of class '{to_check.__name__}', but when I fetched the model out of it, it gave me back something that wasn't a model. Instead, I received something of type '{model_class.__name__}'"
62
+ )
63
+
64
+ if raise_error_message:
65
+ raise TypeError(
66
+ "I expected a model class or reference, but instead I received a class that was neither. It had a type of '"
67
+ + to_check.__class__.__name__
68
+ + "'"
69
+ )
70
+
71
+ return False
18
72
 
19
73
 
20
74
  def is_model_or_class(to_check: Any) -> bool:
21
- """
22
- Returns True/False to denote if the given value is a model instance or model class
23
- """
75
+ """Return True/False to denote if the given value is a model instance or model class."""
24
76
  return is_model(to_check) or is_model_class(to_check)
@@ -1,11 +1,13 @@
1
- from .cli import CLI
2
- from .input_output import InputOutput
3
- from .wsgi import WSGI
4
- from . import exceptions
1
+ from clearskies.input_outputs.cli import Cli
2
+ from clearskies.input_outputs.headers import Headers
3
+ from clearskies.input_outputs.input_output import InputOutput
4
+ from clearskies.input_outputs.programmatic import Programmatic
5
+ from clearskies.input_outputs.wsgi import Wsgi
5
6
 
6
7
  __all__ = [
7
- "CLI",
8
+ "Cli",
9
+ "Headers",
8
10
  "InputOutput",
9
- "WSGI",
10
- "exceptions",
11
+ "Programmatic",
12
+ "Wsgi",
11
13
  ]
@@ -1,71 +1,140 @@
1
1
  import json
2
- from . import exceptions
3
- from ..handlers.exceptions import ClientError
4
-
5
-
6
- class CLI:
7
- _sys = None
8
- _args = None
9
- _flags = None
10
- _cached_body = None
11
- _has_body = None
12
- _input_type = None
13
- _body_loaded_as_json = None
14
- _body_as_json = None
15
- _routing_data = None
16
- _authorization_data = None
17
-
18
- def __init__(self, sys):
19
- self._sys = sys
2
+ import sys
3
+ from os import isatty
4
+ from sys import stdin
5
+
6
+ from clearskies.input_outputs.input_output import InputOutput
7
+
8
+
9
+ class Cli(InputOutput):
10
+ _args: list[str] = []
11
+ _has_body: bool = False
12
+ _body: str = ""
13
+ _request_method: str = ""
14
+ _request_headers: dict[str, str] = {}
15
+
16
+ def __init__(self):
17
+ self._request_headers = {}
20
18
  self._args = []
21
- self._kwargs = {}
22
- self._request_method = None
23
- self._parse_args(self._sys.argv)
24
- self._authorization_data = None
19
+ self._parse_args(sys.argv)
20
+ super().__init__()
25
21
 
26
22
  def respond(self, response, status_code=200):
27
- if status_code == 404:
28
- raise exceptions.CLINotFound()
29
23
  if status_code != 200:
30
- self._sys.exit(response)
24
+ sys.exit(response)
31
25
  if type(response) != str:
32
26
  print(json.dumps(response))
33
27
  else:
34
28
  print(response)
35
29
 
36
- def error(self, body):
37
- return self.respond(body, 400)
38
-
39
- def success(self, body):
40
- return self.respond(body, 200)
41
-
42
30
  def get_arguments(self):
43
- return self._sys.argv
31
+ return sys.argv
44
32
 
45
33
  def _parse_args(self, argv):
46
- for arg in argv[1:]:
47
- if arg[0] == "-":
48
- arg = arg.lstrip("-")
49
- if "=" in arg:
50
- name = arg[: arg.index("=")]
51
- value = arg[arg.index("=") + 1 :]
52
- else:
53
- name = arg
54
- value = True
55
- if name in self._kwargs:
56
- raise exceptions.CLIInputError(f"Received multiple flags for '{name}'")
57
- if name == "X":
58
- name = "request_method"
59
- if name == "request_method":
60
- if self._request_method:
61
- raise ValueError(
62
- "Received multiple flags for the request method (setablve via 'request_method' and 'X')"
63
- )
64
- self._request_method = value.upper()
65
- else:
66
- self._kwargs[name] = value
67
- else:
34
+ tty_data = None
35
+ if not isatty(stdin.fileno()):
36
+ tty_data = sys.stdin.read().strip()
37
+
38
+ request_headers = {}
39
+ self._args = []
40
+ kwargs = {}
41
+ index = 0
42
+ # In general we will use positional arguments for routing, and kwargs for request data.
43
+ # If things start with a dash then they are assumed to be a kwarg. If not, then a positional argument.
44
+ # we don't allow for simple flags: everything is a positional argument or a key/value pair
45
+ # For kwargs, we'll allow for using an equal sign or not, e.g.: '--key=value' or '--key value' or '-d thing'.
46
+ while index < len(argv) - 1:
47
+ index += 1
48
+
49
+ # if we don't start with a dash then we are a positional argument
50
+ arg = argv[index]
51
+ if arg[0] != "-":
68
52
  self._args.append(arg)
53
+ continue
54
+
55
+ # otherwise a kwarg
56
+ arg = arg.strip("-")
57
+
58
+ # if we have an equal sign in our kwarg then it's self-contained
59
+ if "=" in arg:
60
+ [key, value] = arg.split("=", 1)
61
+
62
+ # otherwise we have to grab the next argument to get the value
63
+ else:
64
+ key = arg
65
+ value = argv[index + 1]
66
+ if "-" in value:
67
+ raise ValueError(
68
+ f"Invalid clearskies cli calling sequence: found two key names next to eachother without any values: '-{arg} {value}'"
69
+ )
70
+ index += 1
71
+
72
+ if key.lower() == "h":
73
+ parts = value.split(":", 1)
74
+ if len(parts) != 2:
75
+ raise ValueError(
76
+ f"Invalid clearskies cli calling sequence: a parameter named '-H' was found, which is treated as a request header, but it didn't have the proper 'key: value' format."
77
+ )
78
+ request_headers[parts[0]] = parts[1]
79
+ continue
80
+
81
+ kwargs[key] = value
82
+
83
+ self._request_headers = request_headers
84
+ self._request_method = "GET"
85
+ request_method_source = ""
86
+ for key in ["x", "X", "request_method"]:
87
+ if key not in kwargs:
88
+ continue
89
+
90
+ if request_method_source:
91
+ raise ValueError(
92
+ f"Invalid clearskies cli calling sequence: the request method was specified via both the -{key} parameter and the -{request_method_source} parameter. To avoid ambiguity, it should only be set once."
93
+ )
94
+ self._request_method = kwargs[key]
95
+ del kwargs[key]
96
+ request_method_source = key
97
+
98
+ final_data = None
99
+ data_source = None
100
+ if tty_data:
101
+ final_data = tty_data
102
+ data_source = "piped input"
103
+ if kwargs.get("d"):
104
+ if final_data:
105
+ raise ValueError(
106
+ f"Invalid clearskies cli calling sequence: request data was sent by both the -d parameter and {data_source}. To avoid ambiguity, it should only be sent one way."
107
+ )
108
+ final_data = kwargs.get("d")
109
+ data_source = "the -d parameter"
110
+ del kwargs["d"]
111
+ if kwargs.get("data"):
112
+ if final_data:
113
+ raise ValueError(
114
+ f"Invalid calling sequence: request data was sent by both the -data parameter and {data_source}. To avoid ambiguity, it should only be sent one way."
115
+ )
116
+ final_data = kwargs.get("data")
117
+ data_source = "the -data parameter"
118
+ del kwargs["data"]
119
+ if final_data and len(kwargs):
120
+ raise ValueError(
121
+ f"Invalid calling sequence: extra parameters were specified after sending a body via {data_source}. To avoid ambiguity, send all data via {data_source}."
122
+ )
123
+ if not final_data and len(kwargs):
124
+ final_data = kwargs
125
+ data_source = "kwargs"
126
+
127
+ # Most of the above inputs result in a string for our final data, in which case we'll leave it as the "raw body"
128
+ # so that it can optionally be interpreted as JSON. If we received a bunch of kwargs though, we'll allow those to
129
+ # only be "read" as JSON.
130
+ if data_source == "kwargs":
131
+ self._body_as_json = final_data # type: ignore
132
+ self._body_loaded_as_json = True
133
+ self._has_body = True
134
+ self._body = json.dumps(final_data)
135
+ elif final_data:
136
+ self._has_body = True
137
+ self._body = final_data
69
138
 
70
139
  def get_script_name(self):
71
140
  return sys.argv[0]
@@ -77,106 +146,25 @@ class CLI:
77
146
  return self.get_path_info()
78
147
 
79
148
  def get_request_method(self):
80
- return self._request_method if self._request_method else "GET"
81
-
82
- def request_data(self, required=True, allow_non_json_bodies=False):
83
- request_data = self.json_body(False, allow_non_json_bodies=allow_non_json_bodies)
84
- if not request_data:
85
- if self.has_body() and not allow_non_json_bodies:
86
- raise ClientError("Request body was not valid JSON")
87
- request_data = {}
88
- return request_data
149
+ return self._request_method
89
150
 
90
151
  def has_body(self):
91
- if self._has_body is None:
92
- self._has_body = False
93
- # we have a number of different input modes that we will treat as data input,
94
- # all of which the callable handler will use as structured input when trying to
95
- # compare data against a schema:
96
-
97
- # isatty() means that someone is piping input into the program
98
- # however, it behaves unreliably in "alternate" environments in later versions
99
- # of python, so I'm removing it until I can find a better solution
100
- # if not self._sys.stdin.isatty():
101
- # self._has_body = True
102
- # self._input_type = 'atty'
103
- # or if the user set 'data' or 'd' keys
104
- if "data" in self._kwargs or "d" in self._kwargs:
105
- self._has_body = True
106
- self._input_type = "data" if "data" in self._kwargs else "d"
107
- # or finally if we have kwargs in general
108
- elif len(self._kwargs):
109
- self._has_body = True
110
- self._input_type = "kwargs"
111
152
  return self._has_body
112
153
 
113
154
  def get_body(self):
114
155
  if not self.has_body():
115
156
  return ""
116
157
 
117
- if self._cached_body is None:
118
- if self._input_type == "atty":
119
- self._cached_body = "\n".join([line.strip() for line in self._sys.stdin])
120
- elif self._input_type == "data":
121
- self._cached_body = self._kwargs["data"]
122
- elif self._input_type == "data":
123
- self._cached_body = self._kwargs["d"]
124
- # we don't do anything about self._input_type == 'kwargs' because that only
125
- # makes sense when trying to interpret the body as JSON, so we cover it
126
- # in the _get_json_body method
127
- return self._cached_body
128
-
129
- def json_body(self, required=True, allow_non_json_bodies=False):
130
- json = self._get_json_body()
131
- # if we get None then either the body was not JSON or was empty.
132
- # If it is required then we have an exception either way. If it is not required
133
- # then we have an exception if a body was provided but it was not JSON. We can check for this
134
- # if json is None and there is an actual request body. If json is none, the body is empty,
135
- # and it was not required, then we can just return None
136
- if json is None:
137
- if required or (self.has_body() and not allow_non_json_bodies):
138
- raise ClientError("Request body was not valid JSON")
139
- return json
140
-
141
- def _get_json_body(self):
142
- if not self.has_body():
143
- return None
144
- if not self._body_loaded_as_json:
145
- if self._input_type == "kwargs":
146
- self._body_loaded_as_json = True
147
- self._body_as_json = self._kwargs
148
- elif self.get_body() is None:
149
- self._body_as_json = None
150
- else:
151
- self._body_loaded_as_json = True
152
- try:
153
- self._body_as_json = json.loads(self.get_body())
154
- except json.JSONDecodeError:
155
- self._body_as_json = None
156
- return self._body_as_json
157
-
158
- def routing_data(self):
159
- return self._routing_data if self._routing_data is not None else {}
160
-
161
- def set_routing_data(self, data):
162
- self._routing_data = data
163
-
164
- def add_routing_data(self, key, value=None):
165
- if self._routing_data is None:
166
- self._routing_data = {}
167
- if type(key) == dict:
168
- self._routing_data = {**self._routing_data, **key}
169
- else:
170
- self._routing_data[key] = value
158
+ return self._body
171
159
 
172
160
  def context_specifics(self):
173
161
  return {}
174
162
 
175
163
  def get_client_ip(self):
176
- return "cli"
164
+ return "127.0.0.1"
177
165
 
178
- def set_authorization_data(self, data):
179
- self._authorization_data = data
166
+ def get_query_string(self):
167
+ return ""
180
168
 
181
- def get_authorization_data(self):
182
- return self._authorization_data if self._authorization_data else {}
169
+ def get_request_headers(self):
170
+ return self._request_headers
@@ -1,2 +1,2 @@
1
- from .cli_not_found import CLINotFound
2
1
  from .cli_input_error import CLIInputError
2
+ from .cli_not_found import CLINotFound
@@ -0,0 +1,45 @@
1
+ import re
2
+
3
+
4
+ class Headers:
5
+ _headers: dict[str, str] = {}
6
+
7
+ def __init__(self, headers: dict[str, str] = {}):
8
+ self.__dict__["_headers"] = (
9
+ {key.upper().replace("_", "-"): value for (key, value) in headers.items()} if headers else {}
10
+ )
11
+
12
+ def __contains__(self, key: str):
13
+ return key.upper().replace("_", "-") in self._headers
14
+
15
+ def __getattr__(self, key: str) -> str | None:
16
+ return self._headers.get(key.upper().replace("_", "-"), None)
17
+
18
+ def __setattr__(self, key: str, value: str) -> None:
19
+ if not isinstance(key, str):
20
+ raise TypeError(
21
+ f"Header keys must be strings, but an object of type '{value.__class__.__name__}' was provided."
22
+ )
23
+ if not isinstance(value, str):
24
+ raise TypeError(
25
+ f"Header values must be strings, but an object of type '{value.__class__.__name__}' was provided."
26
+ )
27
+ self._headers[re.sub("\\s+", " ", key.upper().replace("_", "-"))] = re.sub("\\s+", " ", value.strip())
28
+
29
+ def get(self, key, default=None):
30
+ if key not in self:
31
+ return default
32
+ return self.__getattr__(key)
33
+
34
+ def keys(self) -> list[str]:
35
+ return list(self._headers.keys())
36
+
37
+ def values(self) -> list[str]:
38
+ return list(self._headers.keys())
39
+
40
+ def items(self) -> list[tuple[str]]:
41
+ return list(self._headers.items()) # type: ignore
42
+
43
+ def add(self, key: str, value: str) -> None:
44
+ """Add a header. This expects a string with a colon separating the key and value."""
45
+ setattr(self, key, value)