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.
@@ -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,13 @@
1
+ """
2
+ Core utilities module - General-purpose utilities for data manipulation, parsing, and serialization
3
+ """
4
+
5
+ from . import data
6
+ from . import parsing
7
+ from . import serialization
8
+
9
+ __all__ = [
10
+ "data",
11
+ "parsing",
12
+ "serialization"
13
+ ]
@@ -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
@@ -0,0 +1,9 @@
1
+ """
2
+ Query utilities module - Reusable query building functionality
3
+ """
4
+
5
+ from .query_builder import QueryBuilder
6
+
7
+ __all__ = [
8
+ "QueryBuilder"
9
+ ]