clear-skies 1.22.31__py3-none-any.whl → 2.0.1__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 (345) hide show
  1. {clear_skies-1.22.31.dist-info → clear_skies-2.0.1.dist-info}/METADATA +12 -14
  2. clear_skies-2.0.1.dist-info/RECORD +249 -0
  3. {clear_skies-1.22.31.dist-info → clear_skies-2.0.1.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 +46 -0
  8. clearskies/authentication/authorization.py +8 -9
  9. clearskies/authentication/authorization_pass_through.py +11 -9
  10. clearskies/authentication/jwks.py +133 -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 +53 -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 +1229 -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 +162 -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 +24 -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/endpoint_list.py +28 -0
  100. clearskies/configs/float.py +16 -0
  101. clearskies/configs/float_or_callable.py +18 -0
  102. clearskies/configs/integer.py +16 -0
  103. clearskies/configs/integer_or_callable.py +18 -0
  104. clearskies/configs/joins.py +30 -0
  105. clearskies/configs/list_any_dict.py +30 -0
  106. clearskies/configs/list_any_dict_or_callable.py +31 -0
  107. clearskies/configs/model_class.py +35 -0
  108. clearskies/configs/model_column.py +65 -0
  109. clearskies/configs/model_columns.py +56 -0
  110. clearskies/configs/model_destination_name.py +25 -0
  111. clearskies/configs/model_to_id_column.py +43 -0
  112. clearskies/configs/readable_model_column.py +9 -0
  113. clearskies/configs/readable_model_columns.py +9 -0
  114. clearskies/configs/schema.py +23 -0
  115. clearskies/configs/searchable_model_columns.py +9 -0
  116. clearskies/configs/security_headers.py +39 -0
  117. clearskies/configs/select.py +26 -0
  118. clearskies/configs/select_list.py +47 -0
  119. clearskies/configs/string.py +29 -0
  120. clearskies/configs/string_dict.py +32 -0
  121. clearskies/configs/string_list.py +32 -0
  122. clearskies/configs/string_list_or_callable.py +35 -0
  123. clearskies/configs/string_or_callable.py +18 -0
  124. clearskies/configs/timedelta.py +18 -0
  125. clearskies/configs/timezone.py +18 -0
  126. clearskies/configs/url.py +23 -0
  127. clearskies/configs/validators.py +45 -0
  128. clearskies/configs/writeable_model_column.py +9 -0
  129. clearskies/configs/writeable_model_columns.py +9 -0
  130. clearskies/configurable.py +76 -0
  131. clearskies/contexts/__init__.py +8 -8
  132. clearskies/contexts/cli.py +8 -41
  133. clearskies/contexts/context.py +91 -56
  134. clearskies/contexts/wsgi.py +16 -29
  135. clearskies/contexts/wsgi_ref.py +53 -0
  136. clearskies/di/__init__.py +10 -7
  137. clearskies/di/additional_config.py +115 -4
  138. clearskies/di/additional_config_auto_import.py +12 -0
  139. clearskies/di/di.py +742 -121
  140. clearskies/di/inject/__init__.py +23 -0
  141. clearskies/di/inject/by_class.py +21 -0
  142. clearskies/di/inject/by_name.py +18 -0
  143. clearskies/di/inject/di.py +13 -0
  144. clearskies/di/inject/environment.py +14 -0
  145. clearskies/di/inject/input_output.py +20 -0
  146. clearskies/di/inject/now.py +13 -0
  147. clearskies/di/inject/requests.py +13 -0
  148. clearskies/di/inject/secrets.py +14 -0
  149. clearskies/di/inject/utcnow.py +13 -0
  150. clearskies/di/inject/uuid.py +15 -0
  151. clearskies/di/injectable.py +29 -0
  152. clearskies/di/injectable_properties.py +131 -0
  153. clearskies/end.py +183 -0
  154. clearskies/endpoint.py +1310 -0
  155. clearskies/endpoint_group.py +310 -0
  156. clearskies/endpoints/__init__.py +23 -0
  157. clearskies/endpoints/advanced_search.py +526 -0
  158. clearskies/endpoints/callable.py +388 -0
  159. clearskies/endpoints/create.py +202 -0
  160. clearskies/endpoints/delete.py +139 -0
  161. clearskies/endpoints/get.py +275 -0
  162. clearskies/endpoints/health_check.py +181 -0
  163. clearskies/endpoints/list.py +573 -0
  164. clearskies/endpoints/restful_api.py +427 -0
  165. clearskies/endpoints/simple_search.py +286 -0
  166. clearskies/endpoints/update.py +190 -0
  167. clearskies/environment.py +5 -3
  168. clearskies/exceptions/__init__.py +17 -0
  169. clearskies/{handlers/exceptions/input_error.py → exceptions/input_errors.py} +1 -1
  170. clearskies/exceptions/moved_permanently.py +3 -0
  171. clearskies/exceptions/moved_temporarily.py +3 -0
  172. clearskies/exceptions/not_found.py +2 -0
  173. clearskies/functional/__init__.py +2 -2
  174. clearskies/functional/routing.py +92 -0
  175. clearskies/functional/string.py +19 -11
  176. clearskies/functional/validations.py +61 -9
  177. clearskies/input_outputs/__init__.py +9 -7
  178. clearskies/input_outputs/cli.py +130 -142
  179. clearskies/input_outputs/exceptions/__init__.py +1 -1
  180. clearskies/input_outputs/headers.py +45 -0
  181. clearskies/input_outputs/input_output.py +91 -122
  182. clearskies/input_outputs/programmatic.py +69 -0
  183. clearskies/input_outputs/wsgi.py +23 -38
  184. clearskies/model.py +984 -183
  185. clearskies/parameters_to_properties.py +31 -0
  186. clearskies/query/__init__.py +12 -0
  187. clearskies/query/condition.py +223 -0
  188. clearskies/query/join.py +136 -0
  189. clearskies/query/query.py +196 -0
  190. clearskies/query/sort.py +27 -0
  191. clearskies/schema.py +82 -0
  192. clearskies/secrets/__init__.py +3 -31
  193. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer.py +15 -4
  194. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +11 -5
  195. clearskies/secrets/akeyless.py +88 -147
  196. clearskies/secrets/secrets.py +8 -8
  197. clearskies/security_header.py +15 -0
  198. clearskies/security_headers/__init__.py +8 -8
  199. clearskies/security_headers/cache_control.py +47 -110
  200. clearskies/security_headers/cors.py +40 -95
  201. clearskies/security_headers/csp.py +76 -151
  202. clearskies/security_headers/hsts.py +14 -16
  203. clearskies/test_base.py +8 -0
  204. clearskies/typing.py +11 -0
  205. clearskies/validator.py +37 -0
  206. clearskies/validators/__init__.py +33 -0
  207. clearskies/validators/after_column.py +62 -0
  208. clearskies/validators/before_column.py +13 -0
  209. clearskies/validators/in_the_future.py +32 -0
  210. clearskies/validators/in_the_future_at_least.py +11 -0
  211. clearskies/validators/in_the_future_at_most.py +10 -0
  212. clearskies/validators/in_the_past.py +32 -0
  213. clearskies/validators/in_the_past_at_least.py +10 -0
  214. clearskies/validators/in_the_past_at_most.py +10 -0
  215. clearskies/validators/maximum_length.py +26 -0
  216. clearskies/validators/maximum_value.py +29 -0
  217. clearskies/validators/minimum_length.py +26 -0
  218. clearskies/validators/minimum_value.py +29 -0
  219. clearskies/validators/required.py +35 -0
  220. clearskies/validators/timedelta.py +59 -0
  221. clearskies/validators/unique.py +31 -0
  222. clear_skies-1.22.31.dist-info/RECORD +0 -214
  223. clearskies/application.py +0 -29
  224. clearskies/authentication/auth0_jwks.py +0 -118
  225. clearskies/authentication/auth_exception.py +0 -2
  226. clearskies/authentication/jwks_jwcrypto.py +0 -51
  227. clearskies/backends/api_get_only_backend.py +0 -48
  228. clearskies/backends/example_backend.py +0 -43
  229. clearskies/backends/file_backend.py +0 -48
  230. clearskies/backends/json_backend.py +0 -7
  231. clearskies/backends/restful_api_advanced_search_backend.py +0 -103
  232. clearskies/binding_config.py +0 -16
  233. clearskies/column_types/__init__.py +0 -203
  234. clearskies/column_types/audit.py +0 -249
  235. clearskies/column_types/belongs_to.py +0 -271
  236. clearskies/column_types/boolean.py +0 -60
  237. clearskies/column_types/category_tree.py +0 -304
  238. clearskies/column_types/column.py +0 -373
  239. clearskies/column_types/created.py +0 -26
  240. clearskies/column_types/created_by_authorization_data.py +0 -26
  241. clearskies/column_types/created_by_header.py +0 -24
  242. clearskies/column_types/created_by_ip.py +0 -17
  243. clearskies/column_types/created_by_routing_data.py +0 -25
  244. clearskies/column_types/created_by_user_agent.py +0 -17
  245. clearskies/column_types/created_micro.py +0 -26
  246. clearskies/column_types/datetime.py +0 -109
  247. clearskies/column_types/datetime_micro.py +0 -12
  248. clearskies/column_types/email.py +0 -18
  249. clearskies/column_types/float.py +0 -43
  250. clearskies/column_types/has_many.py +0 -179
  251. clearskies/column_types/has_one.py +0 -60
  252. clearskies/column_types/integer.py +0 -41
  253. clearskies/column_types/json.py +0 -25
  254. clearskies/column_types/many_to_many.py +0 -278
  255. clearskies/column_types/many_to_many_with_data.py +0 -162
  256. clearskies/column_types/phone.py +0 -48
  257. clearskies/column_types/select.py +0 -11
  258. clearskies/column_types/string.py +0 -24
  259. clearskies/column_types/timestamp.py +0 -73
  260. clearskies/column_types/updated.py +0 -26
  261. clearskies/column_types/updated_micro.py +0 -26
  262. clearskies/column_types/uuid.py +0 -25
  263. clearskies/columns.py +0 -123
  264. clearskies/condition_parser.py +0 -172
  265. clearskies/contexts/build_context.py +0 -54
  266. clearskies/contexts/convert_to_application.py +0 -190
  267. clearskies/contexts/extract_handler.py +0 -37
  268. clearskies/contexts/test.py +0 -94
  269. clearskies/decorators/__init__.py +0 -41
  270. clearskies/decorators/allow_non_json_bodies.py +0 -9
  271. clearskies/decorators/auth0_jwks.py +0 -22
  272. clearskies/decorators/authorization.py +0 -10
  273. clearskies/decorators/binding_classes.py +0 -9
  274. clearskies/decorators/binding_modules.py +0 -9
  275. clearskies/decorators/bindings.py +0 -9
  276. clearskies/decorators/create.py +0 -10
  277. clearskies/decorators/delete.py +0 -10
  278. clearskies/decorators/docs.py +0 -14
  279. clearskies/decorators/get.py +0 -10
  280. clearskies/decorators/jwks.py +0 -26
  281. clearskies/decorators/merge.py +0 -124
  282. clearskies/decorators/patch.py +0 -10
  283. clearskies/decorators/post.py +0 -10
  284. clearskies/decorators/public.py +0 -11
  285. clearskies/decorators/response_headers.py +0 -10
  286. clearskies/decorators/return_raw_response.py +0 -9
  287. clearskies/decorators/schema.py +0 -10
  288. clearskies/decorators/secret_bearer.py +0 -24
  289. clearskies/decorators/security_headers.py +0 -10
  290. clearskies/di/standard_dependencies.py +0 -151
  291. clearskies/handlers/__init__.py +0 -41
  292. clearskies/handlers/advanced_search.py +0 -271
  293. clearskies/handlers/base.py +0 -479
  294. clearskies/handlers/callable.py +0 -192
  295. clearskies/handlers/create.py +0 -35
  296. clearskies/handlers/crud_by_method.py +0 -18
  297. clearskies/handlers/database_connector.py +0 -32
  298. clearskies/handlers/delete.py +0 -61
  299. clearskies/handlers/exceptions/__init__.py +0 -5
  300. clearskies/handlers/exceptions/not_found.py +0 -3
  301. clearskies/handlers/get.py +0 -156
  302. clearskies/handlers/health_check.py +0 -59
  303. clearskies/handlers/input_processing.py +0 -79
  304. clearskies/handlers/list.py +0 -530
  305. clearskies/handlers/mygrations.py +0 -82
  306. clearskies/handlers/request_method_routing.py +0 -47
  307. clearskies/handlers/restful_api.py +0 -218
  308. clearskies/handlers/routing.py +0 -62
  309. clearskies/handlers/schema_helper.py +0 -128
  310. clearskies/handlers/simple_routing.py +0 -206
  311. clearskies/handlers/simple_routing_route.py +0 -197
  312. clearskies/handlers/simple_search.py +0 -136
  313. clearskies/handlers/update.py +0 -102
  314. clearskies/handlers/write.py +0 -193
  315. clearskies/input_requirements/__init__.py +0 -78
  316. clearskies/input_requirements/after.py +0 -36
  317. clearskies/input_requirements/before.py +0 -36
  318. clearskies/input_requirements/in_the_future_at_least.py +0 -19
  319. clearskies/input_requirements/in_the_future_at_most.py +0 -19
  320. clearskies/input_requirements/in_the_past_at_least.py +0 -19
  321. clearskies/input_requirements/in_the_past_at_most.py +0 -19
  322. clearskies/input_requirements/maximum_length.py +0 -19
  323. clearskies/input_requirements/maximum_value.py +0 -19
  324. clearskies/input_requirements/minimum_length.py +0 -22
  325. clearskies/input_requirements/minimum_value.py +0 -19
  326. clearskies/input_requirements/required.py +0 -23
  327. clearskies/input_requirements/requirement.py +0 -25
  328. clearskies/input_requirements/time_delta.py +0 -38
  329. clearskies/input_requirements/unique.py +0 -18
  330. clearskies/mocks/__init__.py +0 -7
  331. clearskies/mocks/input_output.py +0 -124
  332. clearskies/mocks/models.py +0 -142
  333. clearskies/models.py +0 -350
  334. clearskies/security_headers/base.py +0 -12
  335. clearskies/tests/simple_api/models/__init__.py +0 -2
  336. clearskies/tests/simple_api/models/status.py +0 -23
  337. clearskies/tests/simple_api/models/user.py +0 -21
  338. clearskies/tests/simple_api/users_api.py +0 -64
  339. {clear_skies-1.22.31.dist-info → clear_skies-2.0.1.dist-info}/LICENSE +0 -0
  340. /clearskies/{contexts/bash.py → autodoc/py.typed} +0 -0
  341. /clearskies/{handlers/exceptions → exceptions}/authentication.py +0 -0
  342. /clearskies/{handlers/exceptions → exceptions}/authorization.py +0 -0
  343. /clearskies/{handlers/exceptions → exceptions}/client_error.py +0 -0
  344. /clearskies/{tests/__init__.py → input_outputs/py.typed} +0 -0
  345. /clearskies/{tests/simple_api/__init__.py → py.typed} +0 -0
clearskies/di/di.py CHANGED
@@ -1,70 +1,382 @@
1
- from ..binding_config import BindingConfig
2
- from .additional_config_auto_import import AdditionalConfigAutoImport
1
+ import datetime
3
2
  import inspect
3
+ import os
4
4
  import re
5
5
  import sys
6
- import os
7
- from ..functional import string
6
+ from types import ModuleType
7
+ from typing import Any, Callable
8
+
9
+ import requests
10
+ from requests.adapters import HTTPAdapter
11
+ from requests.packages.urllib3.util.retry import Retry # type: ignore
12
+
13
+ import clearskies.input_outputs.input_output
14
+ import clearskies.secrets
15
+ from clearskies.di.additional_config import AdditionalConfig
16
+ from clearskies.di.additional_config_auto_import import AdditionalConfigAutoImport
17
+ from clearskies.environment import Environment
18
+ from clearskies.functional import string
19
+
20
+
21
+ class Di:
22
+ """
23
+ Build a dependency injection object.
24
+
25
+ The dependency injection (DI) container is a key part of clearskies, so understanding how to both configure
26
+ them and get dependencies for your classes is important. Note however that you don't often have
27
+ to interact with the dependency injection container directly. All of the configuration options for
28
+ the DI container are also available to all the contexts, which is typically how you will build clearskies
29
+ applications. So, while you can create a DI container and use it directly, typically you'll just follow
30
+ the same basic techniques to configure your context and use that to run your application.
31
+
32
+ These are the main ways to configure the DI container:
33
+
34
+ 1. Import classes - each imported class is assigned an injection name based on the class name.
35
+ 2. Import modules - clearskies will iterate over the module and import all the classes and AdditionalConfigAutoImport classes it finds.
36
+ 3. Import AdditionalConfig classes - these allow you to programmatically define dependencies.
37
+ 4. Specify bindings - this allows you to provide any kind of value with whatever name you want.
38
+ 5. Specify class overrides - these allow you to swap out classes directly.
39
+ 6. Extending the Di class - this allows you to provide a default set of values.
40
+
41
+ When the DI system builds a class or calls a function, those classes and functions can themselves request any value
42
+ configured inside the DI container. There are three ways to request the desired dependencies:
43
+
44
+ 1. By type hinting a class on any arguments (excluding python built-ins)
45
+ 2. By specifying the name of a registered dependency
46
+ 3. By extending the `clearskies.di.AutoFillProps` class and creating class properties from the `clearskies.di.inject_from` module
47
+
48
+ Note that when a class is built/function is called by the DI container, keyword arguments are not allowed
49
+ (because the DI container doesn't know whether or not it should provide optional arguments). In addition,
50
+ the DI container must be able to resolve all positional arguments. If the class requests an argument
51
+ that the DI system does not recognize, an error will be thrown. Finally, it's a common pattern in clearskies
52
+ for some portion of the system to accept functions that will be called by the DI container. When this happens,
53
+ it's possible for clearskies to provide additional values that may be useful when executing the function.
54
+ The areas that accept functions like this also document the additional dependency injection names that are available.
55
+
56
+ Given the variety of ways that dependencies can be specified, it's important to understand the order the priority that
57
+ clearskies uses to determine what value to provide in case there is more than one source. That order is:
58
+
59
+ 1. Positional arguments with type hints:
60
+ 1. The override class if the type-hinted class has a registered override
61
+ 2. A value provided by an AdditionalConfig that can provide the type-hinted class
62
+ 3. The class itself if the class has been added explicitly via add_classes or implicitly via add_modules
63
+ 4. A clearskies built-in for predefined types
64
+ 2. All other positional arguments will have values provided based on the argument name and will receive
65
+ 1. Things set via `add_binding(name, value)`
66
+ 2. Class added via `add_classes` or `add_modules` which are made available according to their Di name
67
+ 3. An AdditionalConfig class with a corresponding `provide_[name]` function
68
+ 4. A clearskies built-in for predefined names
69
+
70
+ Here is the list of predefined values with their names and types:
71
+
72
+ | Injection Name | Injection Type | Value |
73
+ |----------------------|---------------------------------------------------------|-------------------------------------------------------------------------------------------|
74
+ | di | - | The active Di container |
75
+ | now | - | The current time in a datetime object, without timezone |
76
+ | utcnow | - | The current time in a datetime object, with timezone set to UTC |
77
+ | requests | requests.Session | A requests object configured to allow a small number of retries |
78
+ | input_output | clearskies.input_outputs.InputOutput | The clearskies builtin used for receiving and sending data to the client |
79
+ | uuid | - | `import uuid` - the uuid module builtin to python |
80
+ | environment | clearskies.Environment | A clearskies helper that access config info from the environment or a .env file |
81
+ | sys | - | `import sys` - the sys module builtin to python |
82
+ | oai3_schema_resolver | - | Used by the autodoc system |
83
+ | connection_details | - | A dictionary containing credentials that pymysql should use when connecting to a database |
84
+ | connection | - | A pymysql connection object |
85
+ | cursor | - | A pymysql cursor object |
86
+
87
+ Note: for dependencies with an injection name but no injection type, this means that to inject those values you
88
+ must name your argument with the given injection name. In all of the above cases though you can still add type
89
+ hints if desired. So, for instance, you can declare an argument of `utcnow: datetime.datetime`. clearskies
90
+ will ignore the type hint (since `datetime.datetime` isn't a type with a predefined value in clearskies) and
91
+ identify the value based on the name of the argument.
92
+
93
+ Note: multiple `AdditionalConfig` classes can be added to the Di container, and so a single injection name or class
94
+ can potentially be provided by multiple AdditionalConfig classes. AdditionalConfig classes are checked in the
95
+ reverse of the order they were addded in - classes added last are checked first when trying to find values.
96
+
97
+ Note: When importing modules, any classes that inherit from `AdditionalConfigAutoImport` are automatically added
98
+ to the list of additional config classes. These classes are added at the top of the list, so they are lower
99
+ priority than any classes you add via `add_additional_configs` or the `additional_configs` argument of the Di
100
+ constructor.
101
+
102
+ Note: Once a value is constructed, it is cached by the Di container and will automatically be provided for future
103
+ references of that same Di name or class. Arguments injected in a constructor will always receive the cached
104
+ value. If you want a "fresh" value of a given dependency, you have to attach instances from the
105
+ `clearskies.di.inject` module onto class proprties. The instances in the `inject` module generally
106
+ give options for cache control.
107
+
108
+ Here's an example that brings most of these pieces together. Once again, note that we're directly using
109
+ the Di contianer to build class/call functions, while normally you configure the Di container via your context
110
+ and then clearskies itself will build your class or call your functions as needed. Full explanation comes after
111
+ the example.
112
+
113
+ ```python
114
+ from clearskies.di import Di, AdditionalConfig
115
+
116
+
117
+ class SomeClass:
118
+ def __init__(self, my_value: int):
119
+ self.my_value = my_value
120
+
121
+
122
+ class MyClass:
123
+ def __init__(self, some_specific_value: int, some_class: SomeClass):
124
+ # `some_specific_value` is defined in both `MyProvider` and `MyOtherProvider`
125
+ # `some_class` will be injected from the type hint, and the actual instance is made by our
126
+ # `MyProvider`
127
+ self.final_value = some_specific_value * some_class.my_value
128
+
129
+
130
+ class VeryNeedy:
131
+ def __init__(self, my_class, some_other_value: str):
132
+ # We're relying on the automatic conversion of class names to snake_case, so clearskies
133
+ # will connect `my_class` to `MyClass`, which we provided directly to the Di container.
134
+
135
+ # some_other_value is specified as a binding
136
+ self.my_class = my_class
137
+ self.some_other_value = some_other_value
138
+
139
+
140
+ class MyOtherProvider(AdditionalConfig):
141
+ def provide_some_specific_value(self):
142
+ # the order of additional configs will cause this function to be invoked
143
+ # (and hence some_specific_value will be `10`) despite the fact that MyProvider
144
+ # also has a `provide_` function with the same name.
145
+ return 10
146
+
147
+
148
+ class MyProvider(AdditionalConfig):
149
+ def provide_some_specific_value(self):
150
+ # note that the name of our function matches the name of the argument
151
+ # expected by MyClass.__init__. Again though, we won't get called because
152
+ # the order the AdditionalConfigs are loaded gives `MyOtherProvider` priority.
153
+ return 5
154
+
155
+ def can_build_class(self, class_to_check: type) -> bool:
156
+ # this lets the DI container know that if someone wants an instance
157
+ # of SomeClass, we can build it.
158
+ return class_to_check == SomeClass
159
+
160
+ def build_class(self, class_to_provide: type, argument_name: str, di, context: str = ""):
161
+ if class_to_provide == SomeClass:
162
+ return SomeClass(5)
163
+ raise ValueError(
164
+ f"I was asked to build a class I didn't expect '{class_to_provide.__name__}'"
165
+ )
166
+
8
167
 
168
+ di = Di(
169
+ classes=[MyClass, VeryNeedy, SomeClass],
170
+ additional_configs=[MyProvider(), MyOtherProvider()],
171
+ bindings={
172
+ "some_other_value": "dog",
173
+ },
174
+ )
175
+
176
+
177
+ def my_function(my_fancy_argument: VeryNeedy):
178
+ print(f"Jane owns {my_fancy_argument.my_class.final_value}:")
179
+ print(f"{my_fancy_argument.some_other_value}s")
180
+
181
+
182
+ print(di.call_function(my_function))
183
+ # prints 'Jane owns 50 dogs'
184
+ ```
185
+
186
+ When `call_function` is executed on `my_function`, the di system checks the calling arguments of `my_function`
187
+ and runs through the priority list above to populate them. `my_function` has one argument -
188
+ `my_fancy_argument: VeryNeedy`, which it resolves as so:
189
+
190
+ 1. The type hint (`VeryNeedy`) matches an imported class. Therefore, clearskies will build an instance of VeryNeedy and
191
+ provide it for `my_fancy_argument`.
192
+ 2. clearskies inpsects the constructor for `VeryNeedy` and finds two arguments, `my_class` and `some_other_value: str`,
193
+ which it attempts to build.
194
+ 1. `my_class` has no type hint, so clearskies falls back on name-based resolution. A class called `MyClass` was imported,
195
+ and per standard naming convention, this automatically becomes available via the name `my_class`. Thus, clearskies
196
+ prepares to build an instance of `MyClass`. `MyClass` has two arguments: `some_specific_value: int` and
197
+ `some_class: SomeClass`
198
+ 1. For `some_specific_value`, the Di service falls back on named-based resolution (because it will never try to
199
+ provide values for type-hints of built-in types). Both `MyOtherProvider` and `MyProvider` have a method called
200
+ `provide_some_specific_value`, so both can be used to provide this value. Since `MyOtherProvider` was added to
201
+ the Di container last, it takes priority. Therefore, clearskies calls `MyOtherProvider.provide_some_specific_value`
202
+ to create the value that it will populate into the `some_specific_value` parameter.
203
+ 2. For `some_class: SomeClass`, clearskies evaluates the type-hint. It works through the additional configs and, since
204
+ `MyProvider` returns True when `can_build_class` is called with `SomeClass`, the Di container will use this
205
+ additional config to create the value for the `some_class` argument. Therefore, clearskies calls
206
+ `MyProvider.build_class(SomeClass, 'some_class', di)` and the return value is used for the `some_class` argument.
207
+ 2. `some_other_value` uses a built-in for a type hint, so clearskies falls back on name-based resolution. It falls back
208
+ on the registered binding of `"dog"` to the name `"some_other_value"`, so clearskies provides `"dog"`.
209
+ """
210
+
211
+ _added_modules: dict[int, bool] = {}
212
+ _additional_configs: list[AdditionalConfig] = []
213
+ _bindings: dict[str, Any] = {}
214
+ _building: dict[int, str] = {}
215
+ _classes: dict[str, dict[str, int | type]] = {}
216
+ _prepared: dict[str, Any] = {}
217
+ _class_overrides_by_name: dict[str, type] = {}
218
+ _class_overrides_by_class: dict[type, Any] = {}
219
+ _type_hint_disallow_list = [int, float, str, dict, list, datetime.datetime]
220
+ _now: datetime.datetime | None = None
221
+ _utcnow: datetime.datetime | None = None
222
+ _predefined_classes_name_map: dict[type, str] = {
223
+ requests.Session: "requests",
224
+ clearskies.input_outputs.input_output.InputOutput: "input_output",
225
+ Environment: "environment",
226
+ }
227
+
228
+ def __init__(
229
+ self,
230
+ classes: type | list[type] = [],
231
+ modules: ModuleType | list[ModuleType] = [],
232
+ bindings: dict[str, Any] = {},
233
+ additional_configs: AdditionalConfig | list[AdditionalConfig] = [],
234
+ class_overrides: dict[type, Any] = {},
235
+ overrides: dict[str, type] = {},
236
+ now: datetime.datetime | None = None,
237
+ utcnow: datetime.datetime | None = None,
238
+ ):
239
+ """
240
+ Create a dependency injection container.
9
241
 
10
- class DI:
11
- _bindings = None
12
- _building = None
13
- _classes = None
14
- _prepared = None
15
- _added_modules = None
16
- _additional_configs = None
17
- _class_mocks = None
242
+ For details on the parameters, see the related methods:
18
243
 
19
- def __init__(self, classes=None, modules=None, bindings=None, additional_configs=None):
20
- self._bindings = {}
21
- self._prepared = {}
22
- self._classes = {}
23
- self._building = {}
244
+ classes -> di.add_classes()
245
+ modules -> di.add_modules()
246
+ bindings -> di.add_binding()
247
+ additional_configs -> di.add_additional_configs()
248
+ class_overrides -> di.add_class_override()
249
+ """
24
250
  self._added_modules = {}
25
251
  self._additional_configs = []
26
- self._class_mocks = {}
252
+ self._bindings = {}
253
+ self._building = {}
254
+ self._classes = {}
255
+ self._class_overrides_by_name = {}
256
+ self._class_overrides_by_class = {}
257
+ self._prepared = {}
27
258
  if classes is not None:
28
259
  self.add_classes(classes)
29
260
  if modules is not None:
30
261
  self.add_modules(modules)
31
262
  if bindings is not None:
32
263
  for key, value in bindings.items():
33
- self.bind(key, value)
264
+ self.add_binding(key, value)
34
265
  if additional_configs is not None:
35
266
  self.add_additional_configs(additional_configs)
267
+ if class_overrides:
268
+ for key, value in class_overrides.items(): # type: ignore
269
+ self.add_class_override(key, value) # type: ignore
270
+ if overrides:
271
+ for key, value in overrides.items():
272
+ self.add_override(key, value)
273
+ if now:
274
+ self.set_now(now)
275
+ if utcnow:
276
+ self.set_utcnow(utcnow)
277
+
278
+ def add_classes(self, classes: type | list[type]) -> None:
279
+ """
280
+ Record any class that should be made available for injection.
281
+
282
+ All classes that come in here become available via their injection name, which is calculated
283
+ by converting the class name from TitleCase to snake_case. e.g. the following class:
284
+
285
+ ```python
286
+ class MyClass:
287
+ pass
288
+ ```
289
+
290
+ gets an injection name of `my_class`. Also, clearskies will only resolve and reject based on type hints
291
+ if those classes are first added via `add_classes`. See the following example:
292
+
293
+ ```python
294
+ from clearskies.di import Di
36
295
 
37
- def add_classes(self, classes):
38
- if inspect.isclass(classes):
296
+ class MyClass:
297
+ name = "Simple Demo"
298
+
299
+ di = Di(classes=[MyClass])
300
+ # equivalent: di.add_classes(MyClass), di.add_classes([MyClass])
301
+
302
+ def my_function(my_class):
303
+ print(my_class.name)
304
+
305
+ def my_function_with_type_hinting(the_name_no_longer_matters: MyClass):
306
+ print(my-class.name)
307
+
308
+ # both print 'Simple Demo'
309
+ di.call_function(my_function)
310
+ di.call_function(my_function_with_type_hinting)
311
+ ```
312
+ """
313
+ if not isinstance(classes, list):
39
314
  classes = [classes]
40
315
  for add_class in classes:
41
316
  name = string.camel_case_to_snake_case(add_class.__name__)
42
- # if name in self._classes:
43
- ## if we're re-adding the same class twice then just ignore it.
44
- # if id(add_class) == self._classes[name]['id']:
45
- # continue
46
-
47
- ## otherwise throw an exception
48
- # raise ValueError(f"More than one class with a name of '{name}' was added")
49
-
50
317
  self._classes[name] = {"id": id(add_class), "class": add_class}
318
+ self._classes[add_class] = {"id": id(add_class), "class": add_class} # type: ignore
51
319
 
52
320
  # if this is a model class then also add a plural version of its name
53
321
  # to the DI configuration
54
322
  if hasattr(add_class, "id_column_name"):
55
323
  self._classes[string.make_plural(name)] = {"id": id(add_class), "class": add_class}
56
324
 
57
- def add_modules(self, modules, root=None, is_root=True):
58
- if inspect.ismodule(modules):
325
+ def add_modules(
326
+ self, modules: ModuleType | list[ModuleType], root: str | None = None, is_root: bool = True
327
+ ) -> None:
328
+ """
329
+ Add a module to the dependency injection container.
330
+
331
+ clearskies will iterate through the module, adding all imported classes into the dependency injection container.
332
+
333
+ So, consider the following file structure inside a module:
334
+
335
+ ```
336
+ my_module/
337
+ __init__.py
338
+ my_sub_module/
339
+ __init__.py
340
+ my_class.py
341
+ ```
342
+
343
+ Assuming that the submodule and class are imported at each level (e.g. my_module/__init__.py imports my_sub_module,
344
+ and my_sub_module/__init__.py imports my_class.py) then you can:
345
+
346
+ ```python
347
+ from clearksies.di import Di
348
+ import my_module
349
+
350
+ di = Di()
351
+ di.add_modules([
352
+ my_module
353
+ ]) # also equivalent: di.add_modules(my_module), or Di(modules=[my_module])
354
+
355
+
356
+ def my_function(my_class):
357
+ pass
358
+
359
+
360
+ di.call_function(my_function)
361
+ ```
362
+
363
+ `my_function` will be called and `my_class` will automatically be populated with an instance of
364
+ `my_module.sub_module.my_class.MyClass`.
365
+
366
+ Note that MyClass will be able to declare its own dependencies per normal dependency injection rules.
367
+ See the main docblock in the clearskies.di.Di class for more details about how all the pieces work together.
368
+ """
369
+ if not isinstance(modules, list):
59
370
  modules = [modules]
60
371
 
61
372
  for module in modules:
373
+ # skip internal python modules
62
374
  if not hasattr(module, "__file__") or not module.__file__:
63
375
  continue
64
376
  module_id = id(module)
65
377
  if is_root:
66
378
  root = os.path.dirname(module.__file__)
67
- root_len = len(root)
379
+ root_len = len(root) if root else 0
68
380
  if module_id in self._added_modules:
69
381
  continue
70
382
  self._added_modules[module_id] = True
@@ -101,63 +413,183 @@ class DI:
101
413
  break
102
414
  self.add_modules([item], root=root, is_root=False)
103
415
 
104
- def add_additional_configs(self, additional_configs):
105
- if type(additional_configs) != list:
416
+ def add_additional_configs(self, additional_configs: AdditionalConfig | list[AdditionalConfig]) -> None:
417
+ """
418
+ Add an additional config instance to the dependency injection container.
419
+
420
+ Additional config class provide an additional way to provide dependencies into the dependency
421
+ injection system. For more details about how to use them, see both base classes:
422
+
423
+ 1. clearskies.di.additional_config.AdditionalConfig
424
+ 2. clearskies.di.additional_config_auto_import.AdditionalConfigAutoImport
425
+
426
+ To use this method:
427
+
428
+ ```python
429
+ import clearskies.di
430
+
431
+
432
+ class MyConfig(clearskies.di.AdditionalConfig):
433
+ def provide_some_value(self):
434
+ return 2
435
+
436
+ def provide_another_value(self, some_value):
437
+ return some_value * 2
438
+
439
+
440
+ di = clearskies.di.Di()
441
+ di.add_additional_configs([MyConfig()])
442
+ # equivalents:
443
+ # di.add_additional_configs(MyConfig())
444
+ # di = clearskies.di.Di(additional_configs=[MyConfig()])
445
+
446
+
447
+ def my_function(another_value):
448
+ print(another_value) # prints 4
449
+
450
+
451
+ di.call_function(my_function)
452
+ ```
453
+ """
454
+ if not isinstance(additional_configs, list):
106
455
  additional_configs = [additional_configs]
107
- for additional_config in additional_configs:
108
- self._additional_configs.append(
109
- additional_config() if inspect.isclass(additional_config) else additional_config
110
- )
456
+ self._additional_configs.extend(additional_configs)
457
+
458
+ def add_binding(self, key, value):
459
+ """
460
+ Provide a specific value for name-based injection.
111
461
 
112
- def bind(self, key, value):
462
+ This method attaches a value to a specific dependency injection name.
463
+
464
+ ```python
465
+ import clearskies.di
466
+
467
+ di = clearskies.di.Di()
468
+ di.add_binding("my_name", 12345)
469
+ # equivalent:
470
+ # di = clearskies.di.Di(bindings={"my_name": 12345})
471
+
472
+
473
+ def my_function(my_name):
474
+ print(my_name) # prints 12345
475
+
476
+
477
+ di.call_function(my_function)
478
+ ```
479
+ """
113
480
  if key in self._building:
114
481
  raise KeyError(f"Attempt to set binding for '{key}' while '{key}' was already being built")
115
482
 
116
- # classes and binding configs are placed in self._bindings, but any other prepared value goes straight
117
- # into self._prepared
118
- if inspect.isclass(value) or isinstance(value, BindingConfig):
483
+ # classes are placed in self._bindings, but any other prepared value goes straight into self._prepared
484
+ if inspect.isclass(value):
119
485
  self._bindings[key] = value
120
486
  if key in self._prepared:
121
487
  del self._prepared[key]
122
488
  else:
123
489
  self._prepared[key] = value
124
490
 
125
- def build(self, thing, context=None, cache=False):
491
+ def add_class_override(self, class_to_override: type, replacement: Any) -> None:
492
+ """
493
+ Override a class for type-based injection.
494
+
495
+ This function allows you to replace/mock class provided when relying on type hinting for injection.
496
+ This is most often (but not exclusively) used for mocking out classes during texting. Note that
497
+ this only overrides that specific class - not classes that extend it.
498
+
499
+ Example:
500
+ ```python
501
+ from clearskies.import Di
502
+
503
+ class TypeHintedClass:
504
+ my_value = 5
505
+
506
+ class ReplacementClass:
507
+ my_value = 10
508
+
509
+ di = Di()
510
+ di.add_classes(TypeHintedClass)
511
+ di.add_class_override(TypeHintedClass, ReplacementClass)
512
+ # also di = Di(class_overrides={TypeHintedClass: ReplacementClass})
513
+
514
+ def my_function(some_value: TypeHintedClass):
515
+ print(some_value.my_value) # prints 10
516
+
517
+ di.call_function(my_function)
518
+ ```
519
+ """
520
+ if not inspect.isclass(class_to_override):
521
+ raise ValueError(
522
+ "Invalid value passed to add_class_override for 'class_or_name' parameter: it was neither a name nor a class"
523
+ )
524
+
525
+ self._class_overrides_by_class[class_to_override] = replacement
526
+
527
+ def has_class_override(self, class_to_check: type) -> bool:
528
+ return class_to_check in self._class_overrides_by_class
529
+
530
+ def get_override_by_class(self, object_to_override: Any) -> Any:
531
+ if object_to_override.__class__ not in self._class_overrides_by_class:
532
+ return object_to_override
533
+
534
+ override = self._class_overrides_by_class[object_to_override.__class__]
535
+ if inspect.isclass(override):
536
+ return self.build_class(override)
537
+ if hasattr(override, "injectable_properties"):
538
+ override.injectable_properties(self)
539
+ return override
540
+
541
+ def add_override(self, name: str, replacement_class: type) -> None:
542
+ """Override a specific injection name by specifying a class that should be injected in its place."""
543
+ if not inspect.isclass(replacement_class):
544
+ raise ValueError(
545
+ "Invalid value passed to add_override for 'replacement_class' parameter: a class should be passed but I got a "
546
+ + str(type(replacement_class))
547
+ )
548
+
549
+ self._class_overrides_by_name[name] = replacement_class
550
+
551
+ def set_now(self, now: datetime.datetime) -> None:
552
+ """Set the current time which will be passed along to any dependency arguments named `now`."""
553
+ if now.tzinfo is not None:
554
+ raise ValueError(
555
+ "set_now() was passed a datetime object with timezone information - it should only be given timezone-naive datetime objects. Maybe you meant to use di.set_utcnow()"
556
+ )
557
+ self._now = now
558
+
559
+ def set_utcnow(self, utcnow: datetime.datetime) -> None:
560
+ """Set the current time which will be passed along to any dependency arguments named `utcnow`."""
561
+ if not utcnow.tzinfo:
562
+ raise ValueError(
563
+ "set_utcnow() was passed a datetime object without timezone information - it should only be given timezone-aware datetime objects. Maybe you meant to use di.set_now()"
564
+ )
565
+ self._utcnow = utcnow
566
+
567
+ def build(self, thing: Any, context: str | None = None, cache: bool = False) -> Any:
568
+ """
569
+ Have the dependency injection container build a value for you.
570
+
571
+ This will accept either a dependency injection name or a class.
572
+ """
126
573
  if inspect.isclass(thing):
127
574
  return self.build_class(thing, context=context, cache=cache)
128
- elif isinstance(thing, BindingConfig):
129
- if not inspect.isclass(thing.object_class):
130
- raise ValueError("BindingConfig contained a non-class!")
131
- instance = self.build_class(thing.object_class, context=context, cache=cache)
132
- if (thing.args or thing.kwargs) and not hasattr(instance, "configure"):
133
- raise ValueError(
134
- f"Cannot build instance of class '{instance.__class__.__name__}' "
135
- + "because it is missing the 'configure' method"
136
- )
137
- instance.configure(*thing.args, **thing.kwargs)
138
- return instance
139
575
  elif type(thing) == str:
140
576
  return self.build_from_name(thing, context=context, cache=cache)
577
+ elif callable(thing):
578
+ raise ValueError("build received a callable: you probably want to use di.call_function()")
141
579
 
142
580
  # if we got here then our thing is already and object of some sort and doesn't need anything further
143
581
  return thing
144
582
 
145
- def build_from_name(self, name, context=None, cache=False):
583
+ def build_from_name(self, name: str, context: str | None = None, cache: bool = False) -> Any:
146
584
  """
147
- Builds a dependency based on its name
585
+ Build a dependency based on its name.
148
586
 
149
587
  Order of priority:
150
- 1. 'di' (aka self)
151
- 2. Already prepared things
152
- 3. Things set via `bind(name, value)`
153
- 4. Class via add_classes or add_modules
154
- 5. Things set in "additional_config" classes
155
- 6. Method on DI class called `provide_[name]`
156
- 7. Already prepared things
588
+ 1. Things set via `add_binding(name, value)`
589
+ 2. Class added via `add_classes` or `add_modules` which are made available according to their Di name
590
+ 3. An AdditionalConfig class with a corresponding `provide_[name]` function
591
+ 4. The Di class itself if it has a matching `provide_[name]` function (aka the builtins)
157
592
  """
158
- if name == "di":
159
- return self
160
-
161
593
  if name in self._prepared and cache:
162
594
  return self._prepared[name]
163
595
 
@@ -167,10 +599,15 @@ class DI:
167
599
  self._prepared[name] = built_value
168
600
  return built_value
169
601
 
170
- if name in self._classes:
171
- built_value = self.build_class(self._classes[name]["class"], context=context)
602
+ if name in self._classes or name in self._class_overrides_by_name:
603
+ class_to_build = (
604
+ self._class_overrides_by_name[name]
605
+ if name in self._class_overrides_by_name
606
+ else self._classes[name]["class"]
607
+ ) # type: ignore
608
+ built_value = self.build_class(class_to_build, context=context) # type: ignore
172
609
  if cache:
173
- self._prepared[name] = built_value
610
+ self._prepared[name] = built_value # type: ignore
174
611
  return built_value
175
612
 
176
613
  # additional configs are meant to override ones that come before, with most recent ones
@@ -179,14 +616,14 @@ class DI:
179
616
  additional_config = self._additional_configs[index]
180
617
  if not additional_config.can_build(name):
181
618
  continue
182
- built_value = additional_config.build(name, self, context=context)
183
- if cache and self.call_function(additional_config.can_cache, name=name, context=context):
619
+ built_value = additional_config.build(name, self, context if context else "")
620
+ if cache and additional_config.can_cache(name, self, context if context else ""):
184
621
  self._prepared[name] = built_value
185
622
  return built_value
186
623
 
187
624
  if hasattr(self, f"provide_{name}"):
188
625
  built_value = self.call_function(getattr(self, f"provide_{name}"))
189
- if cache:
626
+ if cache and self.can_cache(name, context if context else ""):
190
627
  self._prepared[name] = built_value
191
628
  return built_value
192
629
 
@@ -203,36 +640,27 @@ class DI:
203
640
  + f"or a corresponding 'provide_{name}' method for this name."
204
641
  )
