vectorwave 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. tests/__init__.py +0 -0
  2. tests/batch/__init__.py +0 -0
  3. tests/batch/test_batch.py +98 -0
  4. tests/core/__init__.py +0 -0
  5. tests/core/test_decorator.py +345 -0
  6. tests/database/__init__.py +0 -0
  7. tests/database/test_db.py +468 -0
  8. tests/database/test_db_search.py +163 -0
  9. tests/exception/__init__.py +0 -0
  10. tests/models/__init__.py +0 -0
  11. tests/models/test_db_config.py +152 -0
  12. tests/monitoring/__init__.py +0 -0
  13. tests/monitoring/test_tracer.py +202 -0
  14. tests/prediction/__init__.py +0 -0
  15. tests/vectorizer/__init__.py +0 -0
  16. vectorwave/__init__.py +13 -0
  17. vectorwave/batch/__init__.py +0 -0
  18. vectorwave/batch/batch.py +68 -0
  19. vectorwave/core/__init__.py +0 -0
  20. vectorwave/core/core.py +0 -0
  21. vectorwave/core/decorator.py +131 -0
  22. vectorwave/database/__init__.py +0 -0
  23. vectorwave/database/db.py +328 -0
  24. vectorwave/database/db_search.py +122 -0
  25. vectorwave/exception/__init__.py +0 -0
  26. vectorwave/exception/exceptions.py +22 -0
  27. vectorwave/models/__init__.py +0 -0
  28. vectorwave/models/db_config.py +92 -0
  29. vectorwave/monitoring/__init__.py +0 -0
  30. vectorwave/monitoring/monitoring.py +0 -0
  31. vectorwave/monitoring/tracer.py +131 -0
  32. vectorwave/prediction/__init__.py +0 -0
  33. vectorwave/prediction/predictor.py +0 -0
  34. vectorwave/vectorizer/__init__.py +0 -0
  35. vectorwave/vectorizer/base.py +12 -0
  36. vectorwave/vectorizer/factory.py +49 -0
  37. vectorwave/vectorizer/huggingface_vectorizer.py +33 -0
  38. vectorwave/vectorizer/openai_vectorizer.py +35 -0
  39. vectorwave-0.1.3.dist-info/METADATA +352 -0
  40. vectorwave-0.1.3.dist-info/RECORD +44 -0
  41. vectorwave-0.1.3.dist-info/WHEEL +5 -0
  42. vectorwave-0.1.3.dist-info/licenses/LICENSE +21 -0
  43. vectorwave-0.1.3.dist-info/licenses/NOTICE +31 -0
  44. vectorwave-0.1.3.dist-info/top_level.txt +2 -0
