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,59 +1,530 @@
1
- from .. import autodoc
1
+ from clearskies import configs, decorators, di
2
2
 
3
+ from .authentication import Authentication
4
+
5
+
6
+ class SecretBearer(Authentication, di.InjectableProperties):
7
+ """
8
+ Secret Bearer performs authentication by checking against a static API key stored in either environment variables or a secret manager.
9
+
10
+ This can be used in two different ways:
11
+
12
+ 1. Attached to an endpoint to enforce authentication
13
+ 2. Attached to an API backend to specify how to authenticate to the API endpoint.
14
+
15
+ ### Authenticating Endpoints.
16
+
17
+ When attached to an endpoint this will enforce authentication. Clients authenticate themselves by providing the secret value
18
+ via the `authorization` header. In the following example we configure the secret bearer class to get the secretfrom an
19
+ environment variable which is set in the code itself. Normally you wouldn't set environment variables like this,
20
+ but it's done here to create a self-contained example that is easy to run:
21
+
22
+ ```python
23
+ import os
24
+ import clearskies
25
+
26
+ os.environ["MY_AUTH_SECRET"] = "SUPERSECRET"
27
+
28
+ wsgi = clearskies.contexts.WsgiRef(
29
+ clearskies.endpoints.Callable(
30
+ lambda: {"hello": "world"},
31
+ authentication=clearskies.authentication.SecretBearer(environment_key="MY_AUTH_SECRET"),
32
+ )
33
+ )
34
+ wsgi()
35
+ ```
36
+ We can then call it with and without the authentication header:
37
+
38
+ ```bash
39
+ $ curl 'http://localhost:8080' -H 'Authorization: SUPERSECRET' | jq
40
+ {
41
+ "status": "success",
42
+ "error": "",
43
+ "data": {
44
+ "hello": "world"
45
+ },
46
+ "pagination": {},
47
+ "input_errors": {}
48
+ }
49
+
50
+ $ curl 'http://localhost:8080' | jq
51
+ {
52
+ "status": "client_error",
53
+ "error": "Not Authenticated",
54
+ "data": [],
55
+ "pagination": {},
56
+ "input_errors": {}
57
+ }
58
+
59
+ $ curl 'http://localhost:8080' -H 'Authorization: NOTTHESECRET' | jq
60
+ {
61
+ "status": "client_error",
62
+ "error": "Not Authenticated",
63
+ "data": [],
64
+ "pagination": {},
65
+ "input_errors": {}
66
+ }
67
+ ```
68
+
69
+ ### Authenticating to APIs
70
+
71
+ The secret bearer class can also be attached to an API Backend to provide authentication to remote APIs. To
72
+ demonstrate, here is an example server that expects a secret token in the authorization header:
73
+
74
+ ```python
75
+ import os
76
+ import clearskies
77
+ from clearskies import columns
78
+
79
+ os.environ["MY_SECRET"] = "SUPERSECRET"
80
+
81
+
82
+ class Widget(clearskies.Model):
83
+ id_column_name = "id"
84
+ backend = clearskies.backends.MemoryBackend()
85
+
86
+ id = columns.Uuid()
87
+ name = columns.String()
88
+ category = columns.String()
89
+ cost = columns.Float()
90
+ created_at = columns.Created()
91
+ updated_at = columns.Updated()
92
+
93
+
94
+ wsgi = clearskies.contexts.WsgiRef(
95
+ clearskies.endpoints.RestfulApi(
96
+ url="widgets",
97
+ model_class=Widget,
98
+ authentication=clearskies.authentication.SecretBearer(environment_key="MY_SECRET"),
99
+ readable_column_names=["id", "name", "category", "cost", "created_at", "updated_at"],
100
+ writeable_column_names=["name", "category", "cost"],
101
+ sortable_column_names=["name", "category", "cost"],
102
+ searchable_column_names=["id", "name", "category", "cost"],
103
+ default_sort_column_name="name",
104
+ )
105
+ )
106
+ wsgi()
107
+ ```
108
+
109
+ Then here is a client app (you can launch the above server and then run this in a new terminal) that
110
+ similarly uses the secret bearer class to authenticate to the server:
111
+
112
+ ```python
113
+ import os
114
+ import clearskies
115
+ from clearskies import columns
116
+
117
+ os.environ["MY_SECRET"] = "SUPERSECRET"
118
+
119
+
120
+ class Widget(clearskies.Model):
121
+ id_column_name = "id"
122
+ backend = clearskies.backends.ApiBackend(
123
+ base_url="http://localhost:8080",
124
+ authentication=clearskies.authentication.SecretBearer(environment_key="MY_SECRET"),
125
+ )
126
+
127
+ id = columns.String()
128
+ name = columns.String()
129
+ category = columns.String()
130
+ cost = columns.Float()
131
+ created_at = columns.Datetime()
132
+ updated_at = columns.Datetime()
133
+
134
+
135
+ def api_demo(widgets: Widget) -> Widget:
136
+ thinga = widgets.create({"name": "Thinga", "category": "Doohickey", "cost": 125})
137
+ mabob = widgets.create({"name": "Mabob", "category": "Doohicky", "cost": 150})
138
+ return widgets
139
+
140
+
141
+ cli = clearskies.contexts.Cli(
142
+ clearskies.endpoints.Callable(
143
+ api_demo,
144
+ model_class=Widget,
145
+ return_records=True,
146
+ readable_column_names=["id", "name", "category", "cost", "created_at", "updated_at"],
147
+ ),
148
+ classes=[Widget],
149
+ )
150
+ cli()
151
+ ```
152
+
153
+ The above app declares a model class that matches the output from our server/api. Note that the id,
154
+ created_at, and updated_at columns all changed types to their "plain" types. This is very normal. The API
155
+ is the one that is responsible for assigning ids and setting created/updated timestamps, so from the
156
+ perspective of our client, these are plain string/datetime fields. If we used the UUID or created/updated
157
+ columns, then when the client called the API it would try to set all of these columns. Since they are not
158
+ writeable columns, the API would return an input error. If you launch the above server/API and then run
159
+ the given client script, you'll see output like this:
160
+
161
+ ```json
162
+ {
163
+ "status": "success",
164
+ "error": "",
165
+ "data": [
166
+ {
167
+ "id": "54eef01d-7c87-4959-b525-dcb9047d9692",
168
+ "name": "Mabob",
169
+ "category": "Doohicky",
170
+ "cost": 150.0,
171
+ "created_at": "2025-06-13T15:19:27+00:00",
172
+ "updated_at": "2025-06-13T15:19:27+00:00",
173
+ },
174
+ {
175
+ "id": "ed1421b8-88ad-49d2-a130-c34b4ac4dfcf",
176
+ "name": "Thinga",
177
+ "category": "Doohickey",
178
+ "cost": 125.0,
179
+ "created_at": "2025-06-13T15:19:27+00:00",
180
+ "updated_at": "2025-06-13T15:19:27+00:00",
181
+ },
182
+ ],
183
+ "pagination": {},
184
+ "input_errors": {},
185
+ }
186
+ ```
187
+ """
3
188
 
