clear-skies 1.22.31__py3-none-any.whl → 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of clear-skies might be problematic. Click here for more details.

Files changed (344) hide show
  1. {clear_skies-1.22.31.dist-info → clear_skies-2.0.0.dist-info}/METADATA +11 -13
  2. clear_skies-2.0.0.dist-info/RECORD +248 -0
  3. {clear_skies-1.22.31.dist-info → clear_skies-2.0.0.dist-info}/WHEEL +1 -1
  4. clearskies/__init__.py +42 -25
  5. clearskies/action.py +7 -0
  6. clearskies/authentication/__init__.py +8 -41
  7. clearskies/authentication/authentication.py +42 -0
  8. clearskies/authentication/authorization.py +4 -9
  9. clearskies/authentication/authorization_pass_through.py +11 -9
  10. clearskies/authentication/jwks.py +128 -58
  11. clearskies/authentication/public.py +3 -38
  12. clearskies/authentication/secret_bearer.py +516 -54
  13. clearskies/autodoc/formats/oai3_json/__init__.py +1 -1
  14. clearskies/autodoc/formats/oai3_json/oai3_json.py +9 -7
  15. clearskies/autodoc/formats/oai3_json/parameter.py +6 -3
  16. clearskies/autodoc/formats/oai3_json/request.py +7 -5
  17. clearskies/autodoc/formats/oai3_json/response.py +7 -4
  18. clearskies/autodoc/formats/oai3_json/schema/object.py +4 -1
  19. clearskies/autodoc/request/__init__.py +2 -0
  20. clearskies/autodoc/request/header.py +4 -6
  21. clearskies/autodoc/request/json_body.py +4 -6
  22. clearskies/autodoc/request/parameter.py +8 -0
  23. clearskies/autodoc/request/request.py +7 -4
  24. clearskies/autodoc/request/url_parameter.py +4 -6
  25. clearskies/autodoc/request/url_path.py +4 -6
  26. clearskies/autodoc/schema/__init__.py +4 -2
  27. clearskies/autodoc/schema/array.py +5 -6
  28. clearskies/autodoc/schema/boolean.py +4 -10
  29. clearskies/autodoc/schema/date.py +0 -3
  30. clearskies/autodoc/schema/datetime.py +1 -4
  31. clearskies/autodoc/schema/double.py +0 -3
  32. clearskies/autodoc/schema/enum.py +4 -2
  33. clearskies/autodoc/schema/integer.py +4 -9
  34. clearskies/autodoc/schema/long.py +0 -3
  35. clearskies/autodoc/schema/number.py +4 -9
  36. clearskies/autodoc/schema/object.py +5 -7
  37. clearskies/autodoc/schema/password.py +0 -3
  38. clearskies/autodoc/schema/schema.py +11 -0
  39. clearskies/autodoc/schema/string.py +4 -10
  40. clearskies/backends/__init__.py +55 -20
  41. clearskies/backends/api_backend.py +1100 -284
  42. clearskies/backends/backend.py +40 -84
  43. clearskies/backends/cursor_backend.py +236 -186
  44. clearskies/backends/memory_backend.py +519 -226
  45. clearskies/backends/secrets_backend.py +75 -31
  46. clearskies/column.py +1232 -0
  47. clearskies/columns/__init__.py +71 -0
  48. clearskies/columns/audit.py +205 -0
  49. clearskies/columns/belongs_to_id.py +483 -0
  50. clearskies/columns/belongs_to_model.py +128 -0
  51. clearskies/columns/belongs_to_self.py +105 -0
  52. clearskies/columns/boolean.py +109 -0
  53. clearskies/columns/category_tree.py +275 -0
  54. clearskies/columns/category_tree_ancestors.py +51 -0
  55. clearskies/columns/category_tree_children.py +127 -0
  56. clearskies/columns/category_tree_descendants.py +48 -0
  57. clearskies/columns/created.py +94 -0
  58. clearskies/columns/created_by_authorization_data.py +116 -0
  59. clearskies/columns/created_by_header.py +99 -0
  60. clearskies/columns/created_by_ip.py +92 -0
  61. clearskies/columns/created_by_routing_data.py +96 -0
  62. clearskies/columns/created_by_user_agent.py +92 -0
  63. clearskies/columns/date.py +230 -0
  64. clearskies/columns/datetime.py +278 -0
  65. clearskies/columns/email.py +76 -0
  66. clearskies/columns/float.py +149 -0
  67. clearskies/columns/has_many.py +505 -0
  68. clearskies/columns/has_many_self.py +56 -0
  69. clearskies/columns/has_one.py +14 -0
  70. clearskies/columns/integer.py +156 -0
  71. clearskies/columns/json.py +122 -0
  72. clearskies/columns/many_to_many_ids.py +333 -0
  73. clearskies/columns/many_to_many_ids_with_data.py +270 -0
  74. clearskies/columns/many_to_many_models.py +154 -0
  75. clearskies/columns/many_to_many_pivots.py +133 -0
  76. clearskies/columns/phone.py +158 -0
  77. clearskies/columns/select.py +91 -0
  78. clearskies/columns/string.py +98 -0
  79. clearskies/columns/timestamp.py +160 -0
  80. clearskies/columns/updated.py +110 -0
  81. clearskies/columns/uuid.py +86 -0
  82. clearskies/configs/README.md +105 -0
  83. clearskies/configs/__init__.py +159 -0
  84. clearskies/configs/actions.py +43 -0
  85. clearskies/configs/any.py +13 -0
  86. clearskies/configs/any_dict.py +22 -0
  87. clearskies/configs/any_dict_or_callable.py +23 -0
  88. clearskies/configs/authentication.py +23 -0
  89. clearskies/configs/authorization.py +23 -0
  90. clearskies/configs/boolean.py +16 -0
  91. clearskies/configs/boolean_or_callable.py +18 -0
  92. clearskies/configs/callable_config.py +18 -0
  93. clearskies/configs/columns.py +34 -0
  94. clearskies/configs/conditions.py +30 -0
  95. clearskies/configs/config.py +21 -0
  96. clearskies/configs/datetime.py +18 -0
  97. clearskies/configs/datetime_or_callable.py +19 -0
  98. clearskies/configs/endpoint.py +23 -0
  99. clearskies/configs/float.py +16 -0
  100. clearskies/configs/float_or_callable.py +18 -0
  101. clearskies/configs/integer.py +16 -0
  102. clearskies/configs/integer_or_callable.py +18 -0
  103. clearskies/configs/joins.py +30 -0
  104. clearskies/configs/list_any_dict.py +30 -0
  105. clearskies/configs/list_any_dict_or_callable.py +31 -0
  106. clearskies/configs/model_class.py +35 -0
  107. clearskies/configs/model_column.py +65 -0
  108. clearskies/configs/model_columns.py +56 -0
  109. clearskies/configs/model_destination_name.py +25 -0
  110. clearskies/configs/model_to_id_column.py +43 -0
  111. clearskies/configs/readable_model_column.py +9 -0
  112. clearskies/configs/readable_model_columns.py +9 -0
  113. clearskies/configs/schema.py +23 -0
  114. clearskies/configs/searchable_model_columns.py +9 -0
  115. clearskies/configs/security_headers.py +39 -0
  116. clearskies/configs/select.py +26 -0
  117. clearskies/configs/select_list.py +47 -0
  118. clearskies/configs/string.py +29 -0
  119. clearskies/configs/string_dict.py +32 -0
  120. clearskies/configs/string_list.py +32 -0
  121. clearskies/configs/string_list_or_callable.py +35 -0
  122. clearskies/configs/string_or_callable.py +18 -0
  123. clearskies/configs/timedelta.py +18 -0
  124. clearskies/configs/timezone.py +18 -0
  125. clearskies/configs/url.py +23 -0
  126. clearskies/configs/validators.py +45 -0
  127. clearskies/configs/writeable_model_column.py +9 -0
  128. clearskies/configs/writeable_model_columns.py +9 -0
  129. clearskies/configurable.py +76 -0
  130. clearskies/contexts/__init__.py +8 -8
  131. clearskies/contexts/cli.py +5 -42
  132. clearskies/contexts/context.py +78 -56
  133. clearskies/contexts/wsgi.py +13 -30
  134. clearskies/contexts/wsgi_ref.py +49 -0
  135. clearskies/di/__init__.py +10 -7
  136. clearskies/di/additional_config.py +115 -4
  137. clearskies/di/additional_config_auto_import.py +12 -0
  138. clearskies/di/di.py +742 -121
  139. clearskies/di/inject/__init__.py +23 -0
  140. clearskies/di/inject/by_class.py +21 -0
  141. clearskies/di/inject/by_name.py +18 -0
  142. clearskies/di/inject/di.py +13 -0
  143. clearskies/di/inject/environment.py +14 -0
  144. clearskies/di/inject/input_output.py +20 -0
  145. clearskies/di/inject/now.py +13 -0
  146. clearskies/di/inject/requests.py +13 -0
  147. clearskies/di/inject/secrets.py +14 -0
  148. clearskies/di/inject/utcnow.py +13 -0
  149. clearskies/di/inject/uuid.py +15 -0
  150. clearskies/di/injectable.py +29 -0
  151. clearskies/di/injectable_properties.py +131 -0
  152. clearskies/end.py +183 -0
  153. clearskies/endpoint.py +1309 -0
  154. clearskies/endpoint_group.py +297 -0
  155. clearskies/endpoints/__init__.py +23 -0
  156. clearskies/endpoints/advanced_search.py +526 -0
  157. clearskies/endpoints/callable.py +387 -0
  158. clearskies/endpoints/create.py +202 -0
  159. clearskies/endpoints/delete.py +139 -0
  160. clearskies/endpoints/get.py +275 -0
  161. clearskies/endpoints/health_check.py +181 -0
  162. clearskies/endpoints/list.py +573 -0
  163. clearskies/endpoints/restful_api.py +427 -0
  164. clearskies/endpoints/simple_search.py +286 -0
  165. clearskies/endpoints/update.py +190 -0
  166. clearskies/environment.py +5 -3
  167. clearskies/exceptions/__init__.py +17 -0
  168. clearskies/{handlers/exceptions/input_error.py → exceptions/input_errors.py} +1 -1
  169. clearskies/exceptions/moved_permanently.py +3 -0
  170. clearskies/exceptions/moved_temporarily.py +3 -0
  171. clearskies/exceptions/not_found.py +2 -0
  172. clearskies/functional/__init__.py +2 -2
  173. clearskies/functional/routing.py +92 -0
  174. clearskies/functional/string.py +19 -11
  175. clearskies/functional/validations.py +61 -9
  176. clearskies/input_outputs/__init__.py +9 -7
  177. clearskies/input_outputs/cli.py +130 -142
  178. clearskies/input_outputs/exceptions/__init__.py +1 -1
  179. clearskies/input_outputs/headers.py +45 -0
  180. clearskies/input_outputs/input_output.py +91 -122
  181. clearskies/input_outputs/programmatic.py +69 -0
  182. clearskies/input_outputs/wsgi.py +23 -38
  183. clearskies/model.py +489 -184
  184. clearskies/parameters_to_properties.py +31 -0
  185. clearskies/query/__init__.py +12 -0
  186. clearskies/query/condition.py +223 -0
  187. clearskies/query/join.py +136 -0
  188. clearskies/query/query.py +196 -0
  189. clearskies/query/sort.py +27 -0
  190. clearskies/schema.py +82 -0
  191. clearskies/secrets/__init__.py +3 -31
  192. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer.py +15 -4
  193. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +11 -5
  194. clearskies/secrets/akeyless.py +88 -147
  195. clearskies/secrets/secrets.py +8 -8
  196. clearskies/security_header.py +8 -0
  197. clearskies/security_headers/__init__.py +8 -8
  198. clearskies/security_headers/cache_control.py +47 -110
  199. clearskies/security_headers/cors.py +40 -95
  200. clearskies/security_headers/csp.py +76 -151
  201. clearskies/security_headers/hsts.py +14 -16
  202. clearskies/test_base.py +8 -0
  203. clearskies/typing.py +11 -0
  204. clearskies/validator.py +25 -0
  205. clearskies/validators/__init__.py +33 -0
  206. clearskies/validators/after_column.py +62 -0
  207. clearskies/validators/before_column.py +13 -0
  208. clearskies/validators/in_the_future.py +32 -0
  209. clearskies/validators/in_the_future_at_least.py +11 -0
  210. clearskies/validators/in_the_future_at_most.py +10 -0
  211. clearskies/validators/in_the_past.py +32 -0
  212. clearskies/validators/in_the_past_at_least.py +10 -0
  213. clearskies/validators/in_the_past_at_most.py +10 -0
  214. clearskies/validators/maximum_length.py +26 -0
  215. clearskies/validators/maximum_value.py +29 -0
  216. clearskies/validators/minimum_length.py +26 -0
  217. clearskies/validators/minimum_value.py +29 -0
  218. clearskies/validators/required.py +35 -0
  219. clearskies/validators/timedelta.py +59 -0
  220. clearskies/validators/unique.py +31 -0
  221. clear_skies-1.22.31.dist-info/RECORD +0 -214
  222. clearskies/application.py +0 -29
  223. clearskies/authentication/auth0_jwks.py +0 -118
  224. clearskies/authentication/auth_exception.py +0 -2
  225. clearskies/authentication/jwks_jwcrypto.py +0 -51
  226. clearskies/backends/api_get_only_backend.py +0 -48
  227. clearskies/backends/example_backend.py +0 -43
  228. clearskies/backends/file_backend.py +0 -48
  229. clearskies/backends/json_backend.py +0 -7
  230. clearskies/backends/restful_api_advanced_search_backend.py +0 -103
  231. clearskies/binding_config.py +0 -16
  232. clearskies/column_types/__init__.py +0 -203
  233. clearskies/column_types/audit.py +0 -249
  234. clearskies/column_types/belongs_to.py +0 -271
  235. clearskies/column_types/boolean.py +0 -60
  236. clearskies/column_types/category_tree.py +0 -304
  237. clearskies/column_types/column.py +0 -373
  238. clearskies/column_types/created.py +0 -26
  239. clearskies/column_types/created_by_authorization_data.py +0 -26
  240. clearskies/column_types/created_by_header.py +0 -24
  241. clearskies/column_types/created_by_ip.py +0 -17
  242. clearskies/column_types/created_by_routing_data.py +0 -25
  243. clearskies/column_types/created_by_user_agent.py +0 -17
  244. clearskies/column_types/created_micro.py +0 -26
  245. clearskies/column_types/datetime.py +0 -109
  246. clearskies/column_types/datetime_micro.py +0 -12
  247. clearskies/column_types/email.py +0 -18
  248. clearskies/column_types/float.py +0 -43
  249. clearskies/column_types/has_many.py +0 -179
  250. clearskies/column_types/has_one.py +0 -60
  251. clearskies/column_types/integer.py +0 -41
  252. clearskies/column_types/json.py +0 -25
  253. clearskies/column_types/many_to_many.py +0 -278
  254. clearskies/column_types/many_to_many_with_data.py +0 -162
  255. clearskies/column_types/phone.py +0 -48
  256. clearskies/column_types/select.py +0 -11
  257. clearskies/column_types/string.py +0 -24
  258. clearskies/column_types/timestamp.py +0 -73
  259. clearskies/column_types/updated.py +0 -26
  260. clearskies/column_types/updated_micro.py +0 -26
  261. clearskies/column_types/uuid.py +0 -25
  262. clearskies/columns.py +0 -123
  263. clearskies/condition_parser.py +0 -172
  264. clearskies/contexts/build_context.py +0 -54
  265. clearskies/contexts/convert_to_application.py +0 -190
  266. clearskies/contexts/extract_handler.py +0 -37
  267. clearskies/contexts/test.py +0 -94
  268. clearskies/decorators/__init__.py +0 -41
  269. clearskies/decorators/allow_non_json_bodies.py +0 -9
  270. clearskies/decorators/auth0_jwks.py +0 -22
  271. clearskies/decorators/authorization.py +0 -10
  272. clearskies/decorators/binding_classes.py +0 -9
  273. clearskies/decorators/binding_modules.py +0 -9
  274. clearskies/decorators/bindings.py +0 -9
  275. clearskies/decorators/create.py +0 -10
  276. clearskies/decorators/delete.py +0 -10
  277. clearskies/decorators/docs.py +0 -14
  278. clearskies/decorators/get.py +0 -10
  279. clearskies/decorators/jwks.py +0 -26
  280. clearskies/decorators/merge.py +0 -124
  281. clearskies/decorators/patch.py +0 -10
  282. clearskies/decorators/post.py +0 -10
  283. clearskies/decorators/public.py +0 -11
  284. clearskies/decorators/response_headers.py +0 -10
  285. clearskies/decorators/return_raw_response.py +0 -9
  286. clearskies/decorators/schema.py +0 -10
  287. clearskies/decorators/secret_bearer.py +0 -24
  288. clearskies/decorators/security_headers.py +0 -10
  289. clearskies/di/standard_dependencies.py +0 -151
  290. clearskies/handlers/__init__.py +0 -41
  291. clearskies/handlers/advanced_search.py +0 -271
  292. clearskies/handlers/base.py +0 -479
  293. clearskies/handlers/callable.py +0 -192
  294. clearskies/handlers/create.py +0 -35
  295. clearskies/handlers/crud_by_method.py +0 -18
  296. clearskies/handlers/database_connector.py +0 -32
  297. clearskies/handlers/delete.py +0 -61
  298. clearskies/handlers/exceptions/__init__.py +0 -5
  299. clearskies/handlers/exceptions/not_found.py +0 -3
  300. clearskies/handlers/get.py +0 -156
  301. clearskies/handlers/health_check.py +0 -59
  302. clearskies/handlers/input_processing.py +0 -79
  303. clearskies/handlers/list.py +0 -530
  304. clearskies/handlers/mygrations.py +0 -82
  305. clearskies/handlers/request_method_routing.py +0 -47
  306. clearskies/handlers/restful_api.py +0 -218
  307. clearskies/handlers/routing.py +0 -62
  308. clearskies/handlers/schema_helper.py +0 -128
  309. clearskies/handlers/simple_routing.py +0 -206
  310. clearskies/handlers/simple_routing_route.py +0 -197
  311. clearskies/handlers/simple_search.py +0 -136
  312. clearskies/handlers/update.py +0 -102
  313. clearskies/handlers/write.py +0 -193
  314. clearskies/input_requirements/__init__.py +0 -78
  315. clearskies/input_requirements/after.py +0 -36
  316. clearskies/input_requirements/before.py +0 -36
  317. clearskies/input_requirements/in_the_future_at_least.py +0 -19
  318. clearskies/input_requirements/in_the_future_at_most.py +0 -19
  319. clearskies/input_requirements/in_the_past_at_least.py +0 -19
  320. clearskies/input_requirements/in_the_past_at_most.py +0 -19
  321. clearskies/input_requirements/maximum_length.py +0 -19
  322. clearskies/input_requirements/maximum_value.py +0 -19
  323. clearskies/input_requirements/minimum_length.py +0 -22
  324. clearskies/input_requirements/minimum_value.py +0 -19
  325. clearskies/input_requirements/required.py +0 -23
  326. clearskies/input_requirements/requirement.py +0 -25
  327. clearskies/input_requirements/time_delta.py +0 -38
  328. clearskies/input_requirements/unique.py +0 -18
  329. clearskies/mocks/__init__.py +0 -7
  330. clearskies/mocks/input_output.py +0 -124
  331. clearskies/mocks/models.py +0 -142
  332. clearskies/models.py +0 -350
  333. clearskies/security_headers/base.py +0 -12
  334. clearskies/tests/simple_api/models/__init__.py +0 -2
  335. clearskies/tests/simple_api/models/status.py +0 -23
  336. clearskies/tests/simple_api/models/user.py +0 -21
  337. clearskies/tests/simple_api/users_api.py +0 -64
  338. {clear_skies-1.22.31.dist-info → clear_skies-2.0.0.dist-info}/LICENSE +0 -0
  339. /clearskies/{contexts/bash.py → autodoc/py.typed} +0 -0
  340. /clearskies/{handlers/exceptions → exceptions}/authentication.py +0 -0
  341. /clearskies/{handlers/exceptions → exceptions}/authorization.py +0 -0
  342. /clearskies/{handlers/exceptions → exceptions}/client_error.py +0 -0
  343. /clearskies/{tests/__init__.py → input_outputs/py.typed} +0 -0
  344. /clearskies/{tests/simple_api/__init__.py → py.typed} +0 -0
