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