@@ -0,0 +1,152 @@
1
+ import pytest
2
+ import json
3
+ from unittest.mock import patch, mock_open
4
+ from json import JSONDecodeError
5
+
6
+ # Function to test
7
+ from vectorwave.models.db_config import get_weaviate_settings
8
+
9
+ # --- Mock Data ---
10
+
11
+ # Mock content for a successfully loaded .weaviate_properties file
12
+ MOCK_JSON_DATA = """
13
+ {
14
+ "run_id": {
15
+ "data_type": "TEXT",
16
+ "description": "Test run ID"
17
+ },
18
+ "experiment_id": {
19
+ "data_type": "INT",
20
+ "description": "Identifier for the experiment"
21
+ }
22
+ }
23
+ """
24
+
25
+ # Mock content for a malformed .weaviate_properties file (invalid JSON)
26
+ MOCK_INVALID_JSON = """
27
+ {
28
+ "run_id": {
29
+ "data_type": "TEXT"
30
+ }
31
+ """ # Missing closing '}'
32
+
33
+ # --- Test Cases ---
34
+
35
+ @patch('os.path.exists', return_value=True)
36
+ @patch('builtins.open', new_callable=mock_open, read_data=MOCK_JSON_DATA)
37
+ def test_get_settings_loads_custom_props_success(mock_open_file, mock_exists):
38
+ """
39
+ Case 1: .weaviate_properties file exists and JSON is valid
40
+ - settings.custom_properties should be loaded correctly as a dictionary
41
+ """
42
+ # Arrange
43
+ # Clear the @lru_cache to bypass caching for this test
44
+ get_weaviate_settings.cache_clear()
45
+
46
+ # Act
47
+ settings = get_weaviate_settings()
48
+
49
+ # Assert
50
+ # Verify that the default path (.weaviate_properties) was checked
51
+ mock_exists.assert_called_with(".weaviate_properties")
52
+ # Verify the file was opened in 'r' mode
53
+ mock_open_file.assert_called_with(".weaviate_properties", 'r', encoding='utf-8')
54
+
55
+ assert settings.custom_properties is not None
56
+ assert "run_id" in settings.custom_properties
57
+ assert settings.custom_properties["run_id"]["data_type"] == "TEXT"
58
+ assert settings.custom_properties["run_id"]["description"] == "Test run ID"
59
+ assert "experiment_id" in settings.custom_properties
60
+
61
+
62
+ @patch('os.path.exists', return_value=False)
63
+ def test_get_settings_file_not_found(mock_exists, caplog):
64
+ """
65
+ Case 2: .weaviate_properties file does not exist
66
+ - settings.custom_properties should be None
67
+ - A 'file not found' message should be logged at DEBUG level
68
+ """
69
+ import logging
70
+
71
+ # Arrange
72
+ caplog.set_level(logging.DEBUG) # DEBUG 레벨로 설정 (중요!)
73
+ get_weaviate_settings.cache_clear()
74
+
75
+ # Act
76
+ settings = get_weaviate_settings()
77
+
78
+ # Assert
79
+ mock_exists.assert_called_with(".weaviate_properties")
80
+ assert settings.custom_properties is None
81
+
82
+ # Check if 'file not found' message was logged
83
+ assert "file not found" in caplog.text.lower() or "not found" in caplog.text
84
+
85
+
86
+ @patch('os.path.exists', return_value=True)
87
+ @patch('builtins.open', new_callable=mock_open, read_data=MOCK_INVALID_JSON)
88
+ @patch('json.load', side_effect=JSONDecodeError("Mock JSON Decode Error", "", 0))
89
+ def test_get_settings_invalid_json(mock_json_load, mock_open_file, mock_exists, caplog):
90
+ """
91
+ Case 3: File exists but JSON format is invalid
92
+ - settings.custom_properties should be None
93
+ - A 'Could not parse JSON' warning should be logged
94
+ """
95
+ import logging
96
+
97
+ # Arrange
98
+ caplog.set_level(logging.WARNING)
99
+ get_weaviate_settings.cache_clear()
100
+
101
+ # Act
102
+ settings = get_weaviate_settings()
103
+
104
+ # Assert
105
+ mock_exists.assert_called_once()
106
+ mock_open_file.assert_called_once()
107
+ mock_json_load.assert_called_once() # json.load was called but failed (due to side_effect)
108
+ assert settings.custom_properties is None # Should be None due to parsing failure
109
+
110
+ # Check if 'Could not parse JSON' warning was logged
111
+ assert "Could not parse JSON" in caplog.text
112
+
113
+ # Also check the log level
114
+ warning_logs = [r for r in caplog.records if "parse JSON" in r.message]
115
+ assert len(warning_logs) > 0
116
+ assert warning_logs[0].levelname == "WARNING"
117
+
118
+ @patch('os.path.exists', return_value=True)
119
+ @patch('builtins.open', new_callable=mock_open, read_data=MOCK_JSON_DATA)
120
+ @patch('os.environ.get') # os.environ.get을 모킹합니다
121
+ def test_get_settings_loads_global_custom_values(mock_env_get, mock_open_file, mock_exists):
122
+ """
123
+ Case 4: Test if the value of the "RUN_ID" environment variable is loaded
124
+ into global_custom_values for "run_id" defined in .weaviate_properties
125
+ """
126
+ # 1. Arrange
127
+ # MOCK_JSON_DATA defines "run_id" and "experiment_id".
128
+ # Set os.environ.get("RUN_ID") to return "test-run-123".
129
+ # Set os.environ.get("EXPERIMENT_ID") to return None.
130
+ def mock_env_side_effect(key):
131
+ if key == "RUN_ID":
132
+ return "test-run-123"
133
+ return None
134
+
135
+ mock_env_get.side_effect = mock_env_side_effect
136
+ get_weaviate_settings.cache_clear()
137
+
138
+ # 2. Act
139
+ settings = get_weaviate_settings()
140
+
141
+ # 3. Assert
142
+ # .weaviate_properties should be loaded correctly.
143
+ assert settings.custom_properties is not None
144
+ assert "run_id" in settings.custom_properties
145
+
146
+ # Check if global_custom_values was loaded correctly.
147
+ assert settings.global_custom_values is not None
148
+ assert "run_id" in settings.global_custom_values
149
+ assert settings.global_custom_values["run_id"] == "test-run-123"
150
+
151
+ # "EXPERIMENT_ID" should not be included as os.environ.get returned None.
152
+ assert "experiment_id" not in settings.global_custom_values
File without changes
@@ -0,0 +1,202 @@
1
+ import pytest
2
+ from unittest.mock import MagicMock
3
+ import time
4
+
5
+ from vectorwave.monitoring.tracer import trace_root, trace_span
6
+ from vectorwave.models.db_config import WeaviateSettings
7
+
8
+ # --- Import real functions for cache clearing ---
9
+ from vectorwave.batch.batch import get_batch_manager as real_get_batch_manager
10
+ from vectorwave.database.db import get_cached_client as real_get_cached_client
11
+ from vectorwave.models.db_config import get_weaviate_settings as real_get_settings
12
+
13
+ # Module paths to mock (adjust to your project structure if needed)
14
+ TRACER_MODULE_PATH = "vectorwave.monitoring.tracer"
15
+ BATCH_MODULE_PATH = "vectorwave.batch.batch"
16
+
17
+
18
+ @pytest.fixture
19
+ def mock_tracer_deps(monkeypatch):
20
+ """
21
+ Mocks dependencies for tracer.py (batch, settings).
22
+ """
23
+ # 1. Mock BatchManager
24
+ mock_batch_instance = MagicMock()
25
+ mock_batch_instance.add_object = MagicMock()
26
+ mock_get_batch_manager = MagicMock(return_value=mock_batch_instance)
27
+
28
+ # 2. Mock Settings (including global tags)
29
+ mock_settings = WeaviateSettings(
30
+ COLLECTION_NAME="TestFunctions",
31
+ EXECUTION_COLLECTION_NAME="TestExecutions",
32
+ custom_properties=None, # Not important for this test
33
+ global_custom_values={"run_id": "global-run-abc", "env": "test"}
34
+ )
35
+ mock_get_settings = MagicMock(return_value=mock_settings)
36
+
37
+ mock_client = MagicMock()
38
+ mock_get_client = MagicMock(return_value=mock_client)
39
+
40
+ # --- Patch dependencies for tracer.py ---
41
+ monkeypatch.setattr(f"{TRACER_MODULE_PATH}.get_batch_manager", mock_get_batch_manager)
42
+ monkeypatch.setattr(f"{TRACER_MODULE_PATH}.get_weaviate_settings", mock_get_settings)
43
+
44
+ # Patch dependencies inside batch.py to prevent BatchManager init failure
45
+ monkeypatch.setattr(f"{BATCH_MODULE_PATH}.get_weaviate_client", mock_get_client)
46
+ monkeypatch.setattr(f"{BATCH_MODULE_PATH}.get_weaviate_settings", mock_get_settings)
47
+
48
+ # 5. Clear caches
49
+ real_get_batch_manager.cache_clear()
50
+ real_get_cached_client.cache_clear()
51
+ real_get_settings.cache_clear()
52
+
53
+ return {
54
+ "batch": mock_batch_instance,
55
+ "settings": mock_settings
56
+ }
57
+
58
+
59
+ def test_trace_root_and_span_success(mock_tracer_deps):
60
+ """
61
+ Case 1: Success (Root + Span) - The span should be recorded successfully.
62
+ """
63
+ mock_batch = mock_tracer_deps["batch"]
64
+
65
+ @trace_span
66
+ def my_inner_span(x):
67
+ return f"result: {x}"
68
+
69
+ @trace_root()
70
+ def my_workflow_root():
71
+ return my_inner_span(x=10)
72
+
73
+ # --- Act ---
74
+ result = my_workflow_root()
75
+
76
+ # --- Assert ---
77
+ assert result == "result: 10"
78
+ mock_batch.add_object.assert_called_once()
79
+
80
+ args, kwargs = mock_batch.add_object.call_args
81
+ props = kwargs["properties"]
82
+
83
+ assert kwargs["collection"] == "TestExecutions"
84
+ assert props["status"] == "SUCCESS"
85
+ assert props["function_name"] == "my_inner_span"
86
+ assert props["error_message"] is None
87
+ assert "trace_id" in props
88
+ assert props["run_id"] == "global-run-abc"
89
+ assert props["env"] == "test"
90
+
91
+
92
+ def test_trace_span_failure(mock_tracer_deps):
93
+ """
94
+ Case 2: Failure (Root + Failing Span) - The span should be recorded with an ERROR status.
95
+ """
96
+ mock_batch = mock_tracer_deps["batch"]
97
+
98
+ @trace_span
99
+ def my_failing_span():
100
+ raise ValueError("This is a test error")
101
+
102
+ @trace_root()
103
+ def my_workflow_root_fail():
104
+ my_failing_span()
105
+
106
+ # --- Act & Assert (Exception) ---
107
+ with pytest.raises(ValueError, match="This is a test error"):
108
+ my_workflow_root_fail()
109
+
110
+ # --- Assert (Log) ---
111
+ mock_batch.add_object.assert_called_once()
112
+
113
+ args, kwargs = mock_batch.add_object.call_args
114
+ props = kwargs["properties"]
115
+
116
+ assert props["status"] == "ERROR"
117
+ assert "ValueError: This is a test error" in props["error_message"]
118
+ assert props["function_name"] == "my_failing_span"
119
+ assert props["run_id"] == "global-run-abc"
120
+
121
+
122
+ def test_span_without_root_does_nothing(mock_tracer_deps):
123
+ """
124
+ Case 3: Tracing disabled (Span only) - If there's no Root, nothing should be recorded.
125
+ """
126
+ mock_batch = mock_tracer_deps["batch"]
127
+
128
+ @trace_span
129
+ def my_lonely_span():
130
+ return "lonely_result"
131
+
132
+ # --- Act ---
133
+ result = my_lonely_span()
134
+
135
+ # --- Assert ---
136
+ assert result == "lonely_result"
137
+ mock_batch.add_object.assert_not_called()
138
+
139
+
140
+ def test_span_captures_attributes_and_overrides_globals(mock_tracer_deps):
141
+ """
142
+ Case 4/5: Attribute Capturing and Overriding
143
+ """
144
+ mock_batch = mock_tracer_deps["batch"]
145
+
146
+ class MyObject:
147
+ def __str__(self): return "MyObjectInstance"
148
+
149
+ @trace_span(attributes_to_capture=["team", "priority", "run_id", "user_obj"])
150
+ def my_span_with_attrs(team, priority, run_id, user_obj, other_arg="default"):
151
+ return "captured"
152
+
153
+ @trace_root()
154
+ def my_workflow_root_attrs():
155
+ return my_span_with_attrs(
156
+ team="backend",
157
+ priority=1,
158
+ run_id="override-run-xyz", # <-- This should override "global-run-abc"
159
+ user_obj=MyObject(),
160
+ other_arg="should-be-ignored"
161
+ )
162
+
163
+ # --- Act ---
164
+ my_workflow_root_attrs()
165
+
166
+ # --- Assert ---
167
+ mock_batch.add_object.assert_called_once()
168
+ props = mock_batch.add_object.call_args.kwargs["properties"]
169
+
170
+ assert props["team"] == "backend"
171
+ assert props["priority"] == 1
172
+ assert props["user_obj"] == "MyObjectInstance"
173
+ assert props["run_id"] == "override-run-xyz" # Overridden
174
+ assert props["env"] == "test" # Non-overridden global remains
175
+ assert "other_arg" not in props
176
+
177
+
178
+ def test_root_accepts_custom_trace_id(mock_tracer_deps):
179
+ """
180
+ Bonus: Test case for manually providing a 'trace_id'.
181
+ (This is the test that was fixed)
182
+ """
183
+ mock_batch = mock_tracer_deps["batch"]
184
+
185
+ @trace_span
186
+ def my_inner_span():
187
+ pass
188
+
189
+ @trace_root()
190
+ def my_workflow_root_custom_id(): # <-- ✅ FIXED: Removed 'trace_id' arg
191
+ my_inner_span()
192
+
193
+ # --- Act ---
194
+ # The decorator wrapper still receives 'trace_id' from this call
195
+ my_workflow_root_custom_id(trace_id="my-custom-trace-id-123")
196
+
197
+ # --- Assert ---
198
+ mock_batch.add_object.assert_called_once()
199
+ props = mock_batch.add_object.call_args.kwargs["properties"]
200
+
201
+ # Check if the trace_id was popped and injected correctly
202
+ assert props["trace_id"] == "my-custom-trace-id-123"
File without changes
File without changes
vectorwave/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ from .core.decorator import vectorize
2
+
3
+ from .database.db import initialize_database
4
+ from .database.db_search import search_functions, search_executions
5
+ from .monitoring.tracer import trace_span
6
+
7
+ __all__ = [
8
+ 'vectorize',
9
+ 'initialize_database',
10
+ 'search_functions',
11
+ 'search_executions',
12
+ 'trace_span'
13
+ ]
File without changes
@@ -0,0 +1,68 @@
1
+ import weaviate
2
+ import atexit
3
+ import logging
4
+ from functools import lru_cache
5
+ from typing import Optional, List
6
+ from ..models.db_config import get_weaviate_settings, WeaviateSettings
7
+ from ..database.db import get_weaviate_client
8
+ from ..exception.exceptions import WeaviateConnectionError
9
+
10
+ # Create module-level logger
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class WeaviateBatchManager:
14
+ """
15
+ A singleton class that manages Weaviate batch imports.
16
+ """
17
+
18
+ def __init__(self):
19
+ self._initialized = False
20
+ logger.debug("Initializing WeaviateBatchManager")
21
+ self.client: weaviate.WeaviateClient = None
22
+
23
+ try:
24
+ # (get_weaviate_settings is reused as it is handled by lru_cache)
25
+ self.settings: WeaviateSettings = get_weaviate_settings()
26
+ self.client: weaviate.WeaviateClient = get_weaviate_client(self.settings)
27
+
28
+ if not self.client:
29
+ raise WeaviateConnectionError("Client is None, cannot configure batch.")
30
+
31
+ # self.client.batch.configure(
32
+ # batch_size=20,
33
+ # dynamic=True,
34
+ # timeout_retries=3,
35
+ # )
36
+
37
+ # Register atexit: Automatically calls self.flush() on script exit
38
+ # atexit.register(self.flush)
39
+ self._initialized = True
40
+ logger.info("WeaviateBatchManager initialized successfully")
41
+
42
+ except Exception as e:
43
+ # Prevents VectorWave from stopping the main app upon DB connection failure
44
+ logger.error("Failed to initialize WeaviateBatchManager: %s", e)
45
+
46
+ def add_object(self, collection: str, properties: dict, uuid: str = None, vector: Optional[List[float]] = None):
47
+ """
48
+ Adds an object to the Weaviate batch queue.
49
+ """
50
+ if not self._initialized or not self.client:
51
+ logger.warning("Batch manager not initialized, skipping add_object")
52
+ return
53
+
54
+ try:
55
+ self.client.collections.get(collection).data.insert(
56
+ properties=properties,
57
+ uuid=uuid,
58
+ vector=vector
59
+ )
60
+
61
+ except Exception as e:
62
+ logger.error("Failed to add object to batch (collection '%s'): %s", collection, e)
63
+
64
+
65
+
66
+ @lru_cache(None)
67
+ def get_batch_manager() -> WeaviateBatchManager:
68
+ return WeaviateBatchManager()
File without changes
File without changes
@@ -0,0 +1,131 @@
1
+ # vtm/src/vectorwave/core/decorator.py
2
+
3
+ import logging
4
+ import inspect
5
+ from functools import wraps
6
+
7
+ from weaviate.util import generate_uuid5
8
+
9
+ from ..batch.batch import get_batch_manager
10
+ from ..models.db_config import get_weaviate_settings
11
+ from ..monitoring.tracer import trace_root, trace_span
12
+ from ..vectorizer.factory import get_vectorizer
13
+
14
+ # Create module-level logger
15
+ logger = logging.getLogger(__name__)
16
+
17
+ def vectorize(search_description: str,
18
+ sequence_narrative: str,
19
+ **execution_tags):
20
+ """
21
+ VectorWave Decorator
22
+
23
+ (1) Collects function definitions (static data) once on script load.
24
+ (2) Records function execution (dynamic data) every time the function is called.
25
+ """
26
+
27
+ def decorator(func):
28
+
29
+ func_uuid = None
30
+ valid_execution_tags = {}
31
+ try:
32
+ module_name = func.__module__
33
+ function_name = func.__name__
34
+
35
+ func_identifier = f"{module_name}.{function_name}"
36
+ func_uuid = generate_uuid5(func_identifier)
37
+
38
+ static_properties = {
39
+ "function_name": function_name,
40
+ "module_name": module_name,
41
+ "docstring": inspect.getdoc(func) or "",
42
+ "source_code": inspect.getsource(func),
43
+ "search_description": search_description,
44
+ "sequence_narrative": sequence_narrative
45
+ }
46
+
47
+ batch = get_batch_manager()
48
+ settings = get_weaviate_settings()
49
+
50
+ vectorizer = get_vectorizer()
51
+ vector_to_add = None
52
+
53
+ if vectorizer:
54
+
55
+ try:
56
+ print(f"[VectorWave] Vectorizing '{function_name}' using Python vectorizer...")
57
+ vector_to_add = vectorizer.embed(search_description)
58
+ except Exception as e:
59
+ print(f"Warning: Failed to vectorize '{function_name}' with Python client: {e}")
60
+
61
+ if execution_tags:
62
+ if not settings.custom_properties:
63
+ logger.warning(
64
+ f"Function '{function_name}' provided execution_tags {list(execution_tags.keys())} "
65
+ f"but no .weaviate_properties file was loaded. These tags will be IGNORED."
66
+ )
67
+ else:
68
+ allowed_keys = set(settings.custom_properties.keys())
69
+ for key, value in execution_tags.items():
70
+ if key in allowed_keys:
71
+ valid_execution_tags[key] = value
72
+ else:
73
+ logger.warning(
74
+ "Function '%s' has undefined execution_tag: '%s'. "
75
+ "This tag will be IGNORED. Please add it to your .weaviate_properties file.",
76
+ function_name,
77
+ key
78
+ )
79
+
80
+ batch.add_object(
81
+ collection=settings.COLLECTION_NAME,
82
+ properties=static_properties,
83
+ uuid=func_uuid,
84
+ vector=vector_to_add
85
+ )
86
+
87
+ except Exception as e:
88
+
89
+ logger.error("Error in @vectorize setup for '%s': %s", func.__name__, e)
90
+ @wraps(func)
91
+ def original_func_wrapper(*args, **kwargs):
92
+ return func(*args, **kwargs)
93
+ return original_func_wrapper
94
+
95
+
96
+ # 2a. The *inner* wrapper to be wrapped by @trace_span
97
+ # This function receives all tags including full_kwargs from @trace_span.
98
+ @trace_root()
99
+ @trace_span(attributes_to_capture=['function_uuid', 'team', 'priority', 'run_id'])
100
+ @wraps(func)
101
+ def inner_wrapper(*args, **kwargs):
102
+
103
+ original_kwargs = kwargs.copy()
104
+
105
+ keys_to_remove = list(valid_execution_tags.keys())
106
+ keys_to_remove.append('function_uuid')
107
+
108
+ for key in execution_tags.keys():
109
+ if key not in keys_to_remove:
110
+ keys_to_remove.append(key)
111
+
112
+ for key in keys_to_remove:
113
+ original_kwargs.pop(key, None)
114
+
115
+ return func(*args, **original_kwargs)
116
+
117
+
118
+ @wraps(func)
119
+ def outer_wrapper(*args, **kwargs):
120
+
121
+ full_kwargs = kwargs.copy()
122
+ full_kwargs.update(valid_execution_tags)
123
+ full_kwargs['function_uuid'] = func_uuid
124
+
125
+ # 2. Call the *inner* wrapper with the full_kwargs
126
+ # This call passes through the @trace_root -> @trace_span decorators.
127
+ return inner_wrapper(*args, **full_kwargs)
128
+
129
+ return outer_wrapper
130
+
131
+ return decorator
File without changes