@@ -1,62 +1,532 @@
1
- from requests.auth import AuthBase
2
- from requests.models import PreparedRequest
1
+ import clearskies.configs
2
+ import clearskies.di
3
+ import clearskies.parameters_to_properties
4
+ from clearskies import autodoc
5
+ from clearskies.authentication.authentication import Authentication
3
6
 
4
- from .. import autodoc
5
7
 
8
+ class SecretBearer(Authentication, clearskies.di.InjectableProperties):
9
+ """
10
+ Secret Bearer performs authentication by checking against a static API key stored in either environment variables or a secret manager.
11
+
12
+ This can be used in two different ways:
13
+
14
+ 1. Attached to an endpoint to enforce authentication
15
+ 2. Attached to an API backend to specify how to authenticate to the API endpoint.
16
+
17
+ ### Authenticating Endpoints.
18
+
19
+ When attached to an endpoint this will enforce authentication. Clients authenticate themselves by providing the secret value
20
+ via the `authorization` header. In the following example we configure the secret bearer class to get the secretfrom an
21
+ environment variable which is set in the code itself. Normally you wouldn't set environment variables like this,
22
+ but it's done here to create a self-contained example that is easy to run:
23
+
24
+ ```python
25
+ import os
26
+ import clearskies
27
+
28
+ os.environ["MY_AUTH_SECRET"] = "SUPERSECRET"
29
+
30
+ wsgi = clearskies.contexts.WsgiRef(
31
+ clearskies.endpoints.Callable(
32
+ lambda: {"hello": "world"},
33
+ authentication=clearskies.authentication.SecretBearer(environment_key="MY_AUTH_SECRET"),
34
+ )
35
+ )
36
+ wsgi()
37
+ ```
38
+ We can then call it with and without the authentication header:
39
+
40
+ ```bash
41
+ $ curl 'http://localhost:8080' -H 'Authorization: SUPERSECRET' | jq
42
+ {
43
+ "status": "success",
44
+ "error": "",
45
+ "data": {
46
+ "hello": "world"
47
+ },
48
+ "pagination": {},
49
+ "input_errors": {}
50
+ }
51
+
52
+ $ curl 'http://localhost:8080' | jq
53
+ {
54
+ "status": "client_error",
55
+ "error": "Not Authenticated",
56
+ "data": [],
57
+ "pagination": {},
58
+ "input_errors": {}
59
+ }
60
+
61
+ $ curl 'http://localhost:8080' -H 'Authorization: NOTTHESECRET' | jq
62
+ {
63
+ "status": "client_error",
64
+ "error": "Not Authenticated",
65
+ "data": [],
66
+ "pagination": {},
67
+ "input_errors": {}
68
+ }
69
+ ```
70
+
71
+ ### Authenticating to APIs
72
+
73
+ The secret bearer class can also be attached to an API Backend to provide authentication to remote APIs. To
74
+ demonstrate, here is an example server that expects a secret token in the authorization header:
75
+
76
+ ```python
77
+ import os
78
+ import clearskies
79
+ from clearskies import columns
80
+
81
+ os.environ["MY_SECRET"] = "SUPERSECRET"
82
+
83
+
84
+ class Widget(clearskies.Model):
85
+ id_column_name = "id"
86
+ backend = clearskies.backends.MemoryBackend()
87
+
88
+ id = columns.Uuid()
89
+ name = columns.String()
90
+ category = columns.String()
91
+ cost = columns.Float()
92
+ created_at = columns.Created()
93
+ updated_at = columns.Updated()
94
+
95
+
96
+ wsgi = clearskies.contexts.WsgiRef(
97
+ clearskies.endpoints.RestfulApi(
98
+ url="widgets",
99
+ model_class=Widget,
100
+ authentication=clearskies.authentication.SecretBearer(environment_key="MY_SECRET"),
101
+ readable_column_names=["id", "name", "category", "cost", "created_at", "updated_at"],
102
+ writeable_column_names=["name", "category", "cost"],
103
+ sortable_column_names=["name", "category", "cost"],
104
+ searchable_column_names=["id", "name", "category", "cost"],
105
+ default_sort_column_name="name",
106
+ )
107
+ )
108
+ wsgi()
109
+ ```
110
+
111
+ Then here is a client app (you can launch the above server and then run this in a new terminal) that
112
+ similarly uses the secret bearer class to authenticate to the server:
113
+
114
+ ```python
115
+ import os
116
+ import clearskies
117
+ from clearskies import columns
118
+
119
+ os.environ["MY_SECRET"] = "SUPERSECRET"
120
+
121
+
122
+ class Widget(clearskies.Model):
123
+ id_column_name = "id"
124
+ backend = clearskies.backends.ApiBackend(
125
+ base_url="http://localhost:8080",
126
+ authentication=clearskies.authentication.SecretBearer(environment_key="MY_SECRET"),
127
+ )
128
+
129
+ id = columns.String()
130
+ name = columns.String()
131
+ category = columns.String()
132
+ cost = columns.Float()
133
+ created_at = columns.Datetime()
134
+ updated_at = columns.Datetime()
135
+
136
+
137
+ def api_demo(widgets: Widget) -> Widget:
138
+ thinga = widgets.create({"name": "Thinga", "category": "Doohickey", "cost": 125})
139
+ mabob = widgets.create({"name": "Mabob", "category": "Doohicky", "cost": 150})
140
+ return widgets
141
+
142
+
143
+ cli = clearskies.contexts.Cli(
144
+ clearskies.endpoints.Callable(
145
+ api_demo,
146
+ model_class=Widget,
147
+ return_records=True,
148
+ readable_column_names=["id", "name", "category", "cost", "created_at", "updated_at"],
149
+ ),
150
+ classes=[Widget],
151
+ )
152
+ cli()
153
+ ```
154
+
155
+ The above app declares a model class that matches the output from our server/api. Note that the id,
156
+ created_at, and updated_at columns all changed types to their "plain" types. This is very normal. The API
157
+ is the one that is responsible for assigning ids and setting created/updated timestamps, so from the
158
+ perspective of our client, these are plain string/datetime fields. If we used the UUID or created/updated
159
+ columns, then when the client called the API it would try to set all of these columns. Since they are not
160
+ writeable columns, the API would return an input error. If you launch the above server/API and then run
161
+ the given client script, you'll see output like this:
162
+
163
+ ```json
164
+ {
165
+ "status": "success",
166
+ "error": "",
167
+ "data": [
168
+ {
169
+ "id": "54eef01d-7c87-4959-b525-dcb9047d9692",
170
+ "name": "Mabob",
171
+ "category": "Doohicky",
172
+ "cost": 150.0,
173
+ "created_at": "2025-06-13T15:19:27+00:00",
174
+ "updated_at": "2025-06-13T15:19:27+00:00",
175
+ },
176
+ {
177
+ "id": "ed1421b8-88ad-49d2-a130-c34b4ac4dfcf",
178
+ "name": "Thinga",
179
+ "category": "Doohickey",
180
+ "cost": 125.0,
181
+ "created_at": "2025-06-13T15:19:27+00:00",
182
+ "updated_at": "2025-06-13T15:19:27+00:00",
183
+ },
184
+ ],
185
+ "pagination": {},
186
+ "input_errors": {},
187
+ }
188
+ ```
189
+ """
6
190
 
