clear-skies 1.22.10__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 (368) 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.22.10.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 +8 -39
  7. clearskies/authentication/authentication.py +44 -0
  8. clearskies/authentication/authorization.py +14 -8
  9. clearskies/authentication/authorization_pass_through.py +14 -10
  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 +55 -20
  41. clearskies/backends/api_backend.py +1118 -280
  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 +115 -4
  154. clearskies/di/additional_config_auto_import.py +12 -0
  155. clearskies/di/di.py +714 -125
  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 -160
  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 +1874 -193
  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.22.10.dist-info/METADATA +0 -47
  243. clear_skies-1.22.10.dist-info/RECORD +0 -213
  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 -51
  248. clearskies/backends/api_get_only_backend.py +0 -48
  249. clearskies/backends/example_backend.py +0 -43
  250. clearskies/backends/file_backend.py +0 -48
  251. clearskies/backends/json_backend.py +0 -7
  252. clearskies/backends/restful_api_advanced_search_backend.py +0 -103
  253. clearskies/binding_config.py +0 -16
  254. clearskies/column_types/__init__.py +0 -203
  255. clearskies/column_types/audit.py +0 -249
  256. clearskies/column_types/belongs_to.py +0 -271
  257. clearskies/column_types/boolean.py +0 -60
  258. clearskies/column_types/category_tree.py +0 -304
  259. clearskies/column_types/column.py +0 -373
  260. clearskies/column_types/created.py +0 -26
  261. clearskies/column_types/created_by_authorization_data.py +0 -26
  262. clearskies/column_types/created_by_header.py +0 -24
  263. clearskies/column_types/created_by_ip.py +0 -17
  264. clearskies/column_types/created_by_routing_data.py +0 -25
  265. clearskies/column_types/created_by_user_agent.py +0 -17
  266. clearskies/column_types/created_micro.py +0 -26
  267. clearskies/column_types/datetime.py +0 -109
  268. clearskies/column_types/datetime_micro.py +0 -13
  269. clearskies/column_types/email.py +0 -18
  270. clearskies/column_types/float.py +0 -43
  271. clearskies/column_types/has_many.py +0 -179
  272. clearskies/column_types/has_one.py +0 -58
  273. clearskies/column_types/integer.py +0 -41
  274. clearskies/column_types/json.py +0 -25
  275. clearskies/column_types/many_to_many.py +0 -278
  276. clearskies/column_types/many_to_many_with_data.py +0 -162
  277. clearskies/column_types/phone.py +0 -48
  278. clearskies/column_types/select.py +0 -11
  279. clearskies/column_types/string.py +0 -24
  280. clearskies/column_types/timestamp.py +0 -73
  281. clearskies/column_types/updated.py +0 -26
  282. clearskies/column_types/updated_micro.py +0 -26
  283. clearskies/column_types/uuid.py +0 -25
  284. clearskies/columns.py +0 -123
  285. clearskies/condition_parser.py +0 -172
  286. clearskies/contexts/build_context.py +0 -54
  287. clearskies/contexts/convert_to_application.py +0 -190
  288. clearskies/contexts/extract_handler.py +0 -37
  289. clearskies/contexts/test.py +0 -94
  290. clearskies/decorators/__init__.py +0 -39
  291. clearskies/decorators/auth0_jwks.py +0 -22
  292. clearskies/decorators/authorization.py +0 -10
  293. clearskies/decorators/binding_classes.py +0 -9
  294. clearskies/decorators/binding_modules.py +0 -9
  295. clearskies/decorators/bindings.py +0 -9
  296. clearskies/decorators/create.py +0 -10
  297. clearskies/decorators/delete.py +0 -10
  298. clearskies/decorators/docs.py +0 -14
  299. clearskies/decorators/get.py +0 -10
  300. clearskies/decorators/jwks.py +0 -26
  301. clearskies/decorators/merge.py +0 -124
  302. clearskies/decorators/patch.py +0 -10
  303. clearskies/decorators/post.py +0 -10
  304. clearskies/decorators/public.py +0 -11
  305. clearskies/decorators/response_headers.py +0 -10
  306. clearskies/decorators/return_raw_response.py +0 -9
  307. clearskies/decorators/schema.py +0 -10
  308. clearskies/decorators/secret_bearer.py +0 -24
  309. clearskies/decorators/security_headers.py +0 -10
  310. clearskies/di/standard_dependencies.py +0 -151
  311. clearskies/di/test_module/__init__.py +0 -6
  312. clearskies/di/test_module/another_module/__init__.py +0 -2
  313. clearskies/di/test_module/module_class.py +0 -5
  314. clearskies/handlers/__init__.py +0 -41
  315. clearskies/handlers/advanced_search.py +0 -271
  316. clearskies/handlers/base.py +0 -479
  317. clearskies/handlers/callable.py +0 -191
  318. clearskies/handlers/create.py +0 -35
  319. clearskies/handlers/crud_by_method.py +0 -18
  320. clearskies/handlers/database_connector.py +0 -32
  321. clearskies/handlers/delete.py +0 -61
  322. clearskies/handlers/exceptions/__init__.py +0 -5
  323. clearskies/handlers/exceptions/not_found.py +0 -3
  324. clearskies/handlers/get.py +0 -156
  325. clearskies/handlers/health_check.py +0 -59
  326. clearskies/handlers/input_processing.py +0 -79
  327. clearskies/handlers/list.py +0 -530
  328. clearskies/handlers/mygrations.py +0 -82
  329. clearskies/handlers/request_method_routing.py +0 -47
  330. clearskies/handlers/restful_api.py +0 -218
  331. clearskies/handlers/routing.py +0 -62
  332. clearskies/handlers/schema_helper.py +0 -128
  333. clearskies/handlers/simple_routing.py +0 -206
  334. clearskies/handlers/simple_routing_route.py +0 -192
  335. clearskies/handlers/simple_search.py +0 -136
  336. clearskies/handlers/update.py +0 -96
  337. clearskies/handlers/write.py +0 -193
  338. clearskies/input_requirements/__init__.py +0 -78
  339. clearskies/input_requirements/after.py +0 -36
  340. clearskies/input_requirements/before.py +0 -36
  341. clearskies/input_requirements/in_the_future_at_least.py +0 -19
  342. clearskies/input_requirements/in_the_future_at_most.py +0 -19
  343. clearskies/input_requirements/in_the_past_at_least.py +0 -19
  344. clearskies/input_requirements/in_the_past_at_most.py +0 -19
  345. clearskies/input_requirements/maximum_length.py +0 -19
  346. clearskies/input_requirements/maximum_value.py +0 -19
  347. clearskies/input_requirements/minimum_length.py +0 -22
  348. clearskies/input_requirements/minimum_value.py +0 -19
  349. clearskies/input_requirements/requirement.py +0 -25
  350. clearskies/input_requirements/time_delta.py +0 -38
  351. clearskies/input_requirements/unique.py +0 -18
  352. clearskies/mocks/__init__.py +0 -7
  353. clearskies/mocks/input_output.py +0 -124
  354. clearskies/mocks/models.py +0 -142
  355. clearskies/models.py +0 -350
  356. clearskies/security_headers/base.py +0 -12
  357. clearskies/tests/simple_api/models/__init__.py +0 -2
  358. clearskies/tests/simple_api/models/status.py +0 -23
  359. clearskies/tests/simple_api/models/user.py +0 -21
  360. clearskies/tests/simple_api/users_api.py +0 -64
  361. {clear_skies-1.22.10.dist-info → clear_skies-2.0.23.dist-info/licenses}/LICENSE +0 -0
  362. /clearskies/{contexts/bash.py → autodoc/py.typed} +0 -0
  363. /clearskies/{handlers/exceptions → exceptions}/authentication.py +0 -0
  364. /clearskies/{handlers/exceptions → exceptions}/authorization.py +0 -0
  365. /clearskies/{handlers/exceptions → exceptions}/client_error.py +0 -0
  366. /clearskies/{secrets/exceptions → exceptions}/not_found.py +0 -0
  367. /clearskies/{tests/__init__.py → input_outputs/py.typed} +0 -0
  368. /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,182 +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
