oplnk-python-utils 1.0.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 +18 -0
- oplnk_python_utils/cache/__init__.py +21 -0
- oplnk_python_utils/cache/cache_utils.py +88 -0
- oplnk_python_utils/core/__init__.py +13 -0
- oplnk_python_utils/core/data.py +58 -0
- oplnk_python_utils/core/parsing.py +114 -0
- oplnk_python_utils/core/serialization.py +32 -0
- oplnk_python_utils/query/__init__.py +9 -0
- oplnk_python_utils/query/query_builder.py +817 -0
- oplnk_python_utils-1.0.0.dist-info/METADATA +540 -0
- oplnk_python_utils-1.0.0.dist-info/RECORD +14 -0
- oplnk_python_utils-1.0.0.dist-info/WHEEL +5 -0
- oplnk_python_utils-1.0.0.dist-info/licenses/LICENSE +21 -0
- oplnk_python_utils-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenLink Python Utils - Reusable utilities for caching, data manipulation, parsing, and MongoDB query building
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# Import modules only - no function re-exports
|
|
6
|
+
from . import core
|
|
7
|
+
from . import cache
|
|
8
|
+
from . import query
|
|
9
|
+
|
|
10
|
+
__version__ = "1.0.0"
|
|
11
|
+
__author__ = "OpenLink SpA"
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"core",
|
|
15
|
+
"cache",
|
|
16
|
+
"query",
|
|
17
|
+
"__version__"
|
|
18
|
+
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cache utilities module - Reusable caching functionality
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .cache_utils import (
|
|
6
|
+
generate_cache_id,
|
|
7
|
+
get_or_set_cache,
|
|
8
|
+
invalidate_cache_keys
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
# Re-export core utilities for convenience in this domain
|
|
12
|
+
from ..core.data import normalize
|
|
13
|
+
from ..core.serialization import datetime_serializer
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"generate_cache_id",
|
|
17
|
+
"datetime_serializer",
|
|
18
|
+
"get_or_set_cache",
|
|
19
|
+
"invalidate_cache_keys",
|
|
20
|
+
"normalize"
|
|
21
|
+
]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cache utilities - Generic caching functions that can be used across projects
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from typing import Callable, Any, Optional, Union
|
|
8
|
+
from fastapi.encoders import jsonable_encoder
|
|
9
|
+
from bson.objectid import ObjectId
|
|
10
|
+
|
|
11
|
+
# Import core utilities
|
|
12
|
+
from ..core.data import normalize
|
|
13
|
+
from ..core.serialization import datetime_serializer
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def generate_cache_id(params: dict) -> str:
|
|
17
|
+
"""
|
|
18
|
+
Utility to generate a cache key hash from a dictionary.
|
|
19
|
+
Creates a consistent hash based on normalized dictionary content.
|
|
20
|
+
"""
|
|
21
|
+
normalized = normalize(params)
|
|
22
|
+
encoded_data = jsonable_encoder(normalized, custom_encoder={ObjectId: str})
|
|
23
|
+
serialized = json.dumps(encoded_data, separators=(",", ":"), ensure_ascii=True)
|
|
24
|
+
return hashlib.sha256(serialized.encode("utf-8")).hexdigest()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_or_set_cache(
|
|
28
|
+
redis_client,
|
|
29
|
+
cache_key: str,
|
|
30
|
+
fetch_func: Callable,
|
|
31
|
+
clear_cache: Union[str, bool] = False,
|
|
32
|
+
ttl: Optional[int] = None,
|
|
33
|
+
serializer: Optional[Callable] = None,
|
|
34
|
+
) -> Any:
|
|
35
|
+
"""
|
|
36
|
+
Retrieve data from cache if available, otherwise fetch, cache, and return it.
|
|
37
|
+
Optionally clear cache before fetching.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
redis_client: Redis client instance
|
|
41
|
+
cache_key: Key to use for caching
|
|
42
|
+
fetch_func: Function to call if cache miss
|
|
43
|
+
clear_cache: Pattern to clear or boolean to clear specific key
|
|
44
|
+
ttl: Time to live in seconds
|
|
45
|
+
serializer: Function to serialize data before caching
|
|
46
|
+
"""
|
|
47
|
+
if clear_cache:
|
|
48
|
+
invalidate_cache_keys(
|
|
49
|
+
redis_client,
|
|
50
|
+
clear_cache if isinstance(clear_cache, str) else cache_key
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if redis_client.exists(cache_key):
|
|
54
|
+
return json.loads(redis_client.get(cache_key))
|
|
55
|
+
|
|
56
|
+
data = fetch_func()
|
|
57
|
+
if serializer:
|
|
58
|
+
data = serializer(data)
|
|
59
|
+
|
|
60
|
+
encoded_data = jsonable_encoder(data, custom_encoder={ObjectId: str})
|
|
61
|
+
redis_client.set(cache_key, json.dumps(encoded_data), ex=ttl)
|
|
62
|
+
return encoded_data
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def invalidate_cache_keys(redis_client, pattern: str) -> int:
|
|
66
|
+
"""
|
|
67
|
+
Utility to invalidate cache keys matching a pattern.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
redis_client: Redis client instance
|
|
71
|
+
pattern: Pattern to match keys for deletion
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Number of keys deleted
|
|
75
|
+
"""
|
|
76
|
+
cursor = 0
|
|
77
|
+
batch_size = 1000
|
|
78
|
+
total_deleted = 0
|
|
79
|
+
|
|
80
|
+
while True:
|
|
81
|
+
cursor, keys = redis_client.scan(cursor=cursor, match=pattern, count=batch_size)
|
|
82
|
+
if keys:
|
|
83
|
+
redis_client.unlink(*keys)
|
|
84
|
+
total_deleted += len(keys)
|
|
85
|
+
if cursor == 0:
|
|
86
|
+
break
|
|
87
|
+
|
|
88
|
+
return total_deleted
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data manipulation utilities - Functions for normalizing and merging data structures
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Dict, Any, Union, List
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def normalize(value: Union[Dict, List, Any]) -> Union[Dict, List, Any]:
|
|
9
|
+
"""
|
|
10
|
+
Utility to normalize a dictionary for consistent cache key generation.
|
|
11
|
+
Recursively sorts dictionary keys and normalizes nested structures.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
value: The value to normalize (dict, list, or primitive)
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Normalized value with sorted keys for dictionaries
|
|
18
|
+
"""
|
|
19
|
+
if isinstance(value, dict):
|
|
20
|
+
return {k: normalize(value[k]) for k in sorted(value)}
|
|
21
|
+
elif isinstance(value, list):
|
|
22
|
+
return [normalize(v) for v in value]
|
|
23
|
+
else:
|
|
24
|
+
return value
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def merge_dicts(*dicts: Dict[str, Any]) -> Dict[str, Any]:
|
|
28
|
+
"""
|
|
29
|
+
Merge multiple dictionaries, handling nested structures and arrays.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
*dicts: Variable number of dictionaries to merge
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Merged dictionary with nested structures properly combined
|
|
36
|
+
"""
|
|
37
|
+
def merge(a: Dict[str, Any], b: Dict[str, Any]) -> None:
|
|
38
|
+
"""Internal merge function that modifies dictionary a in place"""
|
|
39
|
+
for key in b:
|
|
40
|
+
if key in a:
|
|
41
|
+
if isinstance(a[key], dict) and isinstance(b[key], dict):
|
|
42
|
+
merge(a[key], b[key])
|
|
43
|
+
elif isinstance(a[key], list) and isinstance(b[key], list):
|
|
44
|
+
a[key].extend(b[key])
|
|
45
|
+
elif isinstance(a[key], list):
|
|
46
|
+
a[key].append(b[key])
|
|
47
|
+
elif isinstance(b[key], list):
|
|
48
|
+
b[key].insert(0, a[key])
|
|
49
|
+
a[key] = b[key]
|
|
50
|
+
else:
|
|
51
|
+
a[key] = [a[key], b[key]]
|
|
52
|
+
else:
|
|
53
|
+
a[key] = b[key]
|
|
54
|
+
|
|
55
|
+
result = {}
|
|
56
|
+
for d in dicts:
|
|
57
|
+
merge(result, d)
|
|
58
|
+
return result
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Parsing utilities - Functions for parsing and type conversion
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import List, Dict, Any, Union
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from .data import merge_dicts
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def infer_type(value: Union[str, List, Any]) -> Any:
|
|
12
|
+
"""
|
|
13
|
+
Infer the correct Python type from a string value.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
value: The value to convert (string, list, or other)
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Value converted to appropriate Python type
|
|
20
|
+
"""
|
|
21
|
+
if isinstance(value, list):
|
|
22
|
+
return [infer_type(v) for v in value]
|
|
23
|
+
|
|
24
|
+
if not isinstance(value, str):
|
|
25
|
+
return value # Skip non-string non-list
|
|
26
|
+
|
|
27
|
+
# Now it's safe to do .lower()
|
|
28
|
+
value_lower = value.lower()
|
|
29
|
+
if value_lower in {"true", "false"}:
|
|
30
|
+
return value_lower == "true"
|
|
31
|
+
|
|
32
|
+
# Try to convert to int
|
|
33
|
+
try:
|
|
34
|
+
return int(value)
|
|
35
|
+
except ValueError:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
# Try to convert to float
|
|
39
|
+
try:
|
|
40
|
+
return float(value)
|
|
41
|
+
except ValueError:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
# Try to convert to datetime
|
|
45
|
+
try:
|
|
46
|
+
return datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%fZ")
|
|
47
|
+
except ValueError:
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
# If nothing works, return the original string
|
|
51
|
+
return value
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def parse_key_string_to_list(s: str) -> List[str]:
|
|
55
|
+
"""
|
|
56
|
+
Parse a string like 'filters[name][$containsi]' into ['filters', 'name', '$containsi'].
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
s: String with bracketed notation
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
List of parsed key components
|
|
63
|
+
"""
|
|
64
|
+
parts = re.findall(r"\[([^\]]+)\]", s)
|
|
65
|
+
parts.insert(0, s.split("[")[0])
|
|
66
|
+
return parts
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def parse_list_to_dict(lst: List[str], value: str) -> Dict[str, Any]:
|
|
70
|
+
"""
|
|
71
|
+
Convert a list of keys and a value into a nested dictionary.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
lst: List of keys for nested structure
|
|
75
|
+
value: Value to place at the deepest level
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Nested dictionary structure
|
|
79
|
+
"""
|
|
80
|
+
result = None
|
|
81
|
+
new_value = infer_type(value)
|
|
82
|
+
|
|
83
|
+
for key in reversed(lst):
|
|
84
|
+
if key.isdigit():
|
|
85
|
+
result = [new_value] if result is None else [result]
|
|
86
|
+
else:
|
|
87
|
+
result = {key: new_value} if result is None else {key: result}
|
|
88
|
+
|
|
89
|
+
return result
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def parse_params_to_dict(params: Union[Dict[str, str], Any]) -> Dict[str, Any]:
|
|
93
|
+
"""
|
|
94
|
+
Parse query parameters into a structured dictionary.
|
|
95
|
+
Handles nested parameters like filters[name][$containsi]=john
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
params: Query parameters (FastAPI QueryParams or dict-like object)
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Structured dictionary with nested parameters
|
|
102
|
+
"""
|
|
103
|
+
# Handle FastAPI QueryParams or similar objects
|
|
104
|
+
if hasattr(params, 'items'):
|
|
105
|
+
param_dict = dict(params.items()) if hasattr(params, 'items') else dict(params)
|
|
106
|
+
else:
|
|
107
|
+
param_dict = params
|
|
108
|
+
|
|
109
|
+
result = {}
|
|
110
|
+
for key, value in param_dict.items():
|
|
111
|
+
args = parse_key_string_to_list(key)
|
|
112
|
+
dictionary = parse_list_to_dict(args, value)
|
|
113
|
+
result = merge_dicts(result, dictionary)
|
|
114
|
+
return result
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Serialization utilities - Functions for converting data types to serializable formats
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Union, Dict, List
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def datetime_serializer(data: Union[Dict, List, Any]) -> Union[Dict, List, Any]:
|
|
10
|
+
"""
|
|
11
|
+
Utility to serialize datetime objects to ISO format for cache storage.
|
|
12
|
+
Recursively processes dictionaries and lists to convert all datetime objects.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
data: Data structure that may contain datetime objects
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
Data structure with datetime objects converted to ISO format strings
|
|
19
|
+
"""
|
|
20
|
+
if isinstance(data, dict):
|
|
21
|
+
for key, value in data.items():
|
|
22
|
+
if isinstance(value, datetime):
|
|
23
|
+
data[key] = value.isoformat()
|
|
24
|
+
elif isinstance(value, (dict, list)):
|
|
25
|
+
datetime_serializer(value)
|
|
26
|
+
elif isinstance(data, list):
|
|
27
|
+
for index, value in enumerate(data):
|
|
28
|
+
if isinstance(value, datetime):
|
|
29
|
+
data[index] = value.isoformat()
|
|
30
|
+
elif isinstance(value, (dict, list)):
|
|
31
|
+
datetime_serializer(value)
|
|
32
|
+
return data
|