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,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,174 +1,157 @@
1
+ from __future__ import annotations
2
+
1
3
  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
-
17
- def __init__(self, sys):
18
- self._sys = sys
19
- self._args = []
20
- self._kwargs = {}
21
- self._request_method = None
22
- self._parse_args(self._sys.argv)
4
+ import sys
5
+ from os import isatty
6
+ from sys import stdin
23
7
 
24
- def respond(self, response, status_code=200):
25
- if status_code == 404:
26
- raise exceptions.CLINotFound()
27
- if status_code != 200:
28
- self._sys.exit(response)
29
- if type(response) != str:
30
- print(json.dumps(response))
31
- else:
32
- print(response)
8
+ from clearskies.input_outputs.headers import Headers
9
+ from clearskies.input_outputs.input_output import InputOutput
33
10
 
34
- def error(self, body):
35
- return self.respond(body, 400)
36
11
 
37
- def success(self, body):
38
- return self.respond(body, 200)
12
+ class Cli(InputOutput):
13
+ path: str
14
+ _has_body: bool = False
15
+ _body: str = ""
39
16
 
40
- def get_arguments(self):
41
- return self._sys.argv
17
+ def __init__(self):
18
+ self._parse_args(sys.argv)
19
+ super().__init__()
20
+
21
+ def respond(self, response, status_code=200):
22
+ if type(response) != str:
23
+ final = json.dumps(response)
24
+ else:
25
+ final = response
26
+ if status_code != 200:
27
+ sys.exit(final)
28
+ print(final)
42
29
 
43
30
  def _parse_args(self, argv):
44
- for arg in argv[1:]:
45
- if arg[0] == "-":
46
- arg = arg.lstrip("-")
47
- if "=" in arg:
48
- name = arg[: arg.index("=")]
49
- value = arg[arg.index("=") + 1 :]
50
- else:
51
- name = arg
52
- value = True
53
- if name in self._kwargs:
54
- raise exceptions.CLIInputError(f"Received multiple flags for '{name}'")
55
- if name == "X":
56
- name = "request_method"
57
- if name == "request_method":
58
- if self._request_method:
59
- raise ValueError(
60
- "Received multiple flags for the request method (setablve via 'request_method' and 'X')"
61
- )
62
- self._request_method = value.upper()
63
- else:
64
- self._kwargs[name] = value
31
+ tty_data = None
32
+ if not isatty(stdin.fileno()):
33
+ tty_data = sys.stdin.read().strip()
34
+
35
+ request_headers = {}
36
+ args = []
37
+ kwargs = {}
38
+ index = 0
39
+ # In general we will use positional arguments for routing, and kwargs for request data.
40
+ # If things start with a dash then they are assumed to be a kwarg. If not, then a positional argument.
41
+ # we don't allow for simple flags: everything is a positional argument or a key/value pair
42
+ # For kwargs, we'll allow for using an equal sign or not, e.g.: '--key=value' or '--key value' or '-d thing'.
43
+ while index < len(argv) - 1:
44
+ index += 1
45
+
46
+ # if we don't start with a dash then we are a positional argument which are used for building the URL-equivalent
47
+ arg = argv[index]
48
+ if arg[0] != "-":
49
+ args.append(arg)
50
+ continue
51
+
52
+ # otherwise a kwarg
53
+ arg = arg.strip("-")
54
+
55
+ # if we have an equal sign in our kwarg then it's self-contained
56
+ if "=" in arg:
57
+ [key, value] = arg.split("=", 1)
58
+
59
+ # otherwise we have to grab the next argument to get the value
65
60
  else:
66
- self._args.append(arg)
67
-
68
- def get_script_name(self):
69
- return sys.argv[0]
70
-
71
- def get_path_info(self):
72
- return "/".join(self._args)
61
+ key = arg
62
+ value = argv[index + 1]
63
+ if "-" in value:
64
+ raise ValueError(
65
+ f"Invalid clearskies cli calling sequence: found two key names next to eachother without any values: '-{arg} {value}'"
66
+ )
67
+ index += 1
68
+
69
+ if key.lower() == "h":
70
+ parts = value.split(":", 1)
71
+ if len(parts) != 2:
72
+ raise ValueError(
73
+ 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."
74
+ )
75
+ request_headers[parts[0]] = parts[1]
76
+ continue
77
+
78
+ kwargs[key] = value
79
+
80
+ self.request_headers = Headers(request_headers)
81
+ self.request_method = "GET"
82
+ request_method_source = ""
83
+ for key in ["x", "X", "request_method"]:
84
+ if key not in kwargs:
85
+ continue
86
+
87
+ if request_method_source:
88
+ raise ValueError(
89
+ 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."
90
+ )
91
+ self.request_method = kwargs[key].upper()
92
+ del kwargs[key]
93
+ request_method_source = key
94
+
95
+ final_data = None
96
+ data_source = None
97
+ if tty_data:
98
+ final_data = tty_data
99
+ data_source = "piped input"
100
+ if kwargs.get("d"):
101
+ if final_data:
102
+ raise ValueError(
103
+ 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."
104
+ )
105
+ final_data = kwargs.get("d")
106
+ data_source = "the -d parameter"
107
+ del kwargs["d"]
108
+ if kwargs.get("data"):
109
+ if final_data:
110
+ raise ValueError(
111
+ 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."
112
+ )
113
+ final_data = kwargs.get("data")
114
+ data_source = "the -data parameter"
115
+ del kwargs["data"]
116
+ if final_data and len(kwargs):
117
+ raise ValueError(
118
+ f"Invalid calling sequence: extra parameters were specified after sending a body via {data_source}. To avoid ambiguity, send all data via {data_source}."
119
+ )
120
+ if not final_data and len(kwargs):
121
+ final_data = kwargs
122
+ data_source = "kwargs"
123
+
124
+ self.path = "/".join(args)
125
+
126
+ # Most of the above inputs result in a string for our final data, in which case we'll leave it as the "raw body"
127
+ # so that it can optionally be interpreted as JSON. If we received a bunch of kwargs though, we'll allow those to
128
+ # only be "read" as JSON.
129
+ if data_source == "kwargs":
130
+ self._body_as_json = final_data # type: ignore
131
+ self._body_loaded_as_json = True
132
+ self._has_body = True
133
+ self._body = json.dumps(final_data)
134
+ elif final_data:
135
+ self._has_body = True
136
+ self._body = final_data
73
137
 
74
138
  def get_full_path(self):
75
- return self.get_path_info()
76
-
77
- def get_request_method(self):
78
- return self._request_method if self._request_method else "GET"
79
-
80
- def request_data(self, required=True):
81
- request_data = self.json_body(False)
82
- if not request_data:
83
- if self.has_body():
84
- raise ClientError("Request body was not valid JSON")
85
- request_data = {}
86
- return request_data
139
+ return self.path
87
140
 
88
141
  def has_body(self):
89
- if self._has_body is None:
90
- self._has_body = False
91
- # we have a number of different input modes that we will treat as data input,
92
- # all of which the callable handler will use as structured input when trying to
93
- # compare data against a schema:
94
-
95
- # isatty() means that someone is piping input into the program
96
- # however, it behaves unreliably in "alternate" environments in later versions
97
- # of python, so I'm removing it until I can find a better solution
98
- # if not self._sys.stdin.isatty():
99
- # self._has_body = True
100
- # self._input_type = 'atty'
101
- # or if the user set 'data' or 'd' keys
102
- if "data" in self._kwargs or "d" in self._kwargs:
103
- self._has_body = True
104
- self._input_type = "data" if "data" in self._kwargs else "d"
105
- # or finally if we have kwargs in general
106
- elif len(self._kwargs):
107
- self._has_body = True
108
- self._input_type = "kwargs"
109
142
  return self._has_body
110
143
 
111
144
  def get_body(self):
112
145
  if not self.has_body():
113
146
  return ""
114
147
 