205
642
 
206
- def mock_class(self, class_or_name, replacement):
207
- if type(class_or_name) == str:
208
- name = class_or_name
209
- elif inspect.isclass(class_or_name):
210
- name = string.camel_case_to_snake_case(class_or_name.__name__)
211
- else:
212
- raise ValueError(
213
- "Invalid value passed to 'mock_class' for 'class_or_name' parameter: it was neither a name nor a class"
214
- )
215
- if not inspect.isclass(replacement):
216
- raise ValueError(
217
- "Invalid value passed to 'mock_class' for 'replacement' parameter: a class should be passed but I got a "
218
- + str(type(replacement))
219
- )
643
+ def build_argument(self, argument_name: str, type_hint: type | None, context: str = "", cache: bool = True) -> Any:
644
+ """
645
+ Build an argument given the name and type hint.
220
646
 
221
- self._class_mocks[name] = replacement
647
+ Runs through the resolution order described in the docblock at the top of the Di class to build an argument given
648
+ its name and type-hint.
649
+ """
650
+ built_value = self.build_class_from_type_hint(argument_name, type_hint, context=context, cache=True)
651
+ if built_value is not None:
652
+ return built_value
653
+ return self.build_from_name(argument_name, context=context, cache=True)
222
654
 
223
- def build_class(self, class_to_build, context=None, name=None, cache=False):
655
+ def build_class(self, class_to_build: type, context=None, cache=True) -> Any:
224
656
  """
225
- Builds a class
657
+ Build a class.
226
658
 
227
659
  The class constructor cannot accept any kwargs. See self._disallow_kwargs for more details
228
660
  """