7
- class SecretBearer:
8
191
  is_public = False
9
192
  can_authorize = False
10
- has_dynamic_credentials = False
11
- _environment = None
12
- _secrets = None
13
- _logging = None
14
- _secret = None
15
- _header_prefix = None
16
- _header_prefix_length = None
17
- _documentation_security_name = None
18
-
19
- def __init__(self, secrets, environment, logging):
20
- self._environment = environment
21
- self._secrets = secrets
22
- self._logging = logging
23
-
24
- def configure(
25
- self, secret_key=None, secret=None, environment_key=None, header_prefix=None, documentation_security_name=None
193
+
194
+ environment = clearskies.di.inject.Environment()
195
+ secrets = clearskies.di.inject.Secrets()
196
+
197
+ """
198
+ The path in our secret manager from which the secret should be fetched.
199
+
200
+ Of course, to use `secret_key`, you must also provide a secret manager. The below example uses the dependency
201
+ injection system to create a faux secret manager to demonstrate how it works in general:
202
+
203
+ ```python
204
+ from types import SimpleNamespace
205
+ import clearskies
206
+
207
+ def fetch_secret(path):
208
+ if path == "/path/to/my/secret":
209
+ return "SUPERSECRET"
210
+ raise KeyError(f"Attempt to fetch non-existent secret: {path}")
211
+
212
+ fake_secret_manager = SimpleNamespace(get=fetch_secret)
213
+
214
+ wsgi = clearskies.contexts.WsgiRef(
215
+ clearskies.endpoints.Callable(
216
+ lambda: {"hello": "world"},
217
+ authentication=clearskies.authentication.SecretBearer(secret_key="/path/to/my/secret"),
218
+ ),
219
+ bindings={
220
+ "secrets": fake_secret_manager,
221
+ },
222
+ )
223
+ wsgi()
224
+ ```
225
+
226
+ And when invoked:
227
+
228
+ ```bash
229
+ $ curl 'http://localhost:8080/' -H "Authorization: SUPERSECRET" | jq
230
+ {
231
+ "status": "success",
232
+ "error": "",
233
+ "data": {
234
+ "hello": "world"
235
+ },
236
+ "pagination": {},
237
+ "input_errors": {}
238
+ }
239
+
240
+ $ curl 'http://localhost:8080/' -H "Authorization: definitely-not-the-api-key" | jq
241
+ {
242
+ "status": "client_error",
243
+ "error": "Not Authenticated",
244
+ "data": [],
245
+ "pagination": {},
246
+ "input_errors": {}
247
+ }
248
+ ```
249
+
250
+ """
251
+ secret_key = clearskies.configs.String(default="")
252
+
253
+ """
254
+ The path in our secret manager where an alternate secret can also be fetched
255
+
256
+ The alternate secret is exclusively used to authenticate incoming requests. This allows for secret
257
+ rotation - Point secret_key to a new secret and alternate_secret_key to the old secret. Both will then
258
+ be accepted and you can migrate your applications to only send the new secret. Once they are all updated,
259
+ remove the alternate_secret_key:
260
+
261
+ ```python
262
+ from types import SimpleNamespace
263
+ import clearskies
264
+
265
+ def fetch_secret(path):
266
+ if path == "/path/to/my/secret":
267
+ return "SUPERSECRET"
268
+ if path == "/path/to/alternate/secret":
269
+ return "ALSOOKAY"
270
+ raise KeyError(f"Attempt to fetch non-existent secret: {path}")
271
+
272
+ fake_secret_manager = SimpleNamespace(get=fetch_secret)
273
+
274
+ wsgi = clearskies.contexts.WsgiRef(
275
+ clearskies.endpoints.Callable(
276
+ lambda: {"hello": "world"},
277
+ authentication=clearskies.authentication.SecretBearer(
278
+ secret_key="/path/to/my/secret",
279
+ alternate_secret_key="/path/to/alternate/secret",
280
+ ),
281
+ ),
282
+ bindings={
283
+ "secrets": fake_secret_manager,
284
+ },
285
+ )
286
+ wsgi()
287
+ ```
288
+
289
+ And when invoked:
290
+
291
+ ```bash
292
+ $ curl 'http://localhost:8080/' -H "Authorization: SUPERSECRET" | jq
293
+ {
294
+ "status": "success",
295
+ "error": "",
296
+ "data": {
297
+ "hello": "world"
298
+ },
299
+ "pagination": {},
300
+ "input_errors": {}
301
+ }
302
+
303
+ $ curl 'http://localhost:8080/' -H "Authorization: ALSOOKAY" | jq
304
+ {
305
+ "status": "success",
306
+ "error": "",
307
+ "data": {
308
+ "hello": "world"
309
+ },
310
+ "pagination": {},
311
+ "input_errors": {}
312
+ }
313
+
314
+ $ curl 'http://localhost:8080/' -H "Authorization: NOTTHESECRET" | jq
315
+ {
316
+ "status": "client_error",
317
+ "error": "Not Authenticated",
318
+ "data": [],
319
+ "pagination": {},
320
+ "input_errors": {}
321
+ }
322
+ ```
323
+
324
+ """
325
+ alternate_secret_key = clearskies.configs.String(default="")
326
+
327
+ """
328
+ The name of the environment variable from which we should fetch our key.
329
+ """
330
+ environment_key = clearskies.configs.String(default="")
331
+
332
+ """
333
+ The name of an alternate environment variable from which we should fetch our key.
334
+
335
+ This allows for secret rotation by allowing the API to accept a secret from two different
336
+ environment variables: an old value and a new value. You can then migrate your client applications
337
+ to use the new key and, once they are all migrated, remove the old key from the application
338
+ configuration. Here's an example:
339
+
340
+ ```python
341
+ import os
342
+ import clearskies
343
+
344
+ os.environ["MY_AUTH_SECRET"] = "SUPERSECRET"
345
+ os.environ["MY_ALT_SECRET"] = "ALSOOKAY"
346
+
347
+ wsgi = clearskies.contexts.WsgiRef(
348
+ clearskies.endpoints.Callable(
349
+ lambda: {"hello": "world"},
350
+ authentication=clearskies.authentication.SecretBearer(
351
+ environment_key="MY_AUTH_SECRET",
352
+ alternate_environment_key="MY_ALT_SECRET",
353
+ ),
354
+ ),
355
+ )
356
+ wsgi()
357
+ ```
358
+
359
+ And when invoked:
360
+
361
+ ```bash
362
+ $ curl 'http://localhost:8080/' -H "Authorization: SUPERSECRET" | jq
363
+ {
364
+ "status": "success",
365
+ "error": "",
366
+ "data": {
367
+ "hello": "world"
368
+ },
369
+ "pagination": {},
370
+ "input_errors": {}
371
+ }
372
+
373
+ $ curl 'http://localhost:8080/' -H "Authorization: ALSOOKAY" | jq
374
+ {
375
+ "status": "success",
376
+ "error": "",
377
+ "data": {
378
+ "hello": "world"
379
+ },
380
+ "pagination": {},
381
+ "input_errors": {}
382
+ }
383
+
384
+ $ curl 'http://localhost:8080/' -H "Authorization: NOTTHESECRET" | jq
385
+ {
386
+ "status": "client_error",
387
+ "error": "Not Authenticated",
388
+ "data": [],
389
+ "pagination": {},
390
+ "input_errors": {}
391
+ }
392
+ ```
393
+
394
+ """
395
+ alternate_environment_key = clearskies.configs.String(default="")
396
+
397
+ """
398
+ The expected prefix (if any) that should come before the secret key in the authorization header.
399
+
400
+ This applies to both the incoming authentication process and outgoing authentication headers. Some systems
401
+ require a prefix before the auth token in the HTTP header (e.g. `Authorization: TOKEN [auth key here]`).
402
+ You can provide that prefix to `header_prefix` in order for the endpoint to require a prefix or the api backend
403
+ to provide such a prefix. Note that the prefix is case-insensitive and it does not assume a space between the
404
+ prefix and the token (so, if you want a space, you must explicitly put it in the prefix). Here's an example:
405
+
406
+ ```python
407
+ import os
408
+ import clearskies
409
+
410
+ os.environ["MY_AUTH_SECRET"] = "SUPERSECRET"
411
+
412
+ wsgi = clearskies.contexts.WsgiRef(
413
+ clearskies.endpoints.Callable(
414
+ lambda: {"hello": "world"},
415
+ authentication=clearskies.authentication.SecretBearer(
416
+ environment_key="MY_AUTH_SECRET",
417
+ header_prefix="secret-token ",
418
+ ),
419
+ ),
420
+ )
421
+ wsgi()
422
+ ```
423
+
424
+ And then usage:
425
+
426
+ ```bash
427
+ $ curl 'http://localhost:8080/' -H "Authorization: SECRET-TOKEN SUPERSECRET" | jq
428
+ {
429
+ "status": "success",
430
+ "error": "",
431
+ "data": {
432
+ "hello": "world"
433
+ },
434
+ "pagination": {},
435
+ "input_errors": {}
436
+ }
437
+
438
+ $ curl 'http://localhost:8080/' -H "Authorization: SUPERSECRET" | jq
439
+ {
440
+ "status": "client_error",
441
+ "error": "Not Authenticated",
442
+ "data": [],
443
+ "pagination": {},
444
+ "input_errors": {}
445
+ }
446
+ ```
447
+ """
448
+ header_prefix = clearskies.configs.String(default="")
449
+
450
+ """
451
+ The length of our header prefix
452
+ """
453
+ header_prefix_length = None
454
+
455
+ """
456
+ The name of our security scheme in the auto-generated documentation
457
+ """
458
+ documentation_security_name = clearskies.configs.String(default="ApiKey")
459
+
460
+ _secret: str = None # type: ignore
461
+ _alternate_secret: str = None # type: ignore
462
+
463
+ @clearskies.parameters_to_properties.parameters_to_properties
464
+ def __init__(
465
+ self,
466
+ secret_key: str = "",
467
+ alternate_secret_key: str = "",
468
+ environment_key: str = "",
469
+ alternate_environment_key: str = "",
470
+ header_prefix: str = "",
471
+ documentation_security_name: str = "",
26
472
  ):
27
- if secret_key:
28
- self._secret = self._secrets.get(secret_key)
29
- elif environment_key:
30
- self._secret = self._environment.get(environment_key)
31
- elif secret:
32
- self._secret = secret
33
- else:
34
- raise ValueError(
35
- "Must set either 'secret_key', 'environment_key', or 'secret', when configuring the SecretBearer"
473
+ if not secret_key and not environment_key:
474
+ raise ValueError("Must set either 'secret_key' or 'environment_key' when configuring the SecretBearer")
475
+ self.header_prefix_length = len(header_prefix)
476
+ self.finalize_and_validate_configuration()
477
+
478
+ @property
479
+ def secret(self):
480
+ if not self._secret:
481
+ self._secret = (
482
+ self.secrets.get(self.secret_key) if self.secret_key else self.environment.get(self.environment_key)
36
483
  )
37
- self._header_prefix = header_prefix if header_prefix else "authorization "
38
- self._header_prefix_length = len(self._header_prefix)
39
- self._documentation_security_name = documentation_security_name
484
+ return self._secret
485
+
486
+ def clear_credential_cache(self):
487
+ if self.secret_key:
488
+ self._secret = None # type: ignore
489
+
490
+ @property
491
+ def alternate_secret(self):
492
+ if not self.alternate_secret_key and not self.alternate_environment_key:
493
+ return ""
494
+
495
+ if not self._alternate_secret:
496
+ self._alternate_secret = (
497
+ self.secrets.get(self.alternate_secret_key)
498
+ if self.secret_key
499
+ else self.environment.get(self.alternate_environment_key)
500
+ )
501
+ return self._alternate_secret
40
502
 
41
503
  def headers(self, retry_auth=False):
42
504
  self._configured_guard()
43
- return {"Authorization": f"{self._header_prefix}{self._secret}"}
505
+ if retry_auth:
506
+ self.clear_credential_cache()
507
+ return {"Authorization": f"{self.header_prefix}{self.secret}"}
44
508
 
45
509
  def authenticate(self, input_output):
46
510
  self._configured_guard()
47
- auth_header = input_output.get_request_header("authorization", True)
48
- if auth_header[: self._header_prefix_length].lower() != self._header_prefix.lower():
49
- self._logging.debug(
50
- "Authentication failure due to prefix mismatch. Configured prefix: "
51
- + self._header_prefix.lower()
52
- + ". Found prefix: "
53
- + auth_header[: self._header_prefix_length].lower()
54
- )
511
+ auth_header = input_output.request_headers.authorization
512
+ if not auth_header:
513
+ return False
514
+ if auth_header[: self.header_prefix_length].lower() != self.header_prefix.lower():
515
+ # self._logging.debug(
516
+ # "Authentication failure due to prefix mismatch. Configured prefix: "
517
+ # + self._header_prefix.lower()
518
+ # + ". Found prefix: "
519
+ # + auth_header[: self._header_prefix_length].lower()
520
+ # )
55
521
  return False
56
- if self._secret == auth_header[self._header_prefix_length :]:
57
- self._logging.debug("Authentication success")
522
+ provided_secret = auth_header[self.header_prefix_length :]
523
+ if self.secret == provided_secret:
524
+ # self._logging.debug("Authentication success")
58
525
  return True
59
- self._logging.debug("Authentication failure due to secret mismatch")
526
+ if self.alternate_secret and provided_secret == self._alternate_secret:
527
+ # self._logging.debug("Authentication success with alternate secret")
528
+ return True
529
+ # self._logging.debug("Authentication failure due to secret mismatch")
60
530
  return False
61
531
 
62
532
  def authorize(self, authorization):
@@ -66,7 +536,7 @@ class SecretBearer:
66
536
  cors.add_header("Authorization")
67
537
 
68
538
  def _configured_guard(self):
69
- if not self._secret:
539
+ if not self.secret:
70
540
  raise ValueError("Attempted to use SecretBearer authentication class without providing the configuration")
71
541
 
72
542
  def documentation_request_parameters(self):
@@ -80,12 +550,4 @@ class SecretBearer:
80
550
  }
81
551
 
82
552
  def documentation_security_scheme_name(self):
83
- return self._documentation_security_name if self._documentation_security_name is not None else "ApiKey"
84
-
85
-
86
- class SecretBearerAuth(AuthBase, SecretBearer):
87
- """Wrapper around SecretBearer to allow for the use of the SecretBearer class as an AuthBase class"""
88
-
89
- def __call__(self, r: PreparedRequest) -> PreparedRequest:
90
- r.headers = {**r.headers, **self.headers()}
91
- return r
553
+ return self.documentation_security_name
@@ -1,5 +1,5 @@
1
- from .oai3_schema_resolver import OAI3SchemaResolver
2
1
  from .oai3_json import OAI3JSON
2
+ from .oai3_schema_resolver import OAI3SchemaResolver
3
3
 
4
4
  __all__ = [
5
5
  "OAI3SchemaResolver",
@@ -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
7
  class OAI3JSON:
6
- requests = None
7
- formatted = None
8
- models = None
9
- security_schemes = None
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:
@@ -68,7 +70,7 @@ class OAI3JSON:
68
70
  raise ValueError(f"Two routes had the same path and method: {absolute_path} - {request_method}")
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 Array, 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