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
@@ -0,0 +1,382 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+ from typing import Callable as CallableType
5
+
6
+ from clearskies import authentication, autodoc, configs, decorators, exceptions
7
+ from clearskies.endpoint import Endpoint
8
+ from clearskies.functional import string
9
+
10
+ if TYPE_CHECKING:
11
+ from clearskies import Column, Model, Schema, SecurityHeader
12
+ from clearskies.authentication import Authentication, Authorization
13
+ from clearskies.input_outputs import InputOutput
14
+
15
+
16
+ class Callable(Endpoint):
17
+ """
18
+ An endpoint that executes a user-defined function.
19
+
20
+ The Callable endpoint does exactly that - you provide a function that will be called when the endpoint is invoked. Like
21
+ all callables invoked by clearskies, you can request any defined dependency that can be provided by the clearskies
22
+ framework.
23
+
24
+ Whatever you return will be returned to the client. By default, the return value is sent along in the `data` parameter
25
+ of the standard clearskies response. To suppress this behavior, set `return_standard_response` to `False`. You can also
26
+ return a model instance, a model query, or a list of model instances and the callable endpoint will automatically return
27
+ the columns specified in `readable_column_names` to the client.
28
+
29
+ Here's a basic working example:
30
+
31
+ ```python
32
+ import clearskies
33
+
34
+
35
+ class User(clearskies.Model):
36
+ id_column_name = "id"
37
+ backend = clearskies.backends.MemoryBackend()
38
+ id = clearskies.columns.Uuid()
39
+ first_name = clearskies.columns.String()
40
+ last_name = clearskies.columns.String()
41
+ age = clearskies.columns.Integer()
42
+
43
+
44
+ def my_users_callable(users: User):
45
+ bob = users.create({"first_name": "Bob", "last_name": "Brown", "age": 10})
46
+ jane = users.create({"first_name": "Jane", "last_name": "Brown", "age": 10})
47
+ alice = users.create({"first_name": "Alice", "last_name": "Green", "age": 10})
48
+
49
+ return jane
50
+
51
+
52
+ my_users = clearskies.endpoints.Callable(
53
+ my_users_callable,
54
+ model_class=User,
55
+ readable_column_names=["id", "first_name", "last_name"],
56
+ )
57
+
58
+ wsgi = clearskies.contexts.WsgiRef(
59
+ my_users,
60
+ classes=[User],
61
+ )
62
+ wsgi()
63
+ ```
64
+
65
+ If you run the above script and invoke the server:
66
+
67
+ ```bash
68
+ $ curl 'http://localhost:8080' | jq
69
+ {
70
+ "status": "success",
71
+ "error": "",
72
+ "data": {
73
+ "id": "4a35a616-3d57-456f-8306-7c610a5e80e1",
74
+ "first_name": "Jane",
75
+ "last_name": "Brown"
76
+ },
77
+ "pagination": {},
78
+ "input_errors": {}
79
+ }
80
+ ```
81
+
82
+ The above example demonstrates returning a model and using readable_column_names to decide what is actually sent to the client
83
+ (note that age is left out of the response). The advantage of doing it this way is that clearskies can also auto-generate
84
+ OpenAPI documentation using this strategy. Of course, you can also just return any arbitrary data you want. If you do return
85
+ custom data, and also want your API to be documented, you can pass a schema along to output_schema so clearskies can document
86
+ it:
87
+
88
+ ```python
89
+ import clearskies
90
+
91
+
92
+ class DogResponse(clearskies.Schema):
93
+ species = (clearskies.columns.String(),)
94
+ nickname = (clearskies.columns.String(),)
95
+ level = (clearskies.columns.Integer(),)
96
+
97
+
98
+ clearskies.contexts.WsgiRef(
99
+ clearskies.endpoints.Callable(
100
+ lambda: {"species": "dog", "nickname": "Spot", "level": 100},
101
+ output_schema=DogResponse,
102
+ )
103
+ )()
104
+ ```
105
+
106
+ """
107
+
108
+ """
109
+ The callable to execute when the endpoint is invoked
110
+ """
111
+ to_call = configs.Callable(default=None)
112
+
113
+ """
114
+ A schema that describes the expected input from the client.
115
+
116
+ Note that if this is specified it will take precedence over writeable_column_names and model_class, which
117
+ can also be used to specify the expected input.
118
+
119
+ ```python
120
+ import clearskies
121
+
122
+ class ExpectedInput(clearskies.Schema):
123
+ first_name = clearskies.columns.String(validators=[clearskies.validators.Required()])
124
+ last_name = clearskies.columns.String()
125
+ age = clearskies.columns.Integer(validators=[clearskies.validators.MinimumValue(0)])
126
+
127
+ reflect = clearskies.endpoints.Callable(
128
+ lambda request_data: request_data,
129
+ request_methods=["POST"],
130
+ input_schema=ExpectedInput,
131
+ )
132
+
133
+ wsgi = clearskies.contexts.WsgiRef(reflect)
134
+ wsgi()
135
+ ```
136
+
137
+ And then valid and invalid requests:
138
+
139
+ ```bash
140
+ $ curl http://localhost:8080 -d '{"first_name":"Jane","last_name":"Doe","age":1}' | jq
141
+ {
142
+ "status": "success",
143
+ "error": "",
144
+ "data": {
145
+ "first_name": "Jane",
146
+ "last_name": "Doe",
147
+ "age": 1
148
+ },
149
+ "pagination": {},
150
+ "input_errors": {}
151
+ }
152
+
153
+ $ curl http://localhost:8080 -d '{"last_name":10,"age":-1,"check":"cool"}' | jq
154
+ {
155
+ "status": "input_errors",
156
+ "error": "",
157
+ "data": [],
158
+ "pagination": {},
159
+ "input_errors": {
160
+ "age": "'age' must be at least 0.",
161
+ "first_name": "'first_name' is required.",
162
+ "last_name": "value should be a string",
163
+ "check": "Input column check is not an allowed input column."
164
+ }
165
+ }
166
+ ```
167
+
168
+ """
169
+ input_schema = configs.Schema(default=None)
170
+
171
+ """
172
+ Whether or not the return value is meant to be wrapped up in the standard clearskies response schema.
173
+
174
+ With the standard response schema, the return value of the function will be placed in the `data` portion of
175
+ the standard clearskies response:
176
+
177
+ ```python
178
+ import clearskies
179
+
180
+ wsgi = clearskies.contexts.WsgiRef(
181
+ clearskies.endpoints.Callable(
182
+ lambda: {"hello": "world"},
183
+ return_standard_response=True, # the default value
184
+ )
185
+ )
186
+ wsgi()
187
+ ```
188
+
189
+ Results in:
190
+
191
+ ```bash
192
+ $ curl http://localhost:8080 | jq
193
+ {
194
+ "status": "success",
195
+ "error": "",
196
+ "data": {
197
+ "hello": "world"
198
+ },
199
+ "pagination": {},
200
+ "input_errors": {}
201
+ }
202
+ ```
203
+ But if you want to build your own response:
204
+
205
+ ```python
206
+ import clearskies
207
+
208
+ wsgi = clearskies.contexts.WsgiRef(
209
+ clearskies.endpoints.Callable(
210
+ lambda: {"hello": "world"},
211
+ return_standard_response=False,
212
+ )
213
+ )
214
+ wsgi()
215
+ ```
216
+
217
+ Results in:
218
+
219
+ ```bash
220
+ $ curl http://localhost:8080 | jq
221
+ {
222
+ "hello": "world"
223
+ }
224
+ ```
225
+
226
+ Note that you can also return strings this way instead of objects/JSON.
227
+
228
+ """
229
+ return_standard_response = configs.Boolean(default=True)
230
+
231
+ """
232
+ Set to true if the callable will be returning multiple records (used when building the auto-documentation)
233
+ """
234
+ return_records = configs.Boolean(default=False)
235
+
236
+ @decorators.parameters_to_properties
237
+ def __init__(
238
+ self,
239
+ to_call: CallableType,
240
+ url: str = "",
241
+ request_methods: list[str] = ["GET"],
242
+ model_class: type[Model] | None = None,
243
+ readable_column_names: list[str] = [],
244
+ writeable_column_names: list[str] = [],
245
+ input_schema: type[Schema] | None = None,
246
+ output_schema: type[Schema] | None = None,
247
+ input_validation_callable: CallableType | None = None,
248
+ return_standard_response: bool = True,
249
+ return_records: bool = False,
250
+ response_headers: list[str | CallableType[..., list[str]]] = [],
251
+ output_map: CallableType[..., dict[str, Any]] | None = None,
252
+ column_overrides: dict[str, Column] = {},
253
+ internal_casing: str = "snake_case",
254
+ external_casing: str = "snake_case",
255
+ security_headers: list[SecurityHeader] = [],
256
+ description: str = "",
257
+ authentication: Authentication = authentication.Public(),
258
+ authorization: Authorization = authentication.Authorization(),
259
+ ):
260
+ # we need to call the parent but don't have to pass along any of our kwargs. They are all optional in our parent, and our parent class
261
+ # just stores them in parameters, which we have already done. However, the parent does do some extra initialization stuff that we need,
262
+ # which is why we have to call the parent.
263
+ super().__init__()
264
+
265
+ if self.input_schema and not self.writeable_column_names:
266
+ self.writeable_column_names = list(self.input_schema.get_columns().keys())
267
+
268
+ def handle(self, input_output: InputOutput):
269
+ if self.writeable_column_names or self.input_schema:
270
+ self.validate_input_against_schema(
271
+ self.get_request_data(input_output),
272
+ input_output,
273
+ self.input_schema if self.input_schema else self.model_class,
274
+ )
275
+ else:
276
+ input_errors = self.find_input_errors_from_callable(input_output.request_data, input_output)
277
+ if input_errors:
278
+ raise exceptions.InputErrors(input_errors)
279
+ response = self.di.call_function(self.to_call, **input_output.get_context_for_callables())
280
+
281
+ if not self.return_standard_response:
282
+ return input_output.respond(response, 200)
283
+
284
+ # did the developer return a model?
285
+ if self.model_class and isinstance(response, self.model_class):
286
+ # and is it a query or a single model?
287
+ if response._data:
288
+ return self.success(input_output, self.model_as_json(response, input_output))
289
+ else:
290
+ # with a query we can also get pagination data, maybe?
291
+ converted_models = [self.model_as_json(model, input_output) for model in response]
292
+ return self.success(
293
+ input_output,
294
+ converted_models,
295
+ number_results=len(response) if response.backend.can_count else None,
296
+ next_page=response.next_page_data(),
297
+ limit=response.get_query().limit,
298
+ )
299
+
300
+ # or did they return a list of models?
301
+ if isinstance(response, list) and all(isinstance(item, self.model_class) for item in response):
302
+ return self.success(input_output, [self.model_as_json(model, input_output) for model in response])
303
+
304
+ # if none of the above, just return the data
305
+ return self.success(input_output, response)
306
+
307
+ def documentation(self) -> list[autodoc.request.Request]:
308
+ output_schema = self.output_schema if self.output_schema else self.model_class
309
+ nice_model = string.camel_case_to_words(output_schema.__name__)
310
+
311
+ schema_model_name = string.camel_case_to_snake_case(output_schema.__name__)
312
+ output_data_schema = (
313
+ self.documentation_data_schema(output_schema, self.readable_column_names)
314
+ if self.readable_column_names
315
+ else []
316
+ )
317
+ output_autodoc = (
318
+ autodoc.schema.Object(
319
+ self.auto_case_internal_column_name("data"),
320
+ children=output_data_schema,
321
+ model_name=schema_model_name if self.readable_column_names else "",
322
+ ),
323
+ )
324
+ if self.return_records:
325
+ output_autodoc.name = nice_model # type: ignore
326
+ output_autodoc = autodoc.schema.Array(
327
+ self.auto_case_internal_column_name("data"),
328
+ output_autodoc,
329
+ ) # type: ignore
330
+
331
+ authentication = self.authentication
332
+ standard_error_responses = []
333
+ if not getattr(authentication, "is_public", False):
334
+ standard_error_responses.append(self.documentation_access_denied_response())
335
+ if getattr(authentication, "can_authorize", False):
336
+ standard_error_responses.append(self.documentation_unauthorized_response())
337
+ if self.writeable_column_names:
338
+ standard_error_responses.append(self.documentation_input_error_response())
339
+
340
+ return [
341
+ autodoc.request.Request(
342
+ self.description,
343
+ [
344
+ self.documentation_success_response(
345
+ output_autodoc, # type: ignore
346
+ description=self.description,
347
+ include_pagination=self.return_records,
348
+ ),
349
+ *standard_error_responses,
350
+ self.documentation_generic_error_response(),
351
+ ],
352
+ relative_path=self.url,
353
+ request_methods=self.request_methods,
354
+ parameters=[
355
+ *self.documentation_request_parameters(),
356
+ *self.documentation_url_parameters(),
357
+ ],
358
+ root_properties={
359
+ "security": self.documentation_request_security(),
360
+ },
361
+ ),
362
+ ]
363
+
364
+ def documentation_request_parameters(self) -> list[autodoc.request.Parameter]:
365
+ if not self.writeable_column_names:
366
+ return []
367
+
368
+ return self.standard_json_request_parameters(self.input_schema if self.input_schema else self.model_class)
369
+
370
+ def documentation_models(self) -> dict[str, autodoc.schema.Schema]:
371
+ if not self.readable_column_names:
372
+ return {}
373
+
374
+ output_schema = self.output_schema if self.output_schema else self.model_class
375
+ schema_model_name = string.camel_case_to_snake_case(output_schema.__name__)
376
+
377
+ return {
378
+ schema_model_name: autodoc.schema.Object(
379
+ self.auto_case_internal_column_name("data"),
380
+ children=self.documentation_data_schema(output_schema, self.readable_column_names),
381
+ ),
382
+ }
@@ -0,0 +1,201 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, Callable
4
+
5
+ from clearskies import authentication, autodoc, decorators, exceptions
6
+ from clearskies.endpoint import Endpoint
7
+ from clearskies.functional import string
8
+
9
+ if TYPE_CHECKING:
10
+ from clearskies import Column, Schema, SecurityHeader
11
+ from clearskies.input_outputs import InputOutput
12
+ from clearskies.model import Model
13
+
14
+
15
+ class Create(Endpoint):
16
+ """
17
+ An endpoint to create a record.
18
+
19
+ This endpoint accepts user input and uses it to create a record for the given model class. You have
20
+ to provide the model class, which columns the end-user can set, and which columns get returned
21
+ to the client. The column definitions in the model class are used to strictly validate the user
22
+ input. Here's a basic example of a model class with the create endpoint in use:
23
+
24
+ ```python
25
+ import clearskies
26
+ from clearskies import validators, columns
27
+
28
+
29
+ class MyAwesomeModel(clearskies.Model):
30
+ id_column_name = "id"
31
+ backend = clearskies.backends.MemoryBackend()
32
+
33
+ id = columns.Uuid()
34
+ name = clearskies.columns.String(
35
+ validators=[
36
+ validators.Required(),
37
+ validators.MaximumLength(50),
38
+ ]
39
+ )
40
+ email = columns.Email(validators=[validators.Unique()])
41
+ some_number = columns.Integer()
42
+ expires_at = columns.Date()
43
+ created_at = columns.Created()
44
+
45
+
46
+ wsgi = clearskies.contexts.WsgiRef(
47
+ clearskies.endpoints.Create(
48
+ MyAwesomeModel,
49
+ readable_column_names=["id", "name", "email", "some_number", "expires_at", "created_at"],
50
+ writeable_column_names=["name", "email", "some_number", "expires_at"],
51
+ ),
52
+ )
53
+ wsgi()
54
+ ```
55
+
56
+ The following shows how to invoke it, and demonstrates the strict input validation that happens as part of the
57
+ process:
58
+
59
+ ```bash
60
+ $ curl 'http://localhost:8080/' -d '{"name":"Example", "email":"test@example.com","some_number":5,"expires_at":"2024-12-31"}' | jq
61
+ {
62
+ "status": "success",
63
+ "error": "",
64
+ "data": {
65
+ "id": "74eda1c6-fe66-44ec-9246-758d16e1a304",
66
+ "name": "Example",
67
+ "email": "test@example.com",
68
+ "some_number": 5,
69
+ "expires_at": "2024-12-31",
70
+ "created_at": "2025-05-23T16:36:30+00:00"
71
+ },
72
+ "pagination": {},
73
+ "input_errors": {}
74
+ }
75
+
76
+ $ curl 'http://localhost:8080/' -d '{"name":"", "email":"test@example.com","some_number":"asdf","expires_at":"not-a-date", "not_a_column": "sup"}' | jq
77
+ {
78
+ "status": "input_errors",
79
+ "error": "",
80
+ "data": [],
81
+ "pagination": {},
82
+ "input_errors": {
83
+ "name": "'name' is required.",
84
+ "email": "Invalid value for 'email': the given value already exists, and must be unique.",
85
+ "some_number": "value should be an integer",
86
+ "expires_at": "given value did not appear to be a valid date",
87
+ "not_a_column": "Input column not_a_column is not an allowed input column."
88
+ }
89
+ }
90
+ ```
91
+
92
+ The first call successfully creates a new record. The second call fails with a variety of error messages:
93
+
94
+ 1. A name wasn't provided by the model class marked this as required
95
+ 2. We provided the same email address again, but this column is marked as unique
96
+ 3. The number provided in `some_number` wasn't actually a number
97
+ 4. The provided value for `expires_at` wasn't actually a date.
98
+ 5. We provided an extra column (`not_a_column`) that wasn't in the list of allowed columns.
99
+ """
100
+
101
+ @decorators.parameters_to_properties
102
+ def __init__(
103
+ self,
104
+ model_class: type[Model],
105
+ writeable_column_names: list[str],
106
+ readable_column_names: list[str],
107
+ input_validation_callable: Callable | None = None,
108
+ include_routing_data_in_request_data: bool = False,
109
+ url: str = "",
110
+ request_methods: list[str] = ["POST"],
111
+ response_headers: list[str | Callable[..., list[str]]] = [],
112
+ output_map: Callable[..., dict[str, Any]] | None = None,
113
+ output_schema: Schema | None = None,
114
+ column_overrides: dict[str, Column] = {},
115
+ internal_casing: str = "snake_case",
116
+ external_casing: str = "snake_case",
117
+ security_headers: list[SecurityHeader] = [],
118
+ description: str = "",
119
+ authentication: authentication.Authentication = authentication.Public(),
120
+ authorization: authentication.Authorization = authentication.Authorization(),
121
+ ):
122
+ # a bit weird, but we have to do this because the default in the above definition is different than
123
+ # the default set on the request_mehtods config in the bsae endpoint class. parameters_to_properties will copy
124
+ # parameters to properties, but only for things set by the developer - not for default values set in the kwarg
125
+ # definitions. Therefore, we always set it here to make sure we user our default, not the one in the base class.
126
+ self.request_methods = request_methods
127
+
128
+ # we need to call the parent but don't have to pass along any of our kwargs. They are all optional in our parent, and our parent class
129
+ # just stores them in parameters, which we have already done. However, the parent does do some extra initialization stuff that we need,
130
+ # which is why we have to call the parent.
131
+ super().__init__()
132
+
133
+ def handle(self, input_output: InputOutput) -> Any:
134
+ request_data = self.get_request_data(input_output)
135
+ if not request_data and input_output.has_body():
136
+ raise exceptions.ClientError("Request body was not valid JSON")
137
+ self.validate_input_against_schema(request_data, input_output, self.model_class)
138
+ new_model = self.model.create(request_data, columns=self.columns)
139
+ return self.success(input_output, self.model_as_json(new_model, input_output))
140
+
141
+ def documentation(self) -> list[autodoc.request.Request]:
142
+ output_schema = self.model_class
143
+ nice_model = string.camel_case_to_words(output_schema.__name__)
144
+
145
+ schema_model_name = string.camel_case_to_snake_case(output_schema.__name__)
146
+ output_data_schema = self.documentation_data_schema(output_schema, self.readable_column_names)
147
+ output_autodoc = (
148
+ autodoc.schema.Object(
149
+ self.auto_case_internal_column_name("data"), children=output_data_schema, model_name=schema_model_name
150
+ ),
151
+ )
152
+
153
+ authentication = self.authentication
154
+ # Many swagger UIs will only allow one response per status code, and we use the same status code (200)
155
+ # for both a success response and an input error response. This could be fixed by changing the status
156
+ # code for input error responses, but there's not actually a great HTTP status code for that, so :shrug:
157
+ # standard_error_responses = [self.documentation_input_error_response()]
158
+ standard_error_responses = []
159
+ if not getattr(authentication, "is_public", False):
160
+ standard_error_responses.append(self.documentation_access_denied_response())
161
+ if getattr(authentication, "can_authorize", False):
162
+ standard_error_responses.append(self.documentation_unauthorized_response())
163
+
164
+ return [
165
+ autodoc.request.Request(
166
+ self.description,
167
+ [
168
+ self.documentation_success_response(
169
+ output_autodoc, # type: ignore
170
+ description=self.description,
171
+ ),
172
+ *standard_error_responses,
173
+ self.documentation_generic_error_response(),
174
+ ],
175
+ relative_path=self.url,
176
+ request_methods=self.request_methods,
177
+ parameters=[
178
+ *self.documentation_request_parameters(),
179
+ *self.documentation_url_parameters(),
180
+ ],
181
+ root_properties={
182
+ "security": self.documentation_request_security(),
183
+ },
184
+ ),
185
+ ]
186
+
187
+ def documentation_request_parameters(self) -> list[autodoc.request.Parameter]:
188
+ return [
189
+ *self.standard_json_request_parameters(self.model_class),
190
+ ]
191
+
192
+ def documentation_models(self) -> dict[str, autodoc.schema.Schema]:
193
+ output_schema = self.output_schema if self.output_schema else self.model_class
194
+ schema_model_name = string.camel_case_to_snake_case(output_schema.__name__)
195
+
196
+ return {
197
+ schema_model_name: autodoc.schema.Object(
198
+ self.auto_case_internal_column_name("data"),
199
+ children=self.documentation_data_schema(output_schema, self.readable_column_names),
200
+ ),
201
+ }