229
- if name is None:
230
- name = string.camel_case_to_snake_case(class_to_build.__name__)
231
- if name in self._prepared and cache:
232
- return self._prepared[name]
233
-
234
- if name in self._class_mocks:
235
- class_to_build = self._class_mocks[name]
661
+ if class_to_build in self._prepared and cache:
662
+ return self._prepared[class_to_build] # type: ignore
663
+ my_class_name = class_to_build.__name__
236
664
 
237
665
  init_args = inspect.getfullargspec(class_to_build)
238
666
  if init_args.defaults is not None:
@@ -241,9 +669,11 @@ class DI:
241
669
  # ignore the first argument because that is just `self`
242
670
  build_arguments = init_args.args[1:]
243
671
  if not build_arguments:
672
+ if hasattr(class_to_build, "injectable_properties"):
673
+ class_to_build.injectable_properties(self)
244
674
  built_value = class_to_build()
245
675
  if cache:
246
- self._prepared[name] = built_value
676
+ self._prepared[class_to_build] = built_value # type: ignore
247
677
  return built_value
248
678
 
249
679
  # self._building will help us keep track of what we're already building, and what we are building it for.
@@ -257,28 +687,99 @@ class DI:
257
687
  f"'{self._building[class_id]}'" if self._building[class_id] is not None else "itself"
