oplnk-python-utils 1.1.7__tar.gz → 1.1.9__tar.gz
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.
- {oplnk_python_utils-1.1.7/oplnk_python_utils.egg-info → oplnk_python_utils-1.1.9}/PKG-INFO +18 -10
- {oplnk_python_utils-1.1.7 → oplnk_python_utils-1.1.9}/README.md +17 -9
- {oplnk_python_utils-1.1.7 → oplnk_python_utils-1.1.9}/oplnk_python_utils/__init__.py +1 -1
- {oplnk_python_utils-1.1.7 → oplnk_python_utils-1.1.9}/oplnk_python_utils/core/parsing.py +90 -11
- {oplnk_python_utils-1.1.7 → oplnk_python_utils-1.1.9}/oplnk_python_utils/query/query_builder.py +344 -38
- {oplnk_python_utils-1.1.7 → oplnk_python_utils-1.1.9/oplnk_python_utils.egg-info}/PKG-INFO +18 -10
- {oplnk_python_utils-1.1.7 → oplnk_python_utils-1.1.9}/setup.py +1 -1
- {oplnk_python_utils-1.1.7 → oplnk_python_utils-1.1.9}/LICENSE +0 -0
- {oplnk_python_utils-1.1.7 → oplnk_python_utils-1.1.9}/MANIFEST.in +0 -0
- {oplnk_python_utils-1.1.7 → oplnk_python_utils-1.1.9}/oplnk_python_utils/cache/__init__.py +0 -0
- {oplnk_python_utils-1.1.7 → oplnk_python_utils-1.1.9}/oplnk_python_utils/cache/cache_utils.py +0 -0
- {oplnk_python_utils-1.1.7 → oplnk_python_utils-1.1.9}/oplnk_python_utils/core/__init__.py +0 -0
- {oplnk_python_utils-1.1.7 → oplnk_python_utils-1.1.9}/oplnk_python_utils/core/data.py +0 -0
- {oplnk_python_utils-1.1.7 → oplnk_python_utils-1.1.9}/oplnk_python_utils/core/serialization.py +0 -0
- {oplnk_python_utils-1.1.7 → oplnk_python_utils-1.1.9}/oplnk_python_utils/query/__init__.py +0 -0
- {oplnk_python_utils-1.1.7 → oplnk_python_utils-1.1.9}/oplnk_python_utils/reports/__init__.py +0 -0
- {oplnk_python_utils-1.1.7 → oplnk_python_utils-1.1.9}/oplnk_python_utils.egg-info/SOURCES.txt +0 -0
- {oplnk_python_utils-1.1.7 → oplnk_python_utils-1.1.9}/oplnk_python_utils.egg-info/dependency_links.txt +0 -0
- {oplnk_python_utils-1.1.7 → oplnk_python_utils-1.1.9}/oplnk_python_utils.egg-info/not-zip-safe +0 -0
- {oplnk_python_utils-1.1.7 → oplnk_python_utils-1.1.9}/oplnk_python_utils.egg-info/requires.txt +0 -0
- {oplnk_python_utils-1.1.7 → oplnk_python_utils-1.1.9}/oplnk_python_utils.egg-info/top_level.txt +0 -0
- {oplnk_python_utils-1.1.7 → oplnk_python_utils-1.1.9}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: oplnk-python-utils
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.9
|
|
4
4
|
Summary: Reusable utilities for caching, data manipulation, parsing, and MongoDB query building
|
|
5
5
|
Home-page: https://github.com/openlinkspa/oplnk-python-utils
|
|
6
6
|
Author: OpenLink SpA
|
|
@@ -109,7 +109,7 @@ merged = merge_dicts(dict1, dict2)
|
|
|
109
109
|
|
|
110
110
|
### Parsing Utilities (`core.parsing`)
|
|
111
111
|
|
|
112
|
-
Functions for parsing query parameters and
|
|
112
|
+
Functions for parsing query parameters, type conversion, and safe field-path handling for MongoDB aggregation.
|
|
113
113
|
|
|
114
114
|
```python
|
|
115
115
|
from oplnk_python_utils.core.parsing import (
|
|
@@ -158,6 +158,14 @@ nested_dict = parse_list_to_dict(["filters", "name", "$eq"], "john")
|
|
|
158
158
|
- **`parse_key_string_to_list(s)`**: Parse bracketed notation strings like `"filters[name][$eq]"`
|
|
159
159
|
- **`parse_list_to_dict(lst, value)`**: Convert key list and value into nested dictionary structure
|
|
160
160
|
|
|
161
|
+
#### Field Path Utilities for Aggregation (NEW)
|
|
162
|
+
|
|
163
|
+
- **`normalize_field_path(field)`**: Normalize paths with array notation for MongoDB aggregation.
|
|
164
|
+
- Example: `"user.addresses[1].street" -> "user.addresses.1.street"`
|
|
165
|
+
- **`validate_field_path(field)`**: Validate field paths for invalid characters before using them in pipelines.
|
|
166
|
+
- **`build_aggregation_field_expression(field)`**: Produce a proper aggregation expression, automatically prefixing with `$` when needed.
|
|
167
|
+
- Example: `"user.addresses[1].street" -> "$user.addresses.1.street"`
|
|
168
|
+
|
|
161
169
|
### Serialization Utilities (`core.serialization`)
|
|
162
170
|
|
|
163
171
|
Functions for converting data types to serializable formats.
|
|
@@ -203,15 +211,15 @@ cache_key = generate_cache_id({
|
|
|
203
211
|
})
|
|
204
212
|
# Automatically normalizes data internally and creates SHA256 hash
|
|
205
213
|
|
|
206
|
-
# Get from cache or execute function and cache result
|
|
207
|
-
|
|
208
|
-
# Some expensive database operation
|
|
209
|
-
return
|
|
214
|
+
# Get from cache or execute a function and cache result (sync fetch_func)
|
|
215
|
+
def fetch_users():
|
|
216
|
+
# Some expensive database operation (sync example)
|
|
217
|
+
return list(db.collection.find({"status": "active"}))
|
|
210
218
|
|
|
211
|
-
result =
|
|
219
|
+
result = get_or_set_cache(
|
|
212
220
|
redis_client=redis,
|
|
213
221
|
cache_key="user_query_abc123",
|
|
214
|
-
fetch_func=
|
|
222
|
+
fetch_func=fetch_users, # zero-arg callable
|
|
215
223
|
ttl=3600, # 1 hour
|
|
216
224
|
serializer=datetime_serializer
|
|
217
225
|
)
|
|
@@ -270,10 +278,10 @@ async def create_user(request: Request):
|
|
|
270
278
|
# Automatically invalidates 'users' tag after creation
|
|
271
279
|
return {"created": True}
|
|
272
280
|
|
|
273
|
-
# Manual cache key building
|
|
281
|
+
# Manual cache key building (build_cache_key is async)
|
|
274
282
|
@app.get("/custom")
|
|
275
283
|
async def custom_endpoint(request: Request):
|
|
276
|
-
cache_key = build_cache_key(request, "myapi", "custom", "read")
|
|
284
|
+
cache_key = await build_cache_key(request, "myapi", "custom", "read")
|
|
277
285
|
# Use cache_key for custom caching logic
|
|
278
286
|
return {"data": "custom"}
|
|
279
287
|
|
|
@@ -67,7 +67,7 @@ merged = merge_dicts(dict1, dict2)
|
|
|
67
67
|
|
|
68
68
|
### Parsing Utilities (`core.parsing`)
|
|
69
69
|
|
|
70
|
-
Functions for parsing query parameters and
|
|
70
|
+
Functions for parsing query parameters, type conversion, and safe field-path handling for MongoDB aggregation.
|
|
71
71
|
|
|
72
72
|
```python
|
|
73
73
|
from oplnk_python_utils.core.parsing import (
|
|
@@ -116,6 +116,14 @@ nested_dict = parse_list_to_dict(["filters", "name", "$eq"], "john")
|
|
|
116
116
|
- **`parse_key_string_to_list(s)`**: Parse bracketed notation strings like `"filters[name][$eq]"`
|
|
117
117
|
- **`parse_list_to_dict(lst, value)`**: Convert key list and value into nested dictionary structure
|
|
118
118
|
|
|
119
|
+
#### Field Path Utilities for Aggregation (NEW)
|
|
120
|
+
|
|
121
|
+
- **`normalize_field_path(field)`**: Normalize paths with array notation for MongoDB aggregation.
|
|
122
|
+
- Example: `"user.addresses[1].street" -> "user.addresses.1.street"`
|
|
123
|
+
- **`validate_field_path(field)`**: Validate field paths for invalid characters before using them in pipelines.
|
|
124
|
+
- **`build_aggregation_field_expression(field)`**: Produce a proper aggregation expression, automatically prefixing with `$` when needed.
|
|
125
|
+
- Example: `"user.addresses[1].street" -> "$user.addresses.1.street"`
|
|
126
|
+
|
|
119
127
|
### Serialization Utilities (`core.serialization`)
|
|
120
128
|
|
|
121
129
|
Functions for converting data types to serializable formats.
|
|
@@ -161,15 +169,15 @@ cache_key = generate_cache_id({
|
|
|
161
169
|
})
|
|
162
170
|
# Automatically normalizes data internally and creates SHA256 hash
|
|
163
171
|
|
|
164
|
-
# Get from cache or execute function and cache result
|
|
165
|
-
|
|
166
|
-
# Some expensive database operation
|
|
167
|
-
return
|
|
172
|
+
# Get from cache or execute a function and cache result (sync fetch_func)
|
|
173
|
+
def fetch_users():
|
|
174
|
+
# Some expensive database operation (sync example)
|
|
175
|
+
return list(db.collection.find({"status": "active"}))
|
|
168
176
|
|
|
169
|
-
result =
|
|
177
|
+
result = get_or_set_cache(
|
|
170
178
|
redis_client=redis,
|
|
171
179
|
cache_key="user_query_abc123",
|
|
172
|
-
fetch_func=
|
|
180
|
+
fetch_func=fetch_users, # zero-arg callable
|
|
173
181
|
ttl=3600, # 1 hour
|
|
174
182
|
serializer=datetime_serializer
|
|
175
183
|
)
|
|
@@ -228,10 +236,10 @@ async def create_user(request: Request):
|
|
|
228
236
|
# Automatically invalidates 'users' tag after creation
|
|
229
237
|
return {"created": True}
|
|
230
238
|
|
|
231
|
-
# Manual cache key building
|
|
239
|
+
# Manual cache key building (build_cache_key is async)
|
|
232
240
|
@app.get("/custom")
|
|
233
241
|
async def custom_endpoint(request: Request):
|
|
234
|
-
cache_key = build_cache_key(request, "myapi", "custom", "read")
|
|
242
|
+
cache_key = await build_cache_key(request, "myapi", "custom", "read")
|
|
235
243
|
# Use cache_key for custom caching logic
|
|
236
244
|
return {"data": "custom"}
|
|
237
245
|
|
|
@@ -11,10 +11,10 @@ from .data import merge_dicts
|
|
|
11
11
|
def infer_type(value: Union[str, List, Any]) -> Any:
|
|
12
12
|
"""
|
|
13
13
|
Infer the correct Python type from a string value.
|
|
14
|
-
|
|
14
|
+
|
|
15
15
|
Args:
|
|
16
16
|
value: The value to convert (string, list, or other)
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
Returns:
|
|
19
19
|
Value converted to appropriate Python type
|
|
20
20
|
"""
|
|
@@ -54,10 +54,10 @@ def infer_type(value: Union[str, List, Any]) -> Any:
|
|
|
54
54
|
def parse_key_string_to_list(s: str) -> List[str]:
|
|
55
55
|
"""
|
|
56
56
|
Parse a string like 'filters[name][$containsi]' into ['filters', 'name', '$containsi'].
|
|
57
|
-
|
|
57
|
+
|
|
58
58
|
Args:
|
|
59
59
|
s: String with bracketed notation
|
|
60
|
-
|
|
60
|
+
|
|
61
61
|
Returns:
|
|
62
62
|
List of parsed key components
|
|
63
63
|
"""
|
|
@@ -69,11 +69,11 @@ def parse_key_string_to_list(s: str) -> List[str]:
|
|
|
69
69
|
def parse_list_to_dict(lst: List[str], value: str) -> Dict[str, Any]:
|
|
70
70
|
"""
|
|
71
71
|
Convert a list of keys and a value into a nested dictionary.
|
|
72
|
-
|
|
72
|
+
|
|
73
73
|
Args:
|
|
74
74
|
lst: List of keys for nested structure
|
|
75
75
|
value: Value to place at the deepest level
|
|
76
|
-
|
|
76
|
+
|
|
77
77
|
Returns:
|
|
78
78
|
Nested dictionary structure
|
|
79
79
|
"""
|
|
@@ -93,16 +93,16 @@ def parse_params_to_dict(params: Union[Dict[str, str], Any]) -> Dict[str, Any]:
|
|
|
93
93
|
"""
|
|
94
94
|
Parse query parameters into a structured dictionary.
|
|
95
95
|
Handles nested parameters like filters[name][$containsi]=john
|
|
96
|
-
|
|
96
|
+
|
|
97
97
|
Args:
|
|
98
98
|
params: Query parameters (FastAPI QueryParams or dict-like object)
|
|
99
|
-
|
|
99
|
+
|
|
100
100
|
Returns:
|
|
101
101
|
Structured dictionary with nested parameters
|
|
102
102
|
"""
|
|
103
103
|
# Handle FastAPI QueryParams or similar objects
|
|
104
|
-
if hasattr(params,
|
|
105
|
-
param_dict = dict(params.items()) if hasattr(params,
|
|
104
|
+
if hasattr(params, "items"):
|
|
105
|
+
param_dict = dict(params.items()) if hasattr(params, "items") else dict(params)
|
|
106
106
|
else:
|
|
107
107
|
param_dict = params
|
|
108
108
|
|
|
@@ -111,4 +111,83 @@ def parse_params_to_dict(params: Union[Dict[str, str], Any]) -> Dict[str, Any]:
|
|
|
111
111
|
args = parse_key_string_to_list(key)
|
|
112
112
|
dictionary = parse_list_to_dict(args, value)
|
|
113
113
|
result = merge_dicts(result, dictionary)
|
|
114
|
-
return result
|
|
114
|
+
return result
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# Field handling utilities for MongoDB aggregation pipelines
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def normalize_field_path(field: str) -> str:
|
|
121
|
+
"""
|
|
122
|
+
Normalize field path for MongoDB aggregation pipeline.
|
|
123
|
+
Handles dot notation and prepares for array notation.
|
|
124
|
+
|
|
125
|
+
Examples:
|
|
126
|
+
"establishment_type.name" -> "establishment_type.name"
|
|
127
|
+
"establishment_type[0].name" -> "establishment_type.0.name"
|
|
128
|
+
"user.addresses[1].street" -> "user.addresses.1.street"
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
field: Field path to normalize
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Normalized field path compatible with MongoDB aggregation
|
|
135
|
+
"""
|
|
136
|
+
# Convert array notation to dot notation for MongoDB
|
|
137
|
+
# establishment_type[0].name -> establishment_type.0.name
|
|
138
|
+
|
|
139
|
+
# Pattern to match array notation like [0], [1], etc.
|
|
140
|
+
array_pattern = r"\[(\d+)\]"
|
|
141
|
+
normalized = re.sub(array_pattern, r".\1", field)
|
|
142
|
+
|
|
143
|
+
return normalized
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def validate_field_path(field: str) -> bool:
|
|
147
|
+
"""
|
|
148
|
+
Validate if a field path is valid for MongoDB aggregation.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
field: Field path to validate
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
bool: True if valid, False otherwise
|
|
155
|
+
"""
|
|
156
|
+
if not field or not isinstance(field, str):
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
# Check for invalid characters
|
|
160
|
+
invalid_chars = ["$", " ", "\t", "\n", "\r"]
|
|
161
|
+
if any(
|
|
162
|
+
char in field
|
|
163
|
+
for char in invalid_chars
|
|
164
|
+
if char != "$" or not field.startswith("$")
|
|
165
|
+
):
|
|
166
|
+
return False
|
|
167
|
+
|
|
168
|
+
return True
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def build_aggregation_field_expression(field: str) -> str:
|
|
172
|
+
"""
|
|
173
|
+
Build proper MongoDB aggregation field expression.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
field: Field name or path
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
str: Properly formatted field expression for aggregation
|
|
180
|
+
|
|
181
|
+
Raises:
|
|
182
|
+
ValueError: If field path is invalid
|
|
183
|
+
"""
|
|
184
|
+
if not validate_field_path(field):
|
|
185
|
+
raise ValueError(f"Invalid field path: {field}")
|
|
186
|
+
|
|
187
|
+
normalized_field = normalize_field_path(field)
|
|
188
|
+
|
|
189
|
+
# If field doesn't start with $, add it for aggregation
|
|
190
|
+
if not normalized_field.startswith("$"):
|
|
191
|
+
return f"${normalized_field}"
|
|
192
|
+
|
|
193
|
+
return normalized_field
|
{oplnk_python_utils-1.1.7 → oplnk_python_utils-1.1.9}/oplnk_python_utils/query/query_builder.py
RENAMED
|
@@ -3,18 +3,18 @@ Query Builder - Generic query building functionality for MongoDB aggregation pip
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import re
|
|
6
|
+
import time
|
|
6
7
|
from copy import deepcopy
|
|
7
|
-
from typing import List, Dict, Any, Optional, Union
|
|
8
|
-
from datetime import datetime
|
|
9
|
-
from bson import ObjectId
|
|
8
|
+
from typing import List, Dict, Any, Optional, Union
|
|
10
9
|
from fastapi import HTTPException, status
|
|
10
|
+
from pprint import pprint
|
|
11
11
|
|
|
12
12
|
# Import core utilities
|
|
13
13
|
from ..core.parsing import (
|
|
14
|
-
infer_type,
|
|
15
|
-
parse_key_string_to_list,
|
|
16
|
-
parse_list_to_dict,
|
|
17
14
|
parse_params_to_dict,
|
|
15
|
+
normalize_field_path,
|
|
16
|
+
validate_field_path,
|
|
17
|
+
build_aggregation_field_expression,
|
|
18
18
|
)
|
|
19
19
|
|
|
20
20
|
|
|
@@ -46,6 +46,7 @@ class QueryBuilder:
|
|
|
46
46
|
cache_client=None,
|
|
47
47
|
cache_key_prefix: str = "",
|
|
48
48
|
clear_cache: bool = False,
|
|
49
|
+
debug: bool = False,
|
|
49
50
|
) -> Dict[str, Any]:
|
|
50
51
|
"""
|
|
51
52
|
Complete query method that handles parameter parsing and query execution.
|
|
@@ -60,8 +61,25 @@ class QueryBuilder:
|
|
|
60
61
|
cache_client: Cache client (Redis)
|
|
61
62
|
cache_key_prefix: Prefix for cache keys
|
|
62
63
|
clear_cache: Whether to clear cache before query
|
|
64
|
+
debug: Whether to enable debug output
|
|
63
65
|
"""
|
|
66
|
+
# Start timing
|
|
67
|
+
start_time = time.time()
|
|
68
|
+
|
|
69
|
+
if debug:
|
|
70
|
+
print("🔵 [DEBUG] Parámetros de entrada:")
|
|
71
|
+
pprint(
|
|
72
|
+
{
|
|
73
|
+
"params": params if params is not None else "None",
|
|
74
|
+
"query": query if query is not None else "None",
|
|
75
|
+
"use_cache": use_cache,
|
|
76
|
+
"cache_key_prefix": cache_key_prefix,
|
|
77
|
+
"clear_cache": clear_cache,
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
|
|
64
81
|
# Parse params if provided, otherwise use query directly
|
|
82
|
+
parsing_start_time = time.time()
|
|
65
83
|
if params is not None:
|
|
66
84
|
parsed_query = parse_params_to_dict(params)
|
|
67
85
|
elif query is not None:
|
|
@@ -73,6 +91,13 @@ class QueryBuilder:
|
|
|
73
91
|
if "clear_cache" in parsed_query:
|
|
74
92
|
clear_cache = parsed_query.pop("clear_cache", False)
|
|
75
93
|
|
|
94
|
+
parsing_time = time.time() - parsing_start_time
|
|
95
|
+
|
|
96
|
+
if debug:
|
|
97
|
+
print("🔵 [DEBUG] Query parseada:")
|
|
98
|
+
pprint(parsed_query)
|
|
99
|
+
print(f"🔵 [DEBUG] Tiempo de parsing: {parsing_time:.3f}s")
|
|
100
|
+
|
|
76
101
|
return self.get_query_response(
|
|
77
102
|
db_collection,
|
|
78
103
|
parsed_query,
|
|
@@ -82,6 +107,9 @@ class QueryBuilder:
|
|
|
82
107
|
cache_key_prefix=cache_key_prefix,
|
|
83
108
|
use_cache=use_cache,
|
|
84
109
|
clear_cache=clear_cache,
|
|
110
|
+
debug=debug,
|
|
111
|
+
start_time=start_time,
|
|
112
|
+
parsing_time=parsing_time,
|
|
85
113
|
)
|
|
86
114
|
|
|
87
115
|
def build_filter_pipeline(self, filters: Dict[str, Any]) -> Dict[str, Any]:
|
|
@@ -421,6 +449,9 @@ class QueryBuilder:
|
|
|
421
449
|
field_name = list(condition.keys())[0]
|
|
422
450
|
field_conditions = condition[field_name]
|
|
423
451
|
|
|
452
|
+
# Normalize field name for dot notation support
|
|
453
|
+
normalized_field_name = normalize_field_path(field_name)
|
|
454
|
+
|
|
424
455
|
if isinstance(field_conditions, dict):
|
|
425
456
|
operator = list(field_conditions.keys())[0]
|
|
426
457
|
value = field_conditions[operator]
|
|
@@ -443,38 +474,68 @@ class QueryBuilder:
|
|
|
443
474
|
# Expand this condition into multiple alternatives
|
|
444
475
|
value_type, str_value = get_value_type_and_string(value)
|
|
445
476
|
expanded_conditions = expand_text_search_condition(
|
|
446
|
-
|
|
477
|
+
normalized_field_name, operator, str_value
|
|
447
478
|
)
|
|
448
479
|
|
|
449
480
|
# Add all expanded conditions to the $or
|
|
450
481
|
or_conditions.extend(expanded_conditions)
|
|
451
482
|
else:
|
|
452
|
-
# Regular condition, process normally
|
|
453
|
-
|
|
483
|
+
# Regular condition, process normally with normalized field
|
|
484
|
+
normalized_condition = {
|
|
485
|
+
normalized_field_name: field_conditions
|
|
486
|
+
}
|
|
487
|
+
sub_pipeline = self.build_filter_pipeline(
|
|
488
|
+
normalized_condition
|
|
489
|
+
)
|
|
454
490
|
or_conditions.append(sub_pipeline)
|
|
455
491
|
else:
|
|
456
|
-
# Regular condition, process normally
|
|
457
|
-
|
|
492
|
+
# Regular condition, process normally with normalized field
|
|
493
|
+
normalized_condition = {
|
|
494
|
+
normalized_field_name: field_conditions
|
|
495
|
+
}
|
|
496
|
+
sub_pipeline = self.build_filter_pipeline(
|
|
497
|
+
normalized_condition
|
|
498
|
+
)
|
|
458
499
|
or_conditions.append(sub_pipeline)
|
|
459
500
|
|
|
460
501
|
pipeline["$or"] = or_conditions
|
|
461
502
|
case "$and":
|
|
462
503
|
and_conditions = []
|
|
463
504
|
for condition in conditions:
|
|
464
|
-
|
|
505
|
+
# Normalize field names in condition for dot notation support
|
|
506
|
+
normalized_condition = {}
|
|
507
|
+
for field_name, field_conditions in condition.items():
|
|
508
|
+
normalized_field_name = normalize_field_path(field_name)
|
|
509
|
+
normalized_condition[normalized_field_name] = (
|
|
510
|
+
field_conditions
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
sub_pipeline = self.build_filter_pipeline(normalized_condition)
|
|
465
514
|
and_conditions.append(sub_pipeline)
|
|
466
515
|
pipeline["$and"] = and_conditions
|
|
467
516
|
case _:
|
|
468
517
|
try:
|
|
469
|
-
|
|
518
|
+
# Normalize field path for dot notation and array notation support
|
|
519
|
+
normalized_field = normalize_field_path(field)
|
|
520
|
+
|
|
521
|
+
# Validate the field path
|
|
522
|
+
if not validate_field_path(normalized_field):
|
|
523
|
+
print(
|
|
524
|
+
f"Warning: Invalid field path '{field}', skipping filter condition"
|
|
525
|
+
)
|
|
526
|
+
continue
|
|
527
|
+
|
|
528
|
+
sub_pipeline = build_sub_pipeline(conditions, normalized_field)
|
|
470
529
|
|
|
471
530
|
if isinstance(sub_pipeline, dict):
|
|
472
531
|
if "_field" in sub_pipeline:
|
|
473
|
-
new_field =
|
|
532
|
+
new_field = (
|
|
533
|
+
f"{normalized_field}.{sub_pipeline['_field']}"
|
|
534
|
+
)
|
|
474
535
|
pipeline[new_field] = sub_pipeline["_conditions"]
|
|
475
536
|
continue
|
|
476
537
|
|
|
477
|
-
pipeline[
|
|
538
|
+
pipeline[normalized_field] = sub_pipeline
|
|
478
539
|
except (ValueError, TypeError) as e:
|
|
479
540
|
# Log the error and skip this filter condition
|
|
480
541
|
print(f"Error processing filter for field '{field}': {e}")
|
|
@@ -483,22 +544,64 @@ class QueryBuilder:
|
|
|
483
544
|
return pipeline
|
|
484
545
|
|
|
485
546
|
def build_sort_pipeline(self, sort_params: List[str]) -> Dict[str, Any]:
|
|
486
|
-
"""
|
|
547
|
+
"""
|
|
548
|
+
Build sort pipeline from sort parameters with support for dot notation and array notation.
|
|
549
|
+
"""
|
|
487
550
|
sort_pipeline = {}
|
|
488
551
|
for item in sort_params:
|
|
489
|
-
|
|
490
|
-
|
|
552
|
+
try:
|
|
553
|
+
field, order = item.split(":")
|
|
554
|
+
normalized_field = normalize_field_path(field)
|
|
555
|
+
|
|
556
|
+
if validate_field_path(normalized_field):
|
|
557
|
+
sort_pipeline[normalized_field] = 1 if order == "asc" else -1
|
|
558
|
+
else:
|
|
559
|
+
print(f"Warning: Invalid field path '{field}', skipping in sort")
|
|
560
|
+
except ValueError as e:
|
|
561
|
+
print(
|
|
562
|
+
f"Warning: Invalid sort parameter '{item}', expected format 'field:order': {e}"
|
|
563
|
+
)
|
|
564
|
+
except Exception as e:
|
|
565
|
+
print(f"Warning: Error processing sort parameter '{item}': {e}")
|
|
566
|
+
|
|
491
567
|
return {"$sort": sort_pipeline}
|
|
492
568
|
|
|
493
569
|
def build_fields_pipeline(
|
|
494
570
|
self, fields_params: List[str], populate_params: Optional[List] = None
|
|
495
571
|
) -> Dict[str, Any]:
|
|
496
|
-
"""
|
|
497
|
-
|
|
572
|
+
"""
|
|
573
|
+
Build fields projection pipeline with support for dot notation and array notation.
|
|
574
|
+
"""
|
|
575
|
+
fields_pipeline = {}
|
|
576
|
+
|
|
577
|
+
# Process fields_params with normalization
|
|
578
|
+
for field in fields_params:
|
|
579
|
+
try:
|
|
580
|
+
normalized_field = normalize_field_path(field)
|
|
581
|
+
if validate_field_path(normalized_field):
|
|
582
|
+
fields_pipeline[normalized_field] = 1
|
|
583
|
+
else:
|
|
584
|
+
print(
|
|
585
|
+
f"Warning: Invalid field path '{field}', skipping in projection"
|
|
586
|
+
)
|
|
587
|
+
except Exception as e:
|
|
588
|
+
print(f"Warning: Error processing field '{field}' in projection: {e}")
|
|
498
589
|
|
|
590
|
+
# Process populate_params if provided
|
|
499
591
|
if populate_params:
|
|
500
592
|
for field in populate_params:
|
|
501
|
-
|
|
593
|
+
try:
|
|
594
|
+
normalized_field = normalize_field_path(field)
|
|
595
|
+
if validate_field_path(normalized_field):
|
|
596
|
+
fields_pipeline[normalized_field] = 1
|
|
597
|
+
else:
|
|
598
|
+
print(
|
|
599
|
+
f"Warning: Invalid populate field path '{field}', skipping in projection"
|
|
600
|
+
)
|
|
601
|
+
except Exception as e:
|
|
602
|
+
print(
|
|
603
|
+
f"Warning: Error processing populate field '{field}' in projection: {e}"
|
|
604
|
+
)
|
|
502
605
|
|
|
503
606
|
return {"$project": fields_pipeline}
|
|
504
607
|
|
|
@@ -508,14 +611,84 @@ class QueryBuilder:
|
|
|
508
611
|
include_project: bool = True,
|
|
509
612
|
pipeline: Optional[List] = None,
|
|
510
613
|
) -> List[Dict[str, Any]]:
|
|
511
|
-
"""
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
614
|
+
"""
|
|
615
|
+
Build distinct pipeline with support for dot notation and array notation.
|
|
616
|
+
"""
|
|
617
|
+
try:
|
|
618
|
+
# Build group_id with proper field expressions
|
|
619
|
+
group_id = {}
|
|
620
|
+
project_fields = {}
|
|
621
|
+
|
|
622
|
+
for field in distinct_params:
|
|
623
|
+
try:
|
|
624
|
+
# Use utility function to build proper aggregation expression
|
|
625
|
+
field_expression = build_aggregation_field_expression(field)
|
|
626
|
+
|
|
627
|
+
print(f"🔵 [DEBUG] Field: {field}")
|
|
628
|
+
print(f"🔵 [DEBUG] Field expression: {field_expression}")
|
|
629
|
+
|
|
630
|
+
# For MongoDB $group stage, we need to use a safe key name (no dots)
|
|
631
|
+
# We'll use the original field name as key, but replace dots with underscores for the group
|
|
632
|
+
safe_key = (
|
|
633
|
+
field.replace(".", "_").replace("[", "_").replace("]", "_")
|
|
634
|
+
)
|
|
635
|
+
group_id[safe_key] = field_expression
|
|
636
|
+
|
|
637
|
+
print(f"🔵 [DEBUG] Safe key: {safe_key}")
|
|
638
|
+
print(f"🔵 [DEBUG] Group ID entry: {safe_key} = {field_expression}")
|
|
639
|
+
|
|
640
|
+
# Build nested structure for the projection to maintain original field structure
|
|
641
|
+
parts = field.split(".")
|
|
642
|
+
current = project_fields
|
|
643
|
+
for i, part in enumerate(parts[:-1]):
|
|
644
|
+
if part not in current:
|
|
645
|
+
current[part] = {}
|
|
646
|
+
current = current[part]
|
|
647
|
+
|
|
648
|
+
# Set the final value
|
|
649
|
+
current[parts[-1]] = f"$_id.{safe_key}"
|
|
650
|
+
|
|
651
|
+
print(
|
|
652
|
+
f"🔵 [DEBUG] Project fields after processing {field}: {project_fields}"
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
# Debug: Check if field exists in pipeline before distinct
|
|
656
|
+
print(
|
|
657
|
+
f"🔵 [DEBUG] Pipeline before distinct contains establishment_type lookup: {any('establishment_type' in str(stage) for stage in pipeline) if pipeline else 'No pipeline provided'}"
|
|
658
|
+
)
|
|
659
|
+
print(
|
|
660
|
+
f"🔵 [DEBUG] Available relations: {[rel[1] for rel in self.relations]}"
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
except ValueError as e:
|
|
664
|
+
# Log error and skip invalid field
|
|
665
|
+
print(f"Warning: Skipping invalid field '{field}' in distinct: {e}")
|
|
666
|
+
continue
|
|
667
|
+
|
|
668
|
+
if not group_id:
|
|
669
|
+
raise ValueError("No valid fields provided for distinct operation")
|
|
670
|
+
|
|
671
|
+
group_stage = {"$group": {"_id": group_id}}
|
|
672
|
+
|
|
673
|
+
if include_project:
|
|
674
|
+
# Use $project to create the nested structure
|
|
675
|
+
project_stage = {"$project": project_fields}
|
|
676
|
+
return [group_stage, project_stage]
|
|
677
|
+
else:
|
|
678
|
+
return [group_stage]
|
|
679
|
+
|
|
680
|
+
except Exception as e:
|
|
681
|
+
# Fallback to original behavior for backwards compatibility
|
|
682
|
+
print(
|
|
683
|
+
f"Warning: Error in enhanced distinct pipeline, falling back to original: {e}"
|
|
684
|
+
)
|
|
685
|
+
group_id = {field: f"${field}" for field in distinct_params}
|
|
686
|
+
group_stage = {"$group": {"_id": group_id}}
|
|
687
|
+
if include_project:
|
|
688
|
+
replace_root_stage = {"$replaceRoot": {"newRoot": "$_id"}}
|
|
689
|
+
return [group_stage, replace_root_stage]
|
|
690
|
+
else:
|
|
691
|
+
return [group_stage]
|
|
519
692
|
|
|
520
693
|
def build_pagination_pipeline(
|
|
521
694
|
self, pagination_params: Dict[str, Any], pipeline: Optional[List] = None
|
|
@@ -847,15 +1020,47 @@ class QueryBuilder:
|
|
|
847
1020
|
|
|
848
1021
|
if "filters" in query:
|
|
849
1022
|
if not pipeline:
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
1023
|
+
# Extract potential relation field names from filters, supporting
|
|
1024
|
+
# logical operators ($or/$and) and dot-notation (e.g., "company.name").
|
|
1025
|
+
def extract_relation_candidates(filters_dict):
|
|
1026
|
+
relation_names = set()
|
|
1027
|
+
|
|
1028
|
+
def walk(node):
|
|
1029
|
+
if isinstance(node, dict):
|
|
1030
|
+
for k, v in node.items():
|
|
1031
|
+
if k in ("$or", "$and"):
|
|
1032
|
+
# Value can be a list of conditions or a nested dict
|
|
1033
|
+
if isinstance(v, list):
|
|
1034
|
+
for item in v:
|
|
1035
|
+
walk(item)
|
|
1036
|
+
else:
|
|
1037
|
+
walk(v)
|
|
1038
|
+
else:
|
|
1039
|
+
# Field key: take root before dot and strip _id/_ids
|
|
1040
|
+
if (
|
|
1041
|
+
isinstance(k, str)
|
|
1042
|
+
and k
|
|
1043
|
+
and not k.startswith("$")
|
|
1044
|
+
):
|
|
1045
|
+
root = k.split(".")[0]
|
|
1046
|
+
root = re.sub(r"(_ids|_id)$", "", root)
|
|
1047
|
+
if root:
|
|
1048
|
+
relation_names.add(root)
|
|
1049
|
+
# Continue walking nested structures like
|
|
1050
|
+
# {"company": {"name": {"$containsi": "x"}}}
|
|
1051
|
+
walk(v)
|
|
1052
|
+
elif isinstance(node, list):
|
|
1053
|
+
for item in node:
|
|
1054
|
+
walk(item)
|
|
1055
|
+
# Primitives are ignored
|
|
1056
|
+
|
|
1057
|
+
walk(filters_dict)
|
|
1058
|
+
return list(relation_names)
|
|
1059
|
+
|
|
1060
|
+
relation_candidates = extract_relation_candidates(query["filters"])
|
|
1061
|
+
if relation_candidates:
|
|
857
1062
|
populate_from_filters = self.get_count_lookup_relations(
|
|
858
|
-
|
|
1063
|
+
relation_candidates
|
|
859
1064
|
)
|
|
860
1065
|
pipeline.extend(populate_from_filters)
|
|
861
1066
|
|
|
@@ -883,6 +1088,9 @@ class QueryBuilder:
|
|
|
883
1088
|
cache_key_prefix: str = "",
|
|
884
1089
|
use_cache: bool = False,
|
|
885
1090
|
clear_cache: bool = False,
|
|
1091
|
+
debug: bool = False,
|
|
1092
|
+
start_time: float = 0,
|
|
1093
|
+
parsing_time: float = 0,
|
|
886
1094
|
) -> Dict[str, Any]:
|
|
887
1095
|
"""
|
|
888
1096
|
Get query response with optional caching support
|
|
@@ -896,16 +1104,64 @@ class QueryBuilder:
|
|
|
896
1104
|
cache_key_prefix: Prefix for cache keys
|
|
897
1105
|
use_cache: Whether to use caching
|
|
898
1106
|
clear_cache: Whether to clear cache before query
|
|
1107
|
+
debug: Whether to enable debug output
|
|
1108
|
+
start_time: Start time of the query
|
|
1109
|
+
parsing_time: Time spent parsing parameters
|
|
899
1110
|
"""
|
|
900
1111
|
try:
|
|
1112
|
+
# Build pipelines
|
|
1113
|
+
pipeline_build_start = time.time()
|
|
901
1114
|
base_pipeline = self.make_aggregation_query(query, pipeline)
|
|
902
1115
|
count_pipeline = self.make_count_aggregation_query(query, pipeline_count)
|
|
1116
|
+
pipeline_build_time = time.time() - pipeline_build_start
|
|
1117
|
+
|
|
1118
|
+
if debug:
|
|
1119
|
+
print("🔵 [DEBUG] Pipeline de agregación construido:")
|
|
1120
|
+
pprint(base_pipeline)
|
|
1121
|
+
print("🔵 [DEBUG] Pipeline de count construido:")
|
|
1122
|
+
pprint(count_pipeline)
|
|
1123
|
+
|
|
1124
|
+
# Debug filters
|
|
1125
|
+
if "filters" in query:
|
|
1126
|
+
filter_pipeline = self.build_filter_pipeline(query["filters"])
|
|
1127
|
+
print("🔵 [DEBUG] Filtros aplicados:")
|
|
1128
|
+
pprint(filter_pipeline)
|
|
1129
|
+
|
|
1130
|
+
# Debug special operations
|
|
1131
|
+
if "populate" in query:
|
|
1132
|
+
print("🔵 [DEBUG] Relaciones a popular:")
|
|
1133
|
+
pprint(query["populate"])
|
|
1134
|
+
|
|
1135
|
+
if "distinct" in query:
|
|
1136
|
+
print("🔵 [DEBUG] Campos distinct:")
|
|
1137
|
+
pprint(query["distinct"])
|
|
1138
|
+
|
|
1139
|
+
if "sort" in query:
|
|
1140
|
+
print("🔵 [DEBUG] Ordenamiento aplicado:")
|
|
1141
|
+
pprint(query["sort"])
|
|
1142
|
+
|
|
1143
|
+
if "pagination" in query:
|
|
1144
|
+
print("🔵 [DEBUG] Paginación aplicada:")
|
|
1145
|
+
pprint(query["pagination"])
|
|
1146
|
+
|
|
1147
|
+
# Start MongoDB query timing
|
|
1148
|
+
mongodb_start_time = time.time()
|
|
903
1149
|
|
|
904
1150
|
# Get data
|
|
905
1151
|
data_result = list(db_collection.aggregate(base_pipeline))
|
|
906
1152
|
|
|
907
1153
|
# Get count
|
|
908
1154
|
if use_cache and cache_client:
|
|
1155
|
+
if debug:
|
|
1156
|
+
print("🔵 [DEBUG] Configuración de cache:")
|
|
1157
|
+
pprint(
|
|
1158
|
+
{
|
|
1159
|
+
"cache_key_prefix": cache_key_prefix,
|
|
1160
|
+
"clear_cache": clear_cache,
|
|
1161
|
+
"use_cache": use_cache,
|
|
1162
|
+
}
|
|
1163
|
+
)
|
|
1164
|
+
|
|
909
1165
|
# Import cache utilities
|
|
910
1166
|
from ..cache import (
|
|
911
1167
|
generate_cache_id,
|
|
@@ -919,6 +1175,10 @@ class QueryBuilder:
|
|
|
919
1175
|
query_hash = generate_cache_id(query_for_hash)
|
|
920
1176
|
count_key = f"{cache_key_prefix}:query:count:{query_hash}"
|
|
921
1177
|
|
|
1178
|
+
if debug:
|
|
1179
|
+
print("🔵 [DEBUG] Cache key generada:")
|
|
1180
|
+
print(f"count_key: {count_key}")
|
|
1181
|
+
|
|
922
1182
|
# Get or set meta from cache
|
|
923
1183
|
meta_result = get_or_set_cache(
|
|
924
1184
|
cache_client,
|
|
@@ -935,8 +1195,54 @@ class QueryBuilder:
|
|
|
935
1195
|
else:
|
|
936
1196
|
meta_result = list(db_collection.aggregate(count_pipeline))
|
|
937
1197
|
|
|
938
|
-
|
|
1198
|
+
# End MongoDB query timing
|
|
1199
|
+
mongodb_time = time.time() - mongodb_start_time
|
|
1200
|
+
|
|
1201
|
+
if debug:
|
|
1202
|
+
print("🔵 [DEBUG] Resultado de query de datos:")
|
|
1203
|
+
print(f"Registros encontrados: {len(data_result)}")
|
|
1204
|
+
if data_result:
|
|
1205
|
+
print("Muestra del primer registro:")
|
|
1206
|
+
pprint(data_result[0])
|
|
1207
|
+
if len(data_result) > 1:
|
|
1208
|
+
print(f"... y {len(data_result) - 1} registros más")
|
|
1209
|
+
else:
|
|
1210
|
+
print("Sin datos encontrados")
|
|
1211
|
+
|
|
1212
|
+
print("🔵 [DEBUG] Resultado de count:")
|
|
1213
|
+
pprint(meta_result)
|
|
1214
|
+
|
|
1215
|
+
result = self.sanitize_result(query, data_result, meta_result)
|
|
1216
|
+
|
|
1217
|
+
if debug:
|
|
1218
|
+
# Calculate total time
|
|
1219
|
+
total_time = time.time() - start_time
|
|
1220
|
+
|
|
1221
|
+
print("🔵 [DEBUG] Respuesta final sanitizada:")
|
|
1222
|
+
preview_data = result.get("data", [])
|
|
1223
|
+
data_preview = {
|
|
1224
|
+
"data_count": len(preview_data),
|
|
1225
|
+
"first_record": preview_data[0] if preview_data else None,
|
|
1226
|
+
"remaining_records": (
|
|
1227
|
+
f"... y {len(preview_data) - 1} más"
|
|
1228
|
+
if len(preview_data) > 1
|
|
1229
|
+
else "ninguno más"
|
|
1230
|
+
),
|
|
1231
|
+
"meta": result.get("meta", {}),
|
|
1232
|
+
"has_pagination": "pagination" in query,
|
|
1233
|
+
}
|
|
1234
|
+
pprint(data_preview)
|
|
1235
|
+
|
|
1236
|
+
print("🔵 [DEBUG] ⏱️ MÉTRICAS DE PERFORMANCE:")
|
|
1237
|
+
print(f" 📊 Tiempo de parsing/pre-consulta: {parsing_time:.3f}s")
|
|
1238
|
+
print(f" 🗄️ Tiempo real de consulta MongoDB: {mongodb_time:.3f}s")
|
|
1239
|
+
print(f" 🚀 Tiempo total de procesamiento: {total_time:.3f}s")
|
|
1240
|
+
print("=" * 60)
|
|
1241
|
+
|
|
1242
|
+
return result
|
|
939
1243
|
except Exception as e:
|
|
1244
|
+
if debug:
|
|
1245
|
+
print(f"🔴 [DEBUG] Error en get_query_response: {str(e)}")
|
|
940
1246
|
raise HTTPException(
|
|
941
1247
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
942
1248
|
detail=str(e),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: oplnk-python-utils
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.9
|
|
4
4
|
Summary: Reusable utilities for caching, data manipulation, parsing, and MongoDB query building
|
|
5
5
|
Home-page: https://github.com/openlinkspa/oplnk-python-utils
|
|
6
6
|
Author: OpenLink SpA
|
|
@@ -109,7 +109,7 @@ merged = merge_dicts(dict1, dict2)
|
|
|
109
109
|
|
|
110
110
|
### Parsing Utilities (`core.parsing`)
|
|
111
111
|
|
|
112
|
-
Functions for parsing query parameters and
|
|
112
|
+
Functions for parsing query parameters, type conversion, and safe field-path handling for MongoDB aggregation.
|
|
113
113
|
|
|
114
114
|
```python
|
|
115
115
|
from oplnk_python_utils.core.parsing import (
|
|
@@ -158,6 +158,14 @@ nested_dict = parse_list_to_dict(["filters", "name", "$eq"], "john")
|
|
|
158
158
|
- **`parse_key_string_to_list(s)`**: Parse bracketed notation strings like `"filters[name][$eq]"`
|
|
159
159
|
- **`parse_list_to_dict(lst, value)`**: Convert key list and value into nested dictionary structure
|
|
160
160
|
|
|
161
|
+
#### Field Path Utilities for Aggregation (NEW)
|
|
162
|
+
|
|
163
|
+
- **`normalize_field_path(field)`**: Normalize paths with array notation for MongoDB aggregation.
|
|
164
|
+
- Example: `"user.addresses[1].street" -> "user.addresses.1.street"`
|
|
165
|
+
- **`validate_field_path(field)`**: Validate field paths for invalid characters before using them in pipelines.
|
|
166
|
+
- **`build_aggregation_field_expression(field)`**: Produce a proper aggregation expression, automatically prefixing with `$` when needed.
|
|
167
|
+
- Example: `"user.addresses[1].street" -> "$user.addresses.1.street"`
|
|
168
|
+
|
|
161
169
|
### Serialization Utilities (`core.serialization`)
|
|
162
170
|
|
|
163
171
|
Functions for converting data types to serializable formats.
|
|
@@ -203,15 +211,15 @@ cache_key = generate_cache_id({
|
|
|
203
211
|
})
|
|
204
212
|
# Automatically normalizes data internally and creates SHA256 hash
|
|
205
213
|
|
|
206
|
-
# Get from cache or execute function and cache result
|
|
207
|
-
|
|
208
|
-
# Some expensive database operation
|
|
209
|
-
return
|
|
214
|
+
# Get from cache or execute a function and cache result (sync fetch_func)
|
|
215
|
+
def fetch_users():
|
|
216
|
+
# Some expensive database operation (sync example)
|
|
217
|
+
return list(db.collection.find({"status": "active"}))
|
|
210
218
|
|
|
211
|
-
result =
|
|
219
|
+
result = get_or_set_cache(
|
|
212
220
|
redis_client=redis,
|
|
213
221
|
cache_key="user_query_abc123",
|
|
214
|
-
fetch_func=
|
|
222
|
+
fetch_func=fetch_users, # zero-arg callable
|
|
215
223
|
ttl=3600, # 1 hour
|
|
216
224
|
serializer=datetime_serializer
|
|
217
225
|
)
|
|
@@ -270,10 +278,10 @@ async def create_user(request: Request):
|
|
|
270
278
|
# Automatically invalidates 'users' tag after creation
|
|
271
279
|
return {"created": True}
|
|
272
280
|
|
|
273
|
-
# Manual cache key building
|
|
281
|
+
# Manual cache key building (build_cache_key is async)
|
|
274
282
|
@app.get("/custom")
|
|
275
283
|
async def custom_endpoint(request: Request):
|
|
276
|
-
cache_key = build_cache_key(request, "myapi", "custom", "read")
|
|
284
|
+
cache_key = await build_cache_key(request, "myapi", "custom", "read")
|
|
277
285
|
# Use cache_key for custom caching logic
|
|
278
286
|
return {"data": "custom"}
|
|
279
287
|
|
|
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="oplnk-python-utils",
|
|
5
|
-
version="1.1.
|
|
5
|
+
version="1.1.9",
|
|
6
6
|
author="OpenLink SpA",
|
|
7
7
|
author_email="contacto@openlink.cl",
|
|
8
8
|
description="Reusable utilities for caching, data manipulation, parsing, and MongoDB query building",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{oplnk_python_utils-1.1.7 → oplnk_python_utils-1.1.9}/oplnk_python_utils/cache/cache_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{oplnk_python_utils-1.1.7 → oplnk_python_utils-1.1.9}/oplnk_python_utils/core/serialization.py
RENAMED
|
File without changes
|
|
File without changes
|
{oplnk_python_utils-1.1.7 → oplnk_python_utils-1.1.9}/oplnk_python_utils/reports/__init__.py
RENAMED
|
File without changes
|
{oplnk_python_utils-1.1.7 → oplnk_python_utils-1.1.9}/oplnk_python_utils.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{oplnk_python_utils-1.1.7 → oplnk_python_utils-1.1.9}/oplnk_python_utils.egg-info/not-zip-safe
RENAMED
|
File without changes
|
{oplnk_python_utils-1.1.7 → oplnk_python_utils-1.1.9}/oplnk_python_utils.egg-info/requires.txt
RENAMED
|
File without changes
|
{oplnk_python_utils-1.1.7 → oplnk_python_utils-1.1.9}/oplnk_python_utils.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|