4
- class SecretBearer:
5
189
  is_public = False
6
190
  can_authorize = False
7
- has_dynamic_credentials = False
8
- _environment = None
9
- _secrets = None
10
- _logging = None
11
- _secret = None
12
- _header_prefix = None
13
- _header_prefix_length = None
14
- _documentation_security_name = None
15
-
16
- def __init__(self, secrets, environment, logging):
17
- self._environment = environment
18
- self._secrets = secrets
19
- self._logging = logging
20
-
21
- def configure(
22
- self, secret_key=None, secret=None, environment_key=None, header_prefix=None, documentation_security_name=None
191
+
192
+ environment = di.inject.Environment()
193
+ secrets = di.inject.Secrets()
194
+
195
+ """
196
+ The path in our secret manager from which the secret should be fetched.
197
+
198
+ Of course, to use `secret_key`, you must also provide a secret manager. The below example uses the dependency
199
+ injection system to create a faux secret manager to demonstrate how it works in general:
200
+
201
+ ```python
202
+ from types import SimpleNamespace
203
+ import clearskies
204
+
205
+ def fetch_secret(path):
206
+ if path == "/path/to/my/secret":
207
+ return "SUPERSECRET"
208
+ raise KeyError(f"Attempt to fetch non-existent secret: {path}")
209
+
210
+ fake_secret_manager = SimpleNamespace(get=fetch_secret)
211
+
212
+ wsgi = clearskies.contexts.WsgiRef(
213
+ clearskies.endpoints.Callable(
214
+ lambda: {"hello": "world"},
215
+ authentication=clearskies.authentication.SecretBearer(secret_key="/path/to/my/secret"),
216
+ ),
217
+ bindings={
218
+ "secrets": fake_secret_manager,
219
+ },
220
+ )
221
+ wsgi()
222
+ ```
223
+
224
+ And when invoked:
225
+
226
+ ```bash
227
+ $ curl 'http://localhost:8080/' -H "Authorization: SUPERSECRET" | jq
228
+ {
229
+ "status": "success",
230
+ "error": "",
231
+ "data": {
232
+ "hello": "world"
233
+ },
234
+ "pagination": {},
235
+ "input_errors": {}
236
+ }
237
+
238
+ $ curl 'http://localhost:8080/' -H "Authorization: definitely-not-the-api-key" | jq
239
+ {
240
+ "status": "client_error",
241
+ "error": "Not Authenticated",
242
+ "data": [],
243
+ "pagination": {},
244
+ "input_errors": {}
245
+ }
246
+ ```
247
+
248
+ """
249
+ secret_key = configs.String(default="")
250
+
251
+ """
252
+ The path in our secret manager where an alternate secret can also be fetched
253
+
254
+ The alternate secret is exclusively used to authenticate incoming requests. This allows for secret
255
+ rotation - Point secret_key to a new secret and alternate_secret_key to the old secret. Both will then
256
+ be accepted and you can migrate your applications to only send the new secret. Once they are all updated,
257
+ remove the alternate_secret_key:
258
+
259
+ ```python
260
+ from types import SimpleNamespace
261
+ import clearskies
262
+
263
+ def fetch_secret(path):
264
+ if path == "/path/to/my/secret":
265
+ return "SUPERSECRET"
266
+ if path == "/path/to/alternate/secret":
267
+ return "ALSOOKAY"
268
+ raise KeyError(f"Attempt to fetch non-existent secret: {path}")
269
+
270
+ fake_secret_manager = SimpleNamespace(get=fetch_secret)
271
+
272
+ wsgi = clearskies.contexts.WsgiRef(
273
+ clearskies.endpoints.Callable(
274
+ lambda: {"hello": "world"},
275
+ authentication=clearskies.authentication.SecretBearer(
276
+ secret_key="/path/to/my/secret",
277
+ alternate_secret_key="/path/to/alternate/secret",
278
+ ),
279
+ ),
280
+ bindings={
281
+ "secrets": fake_secret_manager,
282
+ },
283
+ )
284
+ wsgi()
285
+ ```
286
+
287
+ And when invoked:
288
+
289
+ ```bash
290
+ $ curl 'http://localhost:8080/' -H "Authorization: SUPERSECRET" | jq
291
+ {
292
+ "status": "success",
293
+ "error": "",
294
+ "data": {
295
+ "hello": "world"
296
+ },
297
+ "pagination": {},
298
+ "input_errors": {}
299
+ }
300
+
301
+ $ curl 'http://localhost:8080/' -H "Authorization: ALSOOKAY" | jq
302
+ {
303
+ "status": "success",
304
+ "error": "",
305
+ "data": {
306
+ "hello": "world"
307
+ },
308
+ "pagination": {},
309
+ "input_errors": {}
310
+ }
311
+
312
+ $ curl 'http://localhost:8080/' -H "Authorization: NOTTHESECRET" | jq
313
+ {
314
+ "status": "client_error",
315
+ "error": "Not Authenticated",
316
+ "data": [],
317
+ "pagination": {},
318
+ "input_errors": {}
319
+ }
320
+ ```
321
+
322
+ """
323
+ alternate_secret_key = configs.String(default="")
324
+
325
+ """
326
+ The name of the environment variable from which we should fetch our key.
327
+ """
328
+ environment_key = configs.String(default="")
329
+
330
+ """
331
+ The name of an alternate environment variable from which we should fetch our key.
332
+
333
+ This allows for secret rotation by allowing the API to accept a secret from two different
334
+ environment variables: an old value and a new value. You can then migrate your client applications
335
+ to use the new key and, once they are all migrated, remove the old key from the application
336
+ configuration. Here's an example:
337
+
338
+ ```python
339
+ import os
340
+ import clearskies
341
+
342
+ os.environ["MY_AUTH_SECRET"] = "SUPERSECRET"
343
+ os.environ["MY_ALT_SECRET"] = "ALSOOKAY"
344
+
345
+ wsgi = clearskies.contexts.WsgiRef(
346
+ clearskies.endpoints.Callable(
347
+ lambda: {"hello": "world"},
348
+ authentication=clearskies.authentication.SecretBearer(
349
+ environment_key="MY_AUTH_SECRET",
350
+ alternate_environment_key="MY_ALT_SECRET",
351
+ ),
352
+ ),
353
+ )
354
+ wsgi()
355
+ ```
356
+
357
+ And when invoked:
358
+
359
+ ```bash
360
+ $ curl 'http://localhost:8080/' -H "Authorization: SUPERSECRET" | jq
361
+ {
362
+ "status": "success",
363
+ "error": "",
364
+ "data": {
365
+ "hello": "world"
366
+ },
367
+ "pagination": {},
368
+ "input_errors": {}
369
+ }
370
+
371
+ $ curl 'http://localhost:8080/' -H "Authorization: ALSOOKAY" | jq
372
+ {
373
+ "status": "success",
374
+ "error": "",
375
+ "data": {
376
+ "hello": "world"
377
+ },
378
+ "pagination": {},
379
+ "input_errors": {}
380
+ }
381
+
382
+ $ curl 'http://localhost:8080/' -H "Authorization: NOTTHESECRET" | jq
383
+ {
384
+ "status": "client_error",
385
+ "error": "Not Authenticated",
386
+ "data": [],
387
+ "pagination": {},
388
+ "input_errors": {}
389
+ }
390
+ ```
391
+
392
+ """
393
+ alternate_environment_key = configs.String(default="")
394
+
395
+ """
396
+ The expected prefix (if any) that should come before the secret key in the authorization header.
397
+
398
+ This applies to both the incoming authentication process and outgoing authentication headers. Some systems
399
+ require a prefix before the auth token in the HTTP header (e.g. `Authorization: TOKEN [auth key here]`).
400
+ You can provide that prefix to `header_prefix` in order for the endpoint to require a prefix or the api backend
401
+ to provide such a prefix. Note that the prefix is case-insensitive and it does not assume a space between the
402
+ prefix and the token (so, if you want a space, you must explicitly put it in the prefix). Here's an example:
403
+
404
+ ```python
405
+ import os
406
+ import clearskies
407
+
408
+ os.environ["MY_AUTH_SECRET"] = "SUPERSECRET"
409
+
410
+ wsgi = clearskies.contexts.WsgiRef(
411
+ clearskies.endpoints.Callable(
412
+ lambda: {"hello": "world"},
413
+ authentication=clearskies.authentication.SecretBearer(
414
+ environment_key="MY_AUTH_SECRET",
415
+ header_prefix="secret-token ",
416
+ ),
417
+ ),
418
+ )
419
+ wsgi()
420
+ ```
421
+
422
+ And then usage:
423
+
424
+ ```bash
425
+ $ curl 'http://localhost:8080/' -H "Authorization: SECRET-TOKEN SUPERSECRET" | jq
426
+ {
427
+ "status": "success",
428
+ "error": "",
429
+ "data": {
430
+ "hello": "world"
431
+ },
432
+ "pagination": {},
433
+ "input_errors": {}
434
+ }
435
+
436
+ $ curl 'http://localhost:8080/' -H "Authorization: SUPERSECRET" | jq
437
+ {
438
+ "status": "client_error",
439
+ "error": "Not Authenticated",
440
+ "data": [],
441
+ "pagination": {},
442
+ "input_errors": {}
443
+ }
444
+ ```
445
+ """
446
+ header_prefix = configs.String(default="")
447
+
448
+ """
449
+ The length of our header prefix
450
+ """
451
+ header_prefix_length = None
452
+
453
+ """
454
+ The name of our security scheme in the auto-generated documentation
455
+ """
456
+ documentation_security_name = configs.String(default="ApiKey")
457
+
458
+ _secret: str | None = None
459
+ _alternate_secret: str | None = None
460
+
461
+ @decorators.parameters_to_properties
462
+ def __init__(
463
+ self,
464
+ secret_key: str = "",
465
+ alternate_secret_key: str = "",
466
+ environment_key: str = "",
467
+ alternate_environment_key: str = "",
468
+ header_prefix: str = "",
469
+ documentation_security_name: str = "",
23
470
  ):
24
- if secret_key:
25
- self._secret = self._secrets.get(secret_key)
26
- elif environment_key:
27
- self._secret = self._environment.get(environment_key)
28
- elif secret:
29
- self._secret = secret
30
- else:
31
- raise ValueError(
32
- "Must set either 'secret_key', 'environment_key', or 'secret', when configuring the SecretBearer"
471
+ if not secret_key and not environment_key:
472
+ raise ValueError("Must set either 'secret_key' or 'environment_key' when configuring the SecretBearer")
473
+ self.header_prefix_length = len(header_prefix)
474
+ self.finalize_and_validate_configuration()
475
+
476
+ @property
477
+ def secret(self):
478
+ if not self._secret:
479
+ self._secret = (
480
+ self.secrets.get(self.secret_key) if self.secret_key else self.environment.get(self.environment_key)
33
481
  )
34
- self._header_prefix = header_prefix if header_prefix else "authorization "
35
- self._header_prefix_length = len(self._header_prefix)
36
- self._documentation_security_name = documentation_security_name
482
+ return self._secret
483
+
484
+ def clear_credential_cache(self):
485
+ if self.secret_key:
486
+ self._secret = None # type: ignore
487
+
488
+ @property
489
+ def alternate_secret(self):
490
+ if not self.alternate_secret_key and not self.alternate_environment_key:
491
+ return ""
492
+
493
+ if not self._alternate_secret:
494
+ self._alternate_secret = (
495
+ self.secrets.get(self.alternate_secret_key)
496
+ if self.alternate_secret_key
497
+ else self.environment.get(self.alternate_environment_key)
498
+ )
499
+ return self._alternate_secret
37
500
 
38
501
  def headers(self, retry_auth=False):
39
502
  self._configured_guard()
40
- return {"Authorization": f"{self._header_prefix}{self._secret}"}
503
+ if retry_auth:
504
+ self.clear_credential_cache()
505
+ return {"Authorization": f"{self.header_prefix}{self.secret}"}
41
506
 
42
507
  def authenticate(self, input_output):
43
508
  self._configured_guard()
44
- auth_header = input_output.get_request_header("authorization", True)
45
- if auth_header[: self._header_prefix_length].lower() != self._header_prefix.lower():
46
- self._logging.debug(
47
- "Authentication failure due to prefix mismatch. Configured prefix: "
48
- + self._header_prefix.lower()
49
- + ". Found prefix: "
50
- + auth_header[: self._header_prefix_length].lower()
51
- )
509
+ auth_header = input_output.request_headers.authorization
510
+ if not auth_header:
511
+ return False
512
+ if auth_header[: self.header_prefix_length].lower() != self.header_prefix.lower():
513
+ # self._logging.debug(
514
+ # "Authentication failure due to prefix mismatch. Configured prefix: "
515
+ # + self._header_prefix.lower()
516
+ # + ". Found prefix: "
517
+ # + auth_header[: self._header_prefix_length].lower()
518
+ # )
52
519
  return False
53
- if self._secret == auth_header[self._header_prefix_length :]:
54
- self._logging.debug("Authentication success")
520
+ provided_secret = auth_header[self.header_prefix_length :]
521
+ if self.secret == provided_secret:
522
+ # self._logging.debug("Authentication success")
55
523
  return True
56
- self._logging.debug("Authentication failure due to secret mismatch")
524
+ if self.alternate_secret and provided_secret == self._alternate_secret:
525
+ # self._logging.debug("Authentication success with alternate secret")
526
+ return True
527
+ # self._logging.debug("Authentication failure due to secret mismatch")
57
528
  return False
58
529
 
59
530
  def authorize(self, authorization):
@@ -63,7 +534,7 @@ class SecretBearer:
63
534
  cors.add_header("Authorization")
64
535
 
65
536
  def _configured_guard(self):
66
- if not self._secret:
537
+ if not self.secret:
67
538
  raise ValueError("Attempted to use SecretBearer authentication class without providing the configuration")
68
539
 
69
540
  def documentation_request_parameters(self):
@@ -77,4 +548,4 @@ class SecretBearer:
77
548
  }
78
549
 
79
550
  def documentation_security_scheme_name(self):
80
- return self._documentation_security_name if self._documentation_security_name is not None else "ApiKey"
551
+ return self.documentation_security_name
@@ -1,7 +1,7 @@
1
+ from .oai3_json import Oai3Json
1
2
  from .oai3_schema_resolver import OAI3SchemaResolver
2
- from .oai3_json import OAI3JSON
3
3
 
4
4
  __all__ = [
5
5
  "OAI3SchemaResolver",
6
- "OAI3JSON",
6
+ "Oai3Json",
7
7
  ]
@@ -1,12 +1,14 @@
1
- from .request import Request
2
1
  import json
2
+ from typing import Any
3
+
4
+ from .request import Request
3
5
 
4
6
 
5
- class OAI3JSON:
6
- requests = None
7
- formatted = None
8
- models = None
9
- security_schemes = None
7
+ class Oai3Json:
8
+ requests: Any = None
9
+ formatted: Any = None
10
+ models: Any = None
11
+ security_schemes: Any = None
10
12
 
11
13
  def __init__(self, oai3_schema_resolver):
12
14
  self.oai3_schema_resolver = oai3_schema_resolver
@@ -56,7 +58,7 @@ class OAI3JSON:
56
58
  return json.dumps(data)
57
59
 
58
60
  def convert(self):
59
- paths = {}
61
+ paths: dict[str, Any] = {}
60
62
  for request in self.formatted:
61
63
  absolute_path = "/" + request.relative_path.lstrip("/")
62
64
  if absolute_path not in paths:
@@ -65,10 +67,10 @@ class OAI3JSON:
65
67
  path_data = request.convert()
66
68
  for request_method, path_doc in path_data.items():
67
69
  if request_method in paths[absolute_path]:
68
- raise ValueError(f"Two routes had the same path and method: {absolute_path} - {request_method}")
70
+ continue
69
71
  paths[absolute_path][request_method] = path_doc
70
72
 
71
- data = {
73
+ data: dict[str, Any] = {
72
74
  "openapi": "3.0.0",
73
75
  "paths": paths,
74
76
  "components": {},
@@ -1,7 +1,10 @@
1
+ from typing import Any
2
+
3
+
1
4
  class Parameter:
2
- name = None
3
- parameter = None
4
- required = None
5
+ name: str = ""
6
+ parameter: Any = None
7
+ required: bool = False
5
8
  location_map = {
6
9
  "url_parameter": "query",
7
10
  "header": "header",
@@ -1,12 +1,14 @@
1
- from .response import Response
1
+ from typing import Any
2
+
3
+ from ...schema import Object
2
4
  from .parameter import Parameter
3
- from ...schema import Object, Array
5
+ from .response import Response
4
6
 
5
7
 
6
8
  class Request:
7
- formatted_responses = None
8
- request = None
9
- relative_path = None
9
+ formatted_responses: Any = None
10
+ request: Any = None
11
+ relative_path: str = ""
10
12
 
11
13
  def __init__(self, oai3_schema_resolver):
12
14
  self.oai3_schema_resolver = oai3_schema_resolver
@@ -1,8 +1,11 @@
1
+ from typing import Any
2
+
3
+
1
4
  class Response:
2
- response = None
3
- formatted_schema = None
4
- oai3_schema_resolver = None
5
- status_code = None
5
+ response: Any = None
6
+ formatted_schema: Any = None
7
+ oai3_schema_resolver: Any = None
8
+ status_code: Any = None
6
9
 
7
10
  def __init__(self, oai3_schema_resolver):
8
11
  self.oai3_schema_resolver = oai3_schema_resolver