258
688
  )
259
689
  raise ValueError(
260
- f"Circular dependencies detected while building '{class_to_build.__name__}' because '"
261
- + f"{class_to_build.__name__} is a dependency of both '{context}' and {original_context_label}"
690
+ f"Circular dependencies detected while building '{my_class_name}' because '"
691
+ + f"{my_class_name} is a dependency of both '{context}' and {original_context_label}"
262
692
  )
263
693
 
264
694
  self._building[class_id] = context
265
695
  # Turn on caching when building the automatic dependencies that get injected into a class constructor
266
696
  args = [
267
- self.build_from_name(build_argument, context=class_to_build.__name__, cache=True)
697
+ self.build_argument(
698
+ build_argument, init_args.annotations.get(build_argument, None), context=my_class_name, cache=True
699
+ )
268
700
  for build_argument in build_arguments
269
701
  ]
702
+
270
703
  del self._building[class_id]
271
704
 
272
705
  built_value = class_to_build(*args)
706
+ if hasattr(built_value, "injectable_properties"):
707
+ built_value.injectable_properties(self)
273
708
  if cache:
274
- self._prepared[name] = built_value
709
+ self._prepared[class_to_build] = built_value # type: ignore
275
710
  return built_value
276
711
 
277
- def call_function(self, callable_to_execute, **kwargs):
712
+ def build_class_from_type_hint(
713
+ self, argument_name: str, class_to_build: type | None, context: str = "", cache: bool = True
714
+ ) -> Any | None:
715
+ """
716
+ Build an argument from a type hint.
717
+
718
+ Note that in many cases we can't actually build the thing. It may be a type hint of a built-in or some other value
719
+ that we're not configured to deal with. In that case, just return None and the calling method will deal with it.
720
+
721
+ This follows the resolution order defined in the docblock of the Di class.
722
+ """
723
+ # these first checks just verify that it is something that we can actually build
724
+ if not class_to_build:
725
+ return None
726
+ if not callable(class_to_build):
727
+ return None
728
+ if inspect.isabstract(class_to_build):
729
+ return None
730
+
731
+ # then first things first: check our class overrides
732
+ if class_to_build in self._class_overrides_by_class:
733
+ return self.build_class(self._class_overrides_by_class[class_to_build], context=context, cache=cache)
734
+
735
+ # next check our additional config classes
736
+ built_value = None
737
+ can_cache = False
738
+ for index in range(len(self._additional_configs) - 1, -1, -1):
739
+ additional_config = self._additional_configs[index]
740
+ if not additional_config.can_build_class(class_to_build):
741
+ continue
742
+
743
+ built_value = additional_config.build_class(class_to_build, argument_name, self, context=context)
744
+ can_cache = additional_config.can_cache_class(class_to_build, self, context)
745
+ break
746
+
747
+ # a small handful of predefined classes
748
+ if class_to_build in self._predefined_classes_name_map:
749
+ dependency_name = self._predefined_classes_name_map[class_to_build]
750
+ built_value = self.call_function(getattr(self, f"provide_{dependency_name}"))
751
+ can_cache = self.can_cache(dependency_name, context if context else "")
752
+
753
+ # finally, if we found something, cache and/or return it
754
+ if built_value is not None:
755
+ if cache and can_cache:
756
+ self._prepared[class_to_build] = built_value # type: ignore
757
+ return built_value
758
+
759
+ # last but not least we build the class itself as long as it has been imported into the Di system
760
+ if class_to_build in self._classes:
761
+ return self.build_class(class_to_build, context=context, cache=cache)
762
+
763
+ return None
764
+
765
+ def call_function(self, callable_to_execute: Callable, **kwargs):
278
766
  """
279
- Calls a function, building any positional arguments and providing them.
767
+ Call a function, building any positional arguments and providing them.
280
768
 
281
- Any kwargs passed to call_function will populate the equivalent dependencies
769
+ Any kwargs passed to call_function will populate the equivalent dependencies.
770
+
771
+ ```python
772
+ from clearskies.di import Di
773
+
774
+ di = Di(bindings={"some_name": "hello"})
775
+
776
+
777
+ def my_function(some_name, some_other_name):
778
+ print(f"{some_name} {some_other_value}") # prints 'hello world'
779
+
780
+
781
+ di.call_function(my_function, some_other_value="world")
782
+ ```
282
783
  """