- _authorization_data = None
17
-
18
- def __init__(self, sys):
19
- self._sys = sys
20
- self._args = []
21
- self._kwargs = {}
22
- self._request_method = None
23
- self._parse_args(self._sys.argv)
24
- self._authorization_data = None
4
+ import sys
5
+ from os import isatty
6
+ from sys import stdin
25
7
 
26
- def respond(self, response, status_code=200):
27
- if status_code == 404:
28
- raise exceptions.CLINotFound()
29
- if status_code != 200:
30
- self._sys.exit(response)
31
- if type(response) != str:
32
- print(json.dumps(response))
33
- else:
34
- print(response)
8
+ from clearskies.input_outputs.headers import Headers
9
+ from clearskies.input_outputs.input_output import InputOutput
35
10
 
36
- def error(self, body):
37
- return self.respond(body, 400)
38
11
 
39
- def success(self, body):
40
- return self.respond(body, 200)
12
+ class Cli(InputOutput):
13
+ path: str
14
+ _has_body: bool = False
15
+ _body: str = ""
41
16
 
42
- def get_arguments(self):
43
- 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)
44
29
 
45
30
  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
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
67
60
  else:
68
- self._args.append(arg)
69
-
70
- def get_script_name(self):
71
- return sys.argv[0]
72
-
73
- def get_path_info(self):
74
- 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
75
137
 
76
138
  def get_full_path(self):
77
- return self.get_path_info()
78
-
79
- def get_request_method(self):
80
- return self._request_method if self._request_method else "GET"
81
-
82
- def request_data(self, required=True):
83
- request_data = self.json_body(False)
84
- if not request_data:
85
- if self.has_body():
86
- raise ClientError("Request body was not valid JSON")
87
- request_data = {}
88
- return request_data
139
+ return self.path
89
140
 
90
141
  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
142
  return self._has_body
112
143
 
113
144
  def get_body(self):
114
145
  if not self.has_body():
115
146
  return ""
116
147
 
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):
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():
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
148
+ return self._body
171
149
 
172
- def context_specifics(self):
173
- return {}
174
-
175
- def get_client_ip(self):
150
+ def get_protocol(self):
176
151
  return "cli"
177
152
 
178
- def set_authorization_data(self, data):
179
- self._authorization_data = data
153
+ def context_specifics(self):
154
+ return {"sys_argv": sys.argv}
180
155
 
181
- def get_authorization_data(self):
182
- return self._authorization_data if self._authorization_data else {}
156
+ def get_client_ip(self):
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)