oplnk-python-utils 1.2.6__py3-none-any.whl → 1.3.0__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.
- oplnk_python_utils/__init__.py +1 -1
- oplnk_python_utils/cache/cache_utils.py +196 -27
- oplnk_python_utils/query/query_builder.py +90 -20
- {oplnk_python_utils-1.2.6.dist-info → oplnk_python_utils-1.3.0.dist-info}/METADATA +1 -1
- {oplnk_python_utils-1.2.6.dist-info → oplnk_python_utils-1.3.0.dist-info}/RECORD +8 -8
- {oplnk_python_utils-1.2.6.dist-info → oplnk_python_utils-1.3.0.dist-info}/WHEEL +0 -0
- {oplnk_python_utils-1.2.6.dist-info → oplnk_python_utils-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {oplnk_python_utils-1.2.6.dist-info → oplnk_python_utils-1.3.0.dist-info}/top_level.txt +0 -0
oplnk_python_utils/__init__.py
CHANGED
|
@@ -4,16 +4,25 @@ Cache utilities - Generic caching functions that can be used across projects
|
|
|
4
4
|
|
|
5
5
|
import hashlib
|
|
6
6
|
import json
|
|
7
|
+
import logging
|
|
8
|
+
import time
|
|
9
|
+
import asyncio
|
|
7
10
|
from typing import Callable, Any, Optional, Union
|
|
8
11
|
from fastapi.encoders import jsonable_encoder
|
|
9
12
|
from bson.objectid import ObjectId
|
|
10
13
|
from fastapi import Request
|
|
11
14
|
import functools
|
|
15
|
+
from redis.exceptions import (
|
|
16
|
+
ConnectionError as RedisConnectionError,
|
|
17
|
+
TimeoutError as RedisTimeoutError,
|
|
18
|
+
)
|
|
12
19
|
|
|
13
20
|
# Import core utilities
|
|
14
21
|
from ..core.data import normalize
|
|
15
22
|
from ..core.serialization import datetime_serializer
|
|
16
23
|
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
17
26
|
|
|
18
27
|
def generate_cache_id(params: dict) -> str:
|
|
19
28
|
"""
|
|
@@ -39,6 +48,8 @@ def get_or_set_cache(
|
|
|
39
48
|
clear_cache: Union[str, bool] = False,
|
|
40
49
|
ttl: Optional[int] = None,
|
|
41
50
|
serializer: Optional[Callable] = None,
|
|
51
|
+
max_retries: int = 3,
|
|
52
|
+
retry_delay: float = 0.1,
|
|
42
53
|
) -> Any:
|
|
43
54
|
"""
|
|
44
55
|
Retrieve data from cache if available, otherwise fetch, cache, and return it.
|
|
@@ -51,24 +62,45 @@ def get_or_set_cache(
|
|
|
51
62
|
clear_cache: Pattern to clear or boolean to clear specific key
|
|
52
63
|
ttl: Time to live in seconds
|
|
53
64
|
serializer: Function to serialize data before caching
|
|
65
|
+
max_retries: Maximum number of retry attempts for Redis operations
|
|
66
|
+
retry_delay: Delay between retry attempts in seconds
|
|
54
67
|
|
|
55
68
|
Returns:
|
|
56
69
|
Cached or fetched data
|
|
57
70
|
"""
|
|
71
|
+
# Intentar limpiar cache si se solicita
|
|
58
72
|
if clear_cache:
|
|
59
73
|
invalidate_cache_keys(
|
|
60
74
|
redis_client, clear_cache if isinstance(clear_cache, str) else cache_key
|
|
61
75
|
)
|
|
62
76
|
|
|
63
|
-
|
|
64
|
-
|
|
77
|
+
# Intentar obtener del cache
|
|
78
|
+
cached_data = _execute_redis_op(
|
|
79
|
+
lambda: redis_client.get(cache_key) if redis_client.exists(cache_key) else None,
|
|
80
|
+
max_retries=max_retries,
|
|
81
|
+
retry_delay=retry_delay,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if cached_data is not None:
|
|
85
|
+
try:
|
|
86
|
+
return json.loads(cached_data)
|
|
87
|
+
except (json.JSONDecodeError, TypeError) as e:
|
|
88
|
+
logger.warning(f"Failed to decode cached data for key {cache_key}: {e}")
|
|
65
89
|
|
|
90
|
+
# Cache miss o error: obtener datos frescos
|
|
66
91
|
data = fetch_func()
|
|
67
92
|
if serializer:
|
|
68
93
|
data = serializer(data)
|
|
69
94
|
|
|
70
95
|
encoded_data = jsonable_encoder(data, custom_encoder={ObjectId: str})
|
|
71
|
-
|
|
96
|
+
|
|
97
|
+
# Intentar guardar en cache (falla silenciosamente si Redis no está disponible)
|
|
98
|
+
_execute_redis_op(
|
|
99
|
+
lambda: redis_client.set(cache_key, json.dumps(encoded_data), ex=ttl),
|
|
100
|
+
max_retries=max_retries,
|
|
101
|
+
retry_delay=retry_delay,
|
|
102
|
+
)
|
|
103
|
+
|
|
72
104
|
return encoded_data
|
|
73
105
|
|
|
74
106
|
|
|
@@ -81,21 +113,27 @@ def invalidate_cache_keys(redis_client, pattern: str) -> int:
|
|
|
81
113
|
pattern: Pattern to match keys for deletion
|
|
82
114
|
|
|
83
115
|
Returns:
|
|
84
|
-
Number of keys deleted
|
|
116
|
+
Number of keys deleted (0 if Redis is unavailable)
|
|
85
117
|
"""
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
118
|
+
try:
|
|
119
|
+
cursor = 0
|
|
120
|
+
batch_size = 1000
|
|
121
|
+
total_deleted = 0
|
|
122
|
+
|
|
123
|
+
while True:
|
|
124
|
+
cursor, keys = redis_client.scan(
|
|
125
|
+
cursor=cursor, match=pattern, count=batch_size
|
|
126
|
+
)
|
|
127
|
+
if keys:
|
|
128
|
+
redis_client.unlink(*keys)
|
|
129
|
+
total_deleted += len(keys)
|
|
130
|
+
if cursor == 0:
|
|
131
|
+
break
|
|
97
132
|
|
|
98
|
-
|
|
133
|
+
return total_deleted
|
|
134
|
+
except (RedisConnectionError, RedisTimeoutError) as e:
|
|
135
|
+
logger.warning(f"Redis connection error during cache invalidation: {e}")
|
|
136
|
+
return 0
|
|
99
137
|
|
|
100
138
|
|
|
101
139
|
async def build_cache_key(
|
|
@@ -157,6 +195,71 @@ async def build_cache_key(
|
|
|
157
195
|
return f"{api_name}:{collection}:{endpoint_type}:{hashed}"
|
|
158
196
|
|
|
159
197
|
|
|
198
|
+
def _is_async_client(redis_client) -> bool:
|
|
199
|
+
"""
|
|
200
|
+
Detecta si el cliente Redis es asíncrono.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
redis_client: Cliente Redis a verificar
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
True si es asíncrono, False si es síncrono
|
|
207
|
+
"""
|
|
208
|
+
return (
|
|
209
|
+
hasattr(redis_client, "__aenter__") or "async" in type(redis_client).__module__
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _execute_redis_op(
|
|
214
|
+
operation: Callable, max_retries: int = 3, retry_delay: float = 0.1
|
|
215
|
+
) -> Any:
|
|
216
|
+
"""
|
|
217
|
+
Ejecuta operación Redis síncrona con reintentos y degradación elegante.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
operation: Función que ejecuta la operación Redis
|
|
221
|
+
max_retries: Número máximo de reintentos
|
|
222
|
+
retry_delay: Tiempo de espera entre reintentos en segundos
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Resultado de la operación o None si falla después de todos los reintentos
|
|
226
|
+
"""
|
|
227
|
+
for attempt in range(max_retries):
|
|
228
|
+
try:
|
|
229
|
+
return operation()
|
|
230
|
+
except (RedisConnectionError, RedisTimeoutError) as e:
|
|
231
|
+
if attempt == max_retries - 1:
|
|
232
|
+
logger.warning(f"Redis unavailable after {max_retries} attempts: {e}")
|
|
233
|
+
return None
|
|
234
|
+
time.sleep(retry_delay)
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
async def _execute_redis_op_async(
|
|
239
|
+
operation: Callable, max_retries: int = 3, retry_delay: float = 0.1
|
|
240
|
+
) -> Any:
|
|
241
|
+
"""
|
|
242
|
+
Ejecuta operación Redis asíncrona con reintentos y degradación elegante.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
operation: Función async que ejecuta la operación Redis
|
|
246
|
+
max_retries: Número máximo de reintentos
|
|
247
|
+
retry_delay: Tiempo de espera entre reintentos en segundos
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Resultado de la operación o None si falla después de todos los reintentos
|
|
251
|
+
"""
|
|
252
|
+
for attempt in range(max_retries):
|
|
253
|
+
try:
|
|
254
|
+
return await operation()
|
|
255
|
+
except (RedisConnectionError, RedisTimeoutError) as e:
|
|
256
|
+
if attempt == max_retries - 1:
|
|
257
|
+
logger.warning(f"Redis unavailable after {max_retries} attempts: {e}")
|
|
258
|
+
return None
|
|
259
|
+
await asyncio.sleep(retry_delay)
|
|
260
|
+
return None
|
|
261
|
+
|
|
262
|
+
|
|
160
263
|
def cache_response(
|
|
161
264
|
redis_client,
|
|
162
265
|
api_name: str,
|
|
@@ -165,22 +268,25 @@ def cache_response(
|
|
|
165
268
|
tags: list[str],
|
|
166
269
|
ttl: int | None = None,
|
|
167
270
|
use_cache: bool = True,
|
|
271
|
+
max_retries: int = 3,
|
|
168
272
|
):
|
|
169
273
|
"""
|
|
170
274
|
Decorator to cache the response of an endpoint.
|
|
171
275
|
|
|
172
276
|
Args:
|
|
173
|
-
redis_client: Redis client instance
|
|
277
|
+
redis_client: Redis client instance (sync or async)
|
|
174
278
|
api_name: Name of the API
|
|
175
279
|
collection: Name of the collection
|
|
176
280
|
endpoint_type: Type of endpoint
|
|
177
281
|
tags: List of tags to use for caching
|
|
178
282
|
ttl: Time to live in seconds
|
|
179
283
|
use_cache: Whether to use cache
|
|
284
|
+
max_retries: Maximum number of retry attempts for Redis operations
|
|
180
285
|
|
|
181
286
|
Returns:
|
|
182
287
|
Decorator function
|
|
183
288
|
"""
|
|
289
|
+
is_async = _is_async_client(redis_client)
|
|
184
290
|
|
|
185
291
|
def decorator(func: Callable):
|
|
186
292
|
@functools.wraps(func)
|
|
@@ -209,21 +315,81 @@ def cache_response(
|
|
|
209
315
|
}
|
|
210
316
|
else:
|
|
211
317
|
clear_cache_flag = bool(clear_cache_value)
|
|
318
|
+
|
|
319
|
+
# Intentar limpiar cache si se solicita
|
|
212
320
|
if clear_cache_flag:
|
|
213
|
-
|
|
321
|
+
try:
|
|
322
|
+
invalidate_tag(redis_client, f"{collection}")
|
|
323
|
+
except (RedisConnectionError, RedisTimeoutError) as e:
|
|
324
|
+
logger.warning(f"Redis connection error during cache clear: {e}")
|
|
214
325
|
|
|
215
326
|
# Construir clave
|
|
216
327
|
cache_key = await build_cache_key(
|
|
217
328
|
request, api_name, collection, endpoint_type
|
|
218
329
|
)
|
|
219
|
-
if redis_client.exists(cache_key):
|
|
220
|
-
return json.loads(redis_client.get(cache_key))
|
|
221
330
|
|
|
331
|
+
# Intentar obtener del cache
|
|
332
|
+
cached_data = None
|
|
333
|
+
if is_async:
|
|
334
|
+
|
|
335
|
+
async def get_cached_data():
|
|
336
|
+
exists_result = await redis_client.exists(cache_key)
|
|
337
|
+
if exists_result:
|
|
338
|
+
return await redis_client.get(cache_key)
|
|
339
|
+
return None
|
|
340
|
+
|
|
341
|
+
cached_data = await _execute_redis_op_async(
|
|
342
|
+
get_cached_data, max_retries=max_retries
|
|
343
|
+
)
|
|
344
|
+
else:
|
|
345
|
+
cached_data = _execute_redis_op(
|
|
346
|
+
lambda: (
|
|
347
|
+
redis_client.get(cache_key)
|
|
348
|
+
if redis_client.exists(cache_key)
|
|
349
|
+
else None
|
|
350
|
+
),
|
|
351
|
+
max_retries=max_retries,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
if cached_data is not None:
|
|
355
|
+
try:
|
|
356
|
+
return json.loads(cached_data)
|
|
357
|
+
except (json.JSONDecodeError, TypeError) as e:
|
|
358
|
+
logger.warning(
|
|
359
|
+
f"Failed to decode cached data for key {cache_key}: {e}"
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
# Cache miss o error: ejecutar función y cachear respuesta
|
|
222
363
|
response = await func(*args, **kwargs)
|
|
223
364
|
encoded_data = jsonable_encoder(response, custom_encoder={ObjectId: str})
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
365
|
+
|
|
366
|
+
# Intentar guardar en cache (falla silenciosamente si Redis no está disponible)
|
|
367
|
+
if is_async:
|
|
368
|
+
|
|
369
|
+
async def set_cache_data():
|
|
370
|
+
await redis_client.set(cache_key, json.dumps(encoded_data), ex=ttl)
|
|
371
|
+
|
|
372
|
+
await _execute_redis_op_async(set_cache_data, max_retries=max_retries)
|
|
373
|
+
# Agregar tags
|
|
374
|
+
for tag in tags:
|
|
375
|
+
# Capturar el valor de tag en la closure usando parámetro por defecto
|
|
376
|
+
async def add_tag(tag_name=tag):
|
|
377
|
+
await redis_client.sadd(f"tag:{tag_name}", cache_key)
|
|
378
|
+
|
|
379
|
+
await _execute_redis_op_async(add_tag, max_retries=max_retries)
|
|
380
|
+
else:
|
|
381
|
+
_execute_redis_op(
|
|
382
|
+
lambda: redis_client.set(
|
|
383
|
+
cache_key, json.dumps(encoded_data), ex=ttl
|
|
384
|
+
),
|
|
385
|
+
max_retries=max_retries,
|
|
386
|
+
)
|
|
387
|
+
# Agregar tags
|
|
388
|
+
for tag in tags:
|
|
389
|
+
_execute_redis_op(
|
|
390
|
+
lambda t=tag: redis_client.sadd(f"tag:{t}", cache_key),
|
|
391
|
+
max_retries=max_retries,
|
|
392
|
+
)
|
|
227
393
|
|
|
228
394
|
return encoded_data
|
|
229
395
|
|
|
@@ -272,7 +438,10 @@ def invalidate_tag(redis_client, tag: str):
|
|
|
272
438
|
redis_client: Redis client instance
|
|
273
439
|
tag: Tag to invalidate
|
|
274
440
|
"""
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
441
|
+
try:
|
|
442
|
+
keys = redis_client.smembers(f"tag:{tag}")
|
|
443
|
+
for key in keys:
|
|
444
|
+
redis_client.delete(key)
|
|
445
|
+
redis_client.delete(f"tag:{tag}")
|
|
446
|
+
except (RedisConnectionError, RedisTimeoutError) as e:
|
|
447
|
+
logger.warning(f"Redis connection error during tag invalidation: {e}")
|
|
@@ -927,7 +927,9 @@ class QueryBuilder:
|
|
|
927
927
|
|
|
928
928
|
return {"data": data_result, "meta": meta_result}
|
|
929
929
|
|
|
930
|
-
def get_reserved_field_lookup(
|
|
930
|
+
def get_reserved_field_lookup(
|
|
931
|
+
self, field_name: str, populate_options: Optional[Dict[str, Any]] = None
|
|
932
|
+
) -> List[Dict[str, Any]]:
|
|
931
933
|
"""
|
|
932
934
|
Generate lookup pipeline for reserved audit fields (creation_user, modification_user).
|
|
933
935
|
|
|
@@ -936,6 +938,8 @@ class QueryBuilder:
|
|
|
936
938
|
|
|
937
939
|
Args:
|
|
938
940
|
field_name: Name of the reserved field (e.g., "creation_user")
|
|
941
|
+
populate_options: Optional dict with populate options:
|
|
942
|
+
- fields: List of fields to include in projection (e.g., ['full_name'])
|
|
939
943
|
|
|
940
944
|
Returns:
|
|
941
945
|
List of aggregation stages for the lookup, or None if not a reserved field
|
|
@@ -945,6 +949,21 @@ class QueryBuilder:
|
|
|
945
949
|
|
|
946
950
|
field_config = RESERVED_AUDIT_FIELDS[field_name]
|
|
947
951
|
|
|
952
|
+
# Build projection based on populate_options
|
|
953
|
+
if populate_options and isinstance(populate_options, dict):
|
|
954
|
+
fields = populate_options.get("fields", [])
|
|
955
|
+
if fields and isinstance(fields, list):
|
|
956
|
+
# Include specified fields + always include _id
|
|
957
|
+
project_stage = {"_id": 1}
|
|
958
|
+
for field in fields:
|
|
959
|
+
project_stage[field] = 1
|
|
960
|
+
else:
|
|
961
|
+
# Default exclusion projection from config
|
|
962
|
+
project_stage = field_config["project_exclude"]
|
|
963
|
+
else:
|
|
964
|
+
# Default exclusion projection from config
|
|
965
|
+
project_stage = field_config["project_exclude"]
|
|
966
|
+
|
|
948
967
|
lookup_pipeline = [
|
|
949
968
|
{
|
|
950
969
|
"$lookup": {
|
|
@@ -956,7 +975,7 @@ class QueryBuilder:
|
|
|
956
975
|
"$expr": {"$eq": ["$_id", {"$toObjectId": "$$userId"}]}
|
|
957
976
|
}
|
|
958
977
|
},
|
|
959
|
-
{"$project":
|
|
978
|
+
{"$project": project_stage},
|
|
960
979
|
{"$addFields": {"_id": {"$toString": "$_id"}}},
|
|
961
980
|
],
|
|
962
981
|
"as": field_config["alias"],
|
|
@@ -972,8 +991,23 @@ class QueryBuilder:
|
|
|
972
991
|
|
|
973
992
|
return lookup_pipeline
|
|
974
993
|
|
|
975
|
-
def get_lookup_relation(
|
|
976
|
-
|
|
994
|
+
def get_lookup_relation(
|
|
995
|
+
self,
|
|
996
|
+
relation_field: List[str],
|
|
997
|
+
populate_options: Optional[Dict[str, Any]] = None,
|
|
998
|
+
) -> tuple:
|
|
999
|
+
"""
|
|
1000
|
+
Get lookup relation configuration for a specific relation field.
|
|
1001
|
+
|
|
1002
|
+
Args:
|
|
1003
|
+
relation_field: Relation configuration list [collection, field_name, prefix, ...]
|
|
1004
|
+
populate_options: Optional dict with populate options:
|
|
1005
|
+
- fields: List of fields to include in projection (e.g., ['name', 'code'])
|
|
1006
|
+
- filters: Dict of filters to apply to the lookup (future feature)
|
|
1007
|
+
|
|
1008
|
+
Returns:
|
|
1009
|
+
Tuple of (lookup_pipeline, collection_name)
|
|
1010
|
+
"""
|
|
977
1011
|
_prefix = self.collection_prefix + "_"
|
|
978
1012
|
if len(relation_field) > 2 and relation_field[2]:
|
|
979
1013
|
_prefix = relation_field[2] + "_"
|
|
@@ -1003,22 +1037,50 @@ class QueryBuilder:
|
|
|
1003
1037
|
else:
|
|
1004
1038
|
let_value = {"lid": f"${relation_field_name}"}
|
|
1005
1039
|
|
|
1040
|
+
# Build the pipeline for the lookup
|
|
1041
|
+
lookup_pipeline_stages = [
|
|
1042
|
+
{"$addFields": {"_id": {"$toString": "$_id"}}},
|
|
1043
|
+
match_stage,
|
|
1044
|
+
]
|
|
1045
|
+
|
|
1046
|
+
# Build projection based on populate_options
|
|
1047
|
+
if populate_options and isinstance(populate_options, dict):
|
|
1048
|
+
fields = populate_options.get("fields", [])
|
|
1049
|
+
if fields and isinstance(fields, list):
|
|
1050
|
+
# Include specified fields + always include _id
|
|
1051
|
+
project_stage = {"_id": 1}
|
|
1052
|
+
for field in fields:
|
|
1053
|
+
project_stage[field] = 1
|
|
1054
|
+
lookup_pipeline_stages.append({"$project": project_stage})
|
|
1055
|
+
else:
|
|
1056
|
+
# Default exclusion projection
|
|
1057
|
+
lookup_pipeline_stages.append(
|
|
1058
|
+
{
|
|
1059
|
+
"$project": {
|
|
1060
|
+
"creation_user": 0,
|
|
1061
|
+
"creation_date": 0,
|
|
1062
|
+
"disable": 0,
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
)
|
|
1066
|
+
else:
|
|
1067
|
+
# Default exclusion projection
|
|
1068
|
+
lookup_pipeline_stages.append(
|
|
1069
|
+
{
|
|
1070
|
+
"$project": {
|
|
1071
|
+
"creation_user": 0,
|
|
1072
|
+
"creation_date": 0,
|
|
1073
|
+
"disable": 0,
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
)
|
|
1077
|
+
|
|
1006
1078
|
lookup_relation = [
|
|
1007
1079
|
{
|
|
1008
1080
|
"$lookup": {
|
|
1009
1081
|
"from": f"{_prefix}{collection_name_relation}",
|
|
1010
1082
|
"let": let_value,
|
|
1011
|
-
"pipeline":
|
|
1012
|
-
{"$addFields": {"_id": {"$toString": "$_id"}}},
|
|
1013
|
-
match_stage,
|
|
1014
|
-
{
|
|
1015
|
-
"$project": {
|
|
1016
|
-
"creation_user": 0,
|
|
1017
|
-
"creation_date": 0,
|
|
1018
|
-
"disable": 0,
|
|
1019
|
-
}
|
|
1020
|
-
},
|
|
1021
|
-
],
|
|
1083
|
+
"pipeline": lookup_pipeline_stages,
|
|
1022
1084
|
"as": _as,
|
|
1023
1085
|
},
|
|
1024
1086
|
},
|
|
@@ -1067,9 +1129,16 @@ class QueryBuilder:
|
|
|
1067
1129
|
for query_relation in query_relations.keys():
|
|
1068
1130
|
# Check if it's a reserved field first
|
|
1069
1131
|
if query_relation in RESERVED_AUDIT_FIELDS:
|
|
1070
|
-
# Handle both True and dict with options (options are currently ignored)
|
|
1071
1132
|
if query_relations[query_relation]:
|
|
1072
|
-
|
|
1133
|
+
# Pass options if it's a dict, otherwise None
|
|
1134
|
+
populate_opts = (
|
|
1135
|
+
query_relations[query_relation]
|
|
1136
|
+
if isinstance(query_relations[query_relation], dict)
|
|
1137
|
+
else None
|
|
1138
|
+
)
|
|
1139
|
+
reserved_lookup = self.get_reserved_field_lookup(
|
|
1140
|
+
query_relation, populate_opts
|
|
1141
|
+
)
|
|
1073
1142
|
if reserved_lookup:
|
|
1074
1143
|
query_lookup_relations.extend(reserved_lookup)
|
|
1075
1144
|
continue
|
|
@@ -1091,9 +1160,10 @@ class QueryBuilder:
|
|
|
1091
1160
|
]
|
|
1092
1161
|
|
|
1093
1162
|
if isinstance(query_relations[query_relation], dict):
|
|
1094
|
-
#
|
|
1095
|
-
|
|
1096
|
-
|
|
1163
|
+
# Pass populate options (fields, filters, etc.) to the lookup
|
|
1164
|
+
lookup_relation = self.get_lookup_relation(
|
|
1165
|
+
relation, query_relations[query_relation]
|
|
1166
|
+
)
|
|
1097
1167
|
query_lookup_relations = [
|
|
1098
1168
|
*query_lookup_relations,
|
|
1099
1169
|
*lookup_relation[0],
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
oplnk_python_utils/__init__.py,sha256=
|
|
1
|
+
oplnk_python_utils/__init__.py,sha256=EZKfDHMpt3vBb3qmOBCja0UU08uwGaqnYBPXOQADlsk,354
|
|
2
2
|
oplnk_python_utils/cache/__init__.py,sha256=UtCAGUH8WinNMeh-MIOLPZeRMUn5_6Jn_QC2iKWvIN0,646
|
|
3
|
-
oplnk_python_utils/cache/cache_utils.py,sha256=
|
|
3
|
+
oplnk_python_utils/cache/cache_utils.py,sha256=2L4Yyarn0y7sREeLGJANTjq55UXy6YvJ8Sqd6wSEXSM,14856
|
|
4
4
|
oplnk_python_utils/core/__init__.py,sha256=HmT7BR9WCGcu47Bd-eMdSSTImKnzsaalzYVThgTuyMA,253
|
|
5
5
|
oplnk_python_utils/core/data.py,sha256=BwOIGeS4c7gGOOUcx-2RpFJsznjAtBVETRv63A_3PDY,1936
|
|
6
6
|
oplnk_python_utils/core/parsing.py,sha256=EdI_uQJWalXL7yen5iyQ6J5PRLgz6Xi_szWaUDOhlgQ,5190
|
|
7
7
|
oplnk_python_utils/core/serialization.py,sha256=B4g3x6yGjmXySNtOHqqFrq8AlguYz4gldxCJY21UNdQ,1164
|
|
8
8
|
oplnk_python_utils/query/__init__.py,sha256=dd-PRf5fkoQvAbcONumwrRveS_Ejnw-3Q0hA7ZcF6Ic,147
|
|
9
|
-
oplnk_python_utils/query/query_builder.py,sha256=
|
|
9
|
+
oplnk_python_utils/query/query_builder.py,sha256=54VLb_67ncdhehnQp0vKenfSsSUTfMzp_jD9YIt8CgU,64246
|
|
10
10
|
oplnk_python_utils/reports/__init__.py,sha256=CPxWaRgXt9sHK_6mQwsXnUxg9yzJ2GHSlQ4lVNpkanY,94
|
|
11
|
-
oplnk_python_utils-1.
|
|
12
|
-
oplnk_python_utils-1.
|
|
13
|
-
oplnk_python_utils-1.
|
|
14
|
-
oplnk_python_utils-1.
|
|
15
|
-
oplnk_python_utils-1.
|
|
11
|
+
oplnk_python_utils-1.3.0.dist-info/licenses/LICENSE,sha256=JgkAUZElZ6gNf7C2aZPq5-GEjUG2t_yYAKvuhy1OKy0,1090
|
|
12
|
+
oplnk_python_utils-1.3.0.dist-info/METADATA,sha256=s98cLEa9nzgwIsRR5sF0ZkMeKPCj6ZsNQB8M3VruZvc,24924
|
|
13
|
+
oplnk_python_utils-1.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
14
|
+
oplnk_python_utils-1.3.0.dist-info/top_level.txt,sha256=TPZ-emvWspdR-OxTNOvQt-cDrZ4m7P4RMnxsXHOeorc,19
|
|
15
|
+
oplnk_python_utils-1.3.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|