283
784
  args_data = inspect.getfullargspec(callable_to_execute)
284
785
 
@@ -299,9 +800,13 @@ class DI:
299
800
  kwarg_names = call_arguments[nargs - nkwargs :]
300
801
 
301
802
  callable_args = [
302
- kwargs[arg]
303
- if arg in kwargs
304
- else self.build_from_name(arg, context=callable_to_execute.__name__, cache=True)
803
+ (
804
+ kwargs[arg]
805
+ if arg in kwargs
806
+ else self.build_argument(
807
+ arg, args_data.annotations.get(arg, None), context=callable_to_execute.__name__, cache=True
808
+ )
809
+ )
305
810
  for arg in arg_names
306
811
  ]
307
812
  callable_kwargs = {}
@@ -314,7 +819,7 @@ class DI:
314
819
 
315
820
  def _disallow_kwargs(self, action):
316
821
  """
317
- Raises an exception
822
+ Raise an exception.
318
823
 
319
824
  This is used to raise an exception and stop building a class if its constructor accepts kwargs. To be clear,
320
825
  we actually can support kwargs - it just doesn't make much sense. The issue is that keywords are
@@ -332,16 +837,132 @@ class DI:
332
837
  """
333
838
  raise ValueError(f"Cannot {action} because it has keyword arguments.")