115
- if self._cached_body is None:
116
- if self._input_type == "atty":
117
- self._cached_body = "\n".join([line.strip() for line in self._sys.stdin])
118
- elif self._input_type == "data":
119
- self._cached_body = self._kwargs["data"]
120
- elif self._input_type == "data":
121
- self._cached_body = self._kwargs["d"]
122
- # we don't do anything about self._input_type == 'kwargs' because that only
123
- # makes sense when trying to interpret the body as JSON, so we cover it
124
- # in the _get_json_body method
125
- return self._cached_body
126
-
127
- def json_body(self, required=True):
128
- json = self._get_json_body()
129
- # if we get None then either the body was not JSON or was empty.
130
- # If it is required then we have an exception either way. If it is not required
131
- # then we have an exception if a body was provided but it was not JSON. We can check for this
132
- # if json is None and there is an actual request body. If json is none, the body is empty,
133
- # and it was not required, then we can just return None
134
- if json is None:
135
- if required or self.has_body():
136
- raise ClientError("Request body was not valid JSON")
137
- return json
138
-
139
- def _get_json_body(self):
140
- if not self.has_body():
141
- return None
142
- if not self._body_loaded_as_json:
143
- if self._input_type == "kwargs":
144
- self._body_loaded_as_json = True
145
- self._body_as_json = self._kwargs
146
- elif self.get_body() is None:
147
- self._body_as_json = None
148
- else:
149
- self._body_loaded_as_json = True
150
- try:
151
- self._body_as_json = json.loads(self.get_body())
152
- except json.JSONDecodeError:
153
- self._body_as_json = None
154
- return self._body_as_json
155
-
156
- def routing_data(self):
157
- return self._routing_data if self._routing_data is not None else {}
158
-
159
- def set_routing_data(self, data):
160
- self._routing_data = data
161
-
162
- def add_routing_data(self, key, value=None):
163
- if self._routing_data is None:
164
- self._routing_data = {}
165
- if type(key) == dict:
166
- self._routing_data = {**self._routing_data, **key}
167
- else:
168
- self._routing_data[key] = value
148
+ return self._body
149
+
150
+ def get_protocol(self):
151
+ return "cli"
169
152
 
170
153
  def context_specifics(self):
171
- return {}
154
+ return {"sys_argv": sys.argv}
172
155
 
173
156
  def get_client_ip(self):
174
- return "cli"
157
+ return "127.0.0.1"
@@ -1,2 +1,7 @@
1
- from .cli_not_found import CLINotFound
2
1
  from .cli_input_error import CLIInputError
2
+ from .cli_not_found import CLINotFound
3
+
4
+ __all__ = [
5
+ "CLIInputError",
6
+ "CLINotFound",
7
+ ]
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import TypeVar
5
+
6
+ _T = TypeVar("_T")
7
+
8
+
9
+ class Headers(dict[str, str]):
10
+ _duck_cheat = "headers"
11
+
12
+ def __init__(self, headers: dict[str, str] = {}) -> None:
13
+ normalized_headers = (
14
+ {key.upper().replace("_", "-"): value for (key, value) in headers.items()} if headers else {}
15
+ )
16
+ super().__init__(normalized_headers)
17
+
18
+ def __contains__(self, key: object) -> bool:
19
+ if not isinstance(key, str):
20
+ return False
21
+ return super().__contains__(key.upper().replace("_", "-"))
22
+
23
+ def __getitem__(self, key: str) -> str:
24
+ return super().__getitem__(key.upper().replace("_", "-"))
25
+
26
+ def __setitem__(self, key: str, value: str) -> None:
27
+ if not isinstance(key, str):
28
+ raise TypeError(
29
+ f"Header keys must be strings, but an object of type '{key.__class__.__name__}' was provided."
30
+ )
31
+ if not isinstance(value, str):
32
+ raise TypeError(
33
+ f"Header values must be strings, but an object of type '{value.__class__.__name__}' was provided."
34
+ )
35
+ normalized_key = re.sub("\\s+", " ", key.upper().replace("_", "-"))
36
+ normalized_value = re.sub("\\s+", " ", value.strip())
37
+ super().__setitem__(normalized_key, normalized_value)
38
+
39
+ def __getattr__(self, key: str) -> str | None:
40
+ return self.get(key.upper().replace("_", "-"), None)
41
+
42
+ def __setattr__(self, key: str, value: str) -> None:
43
+ if key.startswith("_") or key == "_duck_cheat":
44
+ # Allow setting private attributes and special attributes normally
45
+ super().__setattr__(key, value)
46
+ else:
47
+ self.__setitem__(key, value)
48
+
49
+ def get(self, key: str, default: _T = None) -> str | _T: # type: ignore[assignment]
50
+ return super().get(key.upper().replace("_", "-"), default)
51
+
52
+ def add(self, key: str, value: str) -> None:
53
+ """Add a header. This expects a string with a colon separating the key and value."""
54
+ setattr(self, key, value)