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