334
839
 
335
- @classmethod
336
- def init(cls, *binding_classes, **bindings):
337
- modules = None
338
- additional_configs = None
339
- if "modules" in bindings:
340
- modules = bindings["modules"]
341
- del bindings["modules"]
342
- if "additional_configs" in bindings:
343
- additional_configs = bindings["additional_configs"]
344
- del bindings["additional_configs"]
345
-
346
- di = cls(classes=binding_classes, modules=modules, bindings=bindings, additional_configs=additional_configs)
347
- return di
840
+ def can_cache(self, name: str, context: str) -> bool:
841
+ """Control whether or not to cache a value built by the DI container."""
842
+ if name == "now" or name == "utcnow":
843
+ return False
844
+ return True
845
+
846
+ def provide_di(self):
847
+ return self
848
+
849
+ def provide_requests(self):
850
+ retry_strategy = Retry(
851
+ total=3,
852
+ status_forcelist=[429, 500, 502, 503, 504],
853
+ backoff_factor=1,
854
+ allowed_methods=["GET", "POST", "DELETE", "OPTIONS", "PATCH"],
855
+ )
856
+ adapter = HTTPAdapter(max_retries=retry_strategy)
857
+ session = requests.Session()
858
+ session.mount("https://", adapter)
859
+ session.mount("http://", adapter)
860
+ return session
861
+
862
+ def provide_sys(self):
863
+ import sys
864
+
865
+ return sys
866
+
867
+ def provide_environment(self):
868
+ return Environment(os.getcwd() + "/.env", os.environ, {})
869
+
870
+ def provide_connection_no_autocommit(self, connection_details):
871
+ # I should probably just switch things so that autocommit is *off* by default
872
+ # and only have one of these, but for now I'm being lazy.
873
+ try:
874
+ import pymysql
875
+ except:
876
+ raise ValueError(
877
+ "The cursor requires pymysql to be installed. This is an optional dependency of clearskies, so to include it do a `pip install 'clear-skies[mysql]'`"
878
+ )
879
+
880
+ return pymysql.connect(
881
+ user=connection_details["username"],
882
+ password=connection_details["password"],
883
+ host=connection_details["host"],
884
+ database=connection_details["database"],
885
+ port=connection_details.get("port", 3306),
886
+ ssl_ca=connection_details.get("ssl_ca", None),
887
+ autocommit=False,
888
+ connect_timeout=2,
889
+ cursorclass=pymysql.cursors.DictCursor,
890
+ )
891
+
892
+ def provide_connection(self, connection_details):
893
+ try:
894
+ import pymysql
895
+ except:
896
+ raise ValueError(
897
+ "The cursor requires pymysql to be installed. This is an optional dependency of clearskies, so to include it do a `pip install 'clear-skies[mysql]'`"
898
+ )
899
+
900
+ return pymysql.connect(
901
+ user=connection_details["username"],
902
+ password=connection_details["password"],
903
+ host=connection_details["host"],
904
+ database=connection_details["database"],
905
+ port=connection_details.get("port", 3306),
906
+ ssl_ca=connection_details.get("ssl_ca", None),
907
+ autocommit=True,
908
+ connect_timeout=2,
909
+ cursorclass=pymysql.cursors.DictCursor,
910
+ )
911
+
912
+ def provide_connection_details(self, environment):
913
+ try:
914
+ port = environment.get("db_port")
915
+ except:
916
+ port = 3306
917
+
918
+ try:
919
+ ssl_ca = environment.get("db_ssl_ca")
920
+ except:
921
+ ssl_ca = None
922
+
923
+ return {
924
+ "username": environment.get("db_username"),
925
+ "password": environment.get("db_password"),
926
+ "host": environment.get("db_host"),
927
+ "database": environment.get("db_database"),
928
+ "port": port,
929
+ "ssl_ca": ssl_ca,
930
+ }
931
+
932
+ def provide_cursor(self, connection):
933
+ return connection.cursor()
934
+
935
+ def provide_now(self):
936
+ return datetime.datetime.now() if self._now is None else self._now
937
+
938
+ def provide_utcnow(self):
939
+ return datetime.datetime.now(datetime.timezone.utc) if self._utcnow is None else self._utcnow
940
+
941
+ def provide_input_output(self):
942
+ raise AttributeError(
943
+ "The dependency injector requested an InputOutput but none has been configured. Alternatively, if you directly called `di.build('input_output')` then try again with `di.build('input_output', cache=True)`"
944
+ )
945
+
946
+ def provide_oai3_schema_resolver(self):
947
+ from clearskies import autodoc
948
+
949
+ return autodoc.formats.oai3_json.OAI3SchemaResolver()
950
+
951
+ def provide_uuid(self):
952
+ import uuid
953
+
954
+ return uuid
955
+
956
+ def provide_secrets(self):
957
+ return clearskies.secrets.Secrets()
958
+
959
+ def provide_memory_backend_default_data(self):
960
+ return []
961
+
962
+ def provide_global_table_prefix(self):
963
+ return ""
964
+
965
+ def provide_akeyless(self):
966
+ import akeyless # type: ignore
967
+
968
+ return akeyless