oplnk-python-utils 1.0.0__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 OpenLink SpA
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ include README.md
2
+ include LICENSE
3
+ include MANIFEST.in
4
+ recursive-include * *.py
5
+ recursive-exclude * __pycache__
6
+ recursive-exclude * *.py[co]
@@ -0,0 +1,540 @@
1
+ Metadata-Version: 2.4
2
+ Name: oplnk-python-utils
3
+ Version: 1.0.0
4
+ Summary: Reusable utilities for caching, data manipulation, parsing, and MongoDB query building
5
+ Home-page: https://github.com/openlinkspa/oplnk-python-utils
6
+ Author: OpenLink SpA
7
+ Author-email: contacto@openlink.cl
8
+ License: MIT
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Requires-Python: >=3.8
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: pymongo>=3.0.0
23
+ Requires-Dist: redis>=3.0.0
24
+ Requires-Dist: fastapi>=0.65.0
25
+ Requires-Dist: bson>=0.5.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=6.0; extra == "dev"
28
+ Requires-Dist: pytest-cov>=2.0; extra == "dev"
29
+ Requires-Dist: black>=21.0; extra == "dev"
30
+ Requires-Dist: flake8>=3.8; extra == "dev"
31
+ Dynamic: author
32
+ Dynamic: author-email
33
+ Dynamic: classifier
34
+ Dynamic: description
35
+ Dynamic: description-content-type
36
+ Dynamic: home-page
37
+ Dynamic: license
38
+ Dynamic: license-file
39
+ Dynamic: provides-extra
40
+ Dynamic: requires-dist
41
+ Dynamic: requires-python
42
+ Dynamic: summary
43
+
44
+ # OpenLink Python Utils
45
+
46
+ A comprehensive Python package providing reusable utilities for caching, data manipulation, parsing, and MongoDB query building. Developed by OpenLink SpA for use across multiple projects.
47
+
48
+ ## Overview
49
+
50
+ This package abstracts common functionality for:
51
+
52
+ - **Caching**: Redis-based caching utilities with serialization support
53
+ - **Query Building**: MongoDB aggregation pipeline builders with advanced filtering, sorting, pagination, and population features
54
+
55
+ ## Installation
56
+
57
+ Install from PyPI:
58
+
59
+ ```bash
60
+ pip install oplnk-python-utils
61
+ ```
62
+
63
+ Or install in development mode:
64
+
65
+ ```bash
66
+ pip install -e /path/to/oplnk-python-utils
67
+ ```
68
+
69
+ Add to your `requirements.txt`:
70
+
71
+ ```
72
+ oplnk-python-utils>=1.0.0
73
+ ```
74
+
75
+ ## Core Module
76
+
77
+ The core module contains general-purpose utilities organized by functionality. These are the foundational utilities used by other modules and can be used independently.
78
+
79
+ ### Data Utilities (`core.data`)
80
+
81
+ Functions for data structure manipulation and normalization.
82
+
83
+ ```python
84
+ from oplnk_python_utils.core.data import normalize, merge_dicts
85
+
86
+ # Normalize data for consistent processing (useful for caching)
87
+ data = {"z": 1, "a": {"c": 3, "b": 2}}
88
+ normalized = normalize(data)
89
+ # Result: {"a": {"b": 2, "c": 3}, "z": 1} - keys sorted recursively
90
+
91
+ # Merge multiple dictionaries with nested structure support
92
+ result = merge_dicts(
93
+ {"filters": {"name": "John"}},
94
+ {"filters": {"age": 25}},
95
+ {"sort": ["name:asc"]}
96
+ )
97
+ # Result: {"filters": {"name": ["John"], "age": 25}, "sort": ["name:asc"]}
98
+
99
+ # Advanced merging with arrays and conflicts
100
+ dict1 = {"tags": ["python"], "meta": {"version": 1}}
101
+ dict2 = {"tags": ["fastapi"], "meta": {"author": "team"}}
102
+ merged = merge_dicts(dict1, dict2)
103
+ # Result: {"tags": ["python", "fastapi"], "meta": {"version": 1, "author": "team"}}
104
+ ```
105
+
106
+ #### Data Functions
107
+
108
+ - **`normalize(value)`**: Recursively normalize data structures by sorting dictionary keys
109
+ - **`merge_dicts(*dicts)`**: Intelligently merge multiple dictionaries with nested structure support
110
+
111
+ ### Parsing Utilities (`core.parsing`)
112
+
113
+ Functions for parsing query parameters and type conversion.
114
+
115
+ ```python
116
+ from oplnk_python_utils.core.parsing import (
117
+ parse_params_to_dict,
118
+ infer_type,
119
+ parse_key_string_to_list,
120
+ parse_list_to_dict
121
+ )
122
+
123
+ # Parse nested query parameters (main function)
124
+ params = {
125
+ "filters[name][$containsi]": "john",
126
+ "filters[age][$gte]": "18",
127
+ "pagination[page]": "1",
128
+ "sort[0]": "name:asc",
129
+ "populate[0]": "user"
130
+ }
131
+ parsed = parse_params_to_dict(params)
132
+ # Result: {
133
+ # "filters": {"name": {"$containsi": "john"}, "age": {"$gte": 18}},
134
+ # "pagination": {"page": 1},
135
+ # "sort": ["name:asc"],
136
+ # "populate": ["user"]
137
+ # }
138
+
139
+ # Smart type inference with various formats
140
+ value = infer_type("123") # Returns: 123 (int)
141
+ value = infer_type("12.5") # Returns: 12.5 (float)
142
+ value = infer_type("true") # Returns: True (bool)
143
+ value = infer_type("false") # Returns: False (bool)
144
+ value = infer_type("hello") # Returns: "hello" (str)
145
+ value = infer_type(["1", "2"]) # Returns: [1, 2] (list with converted types)
146
+
147
+ # Lower-level parsing functions
148
+ keys = parse_key_string_to_list("filters[name][$eq]")
149
+ # Result: ["filters", "name", "$eq"]
150
+
151
+ nested_dict = parse_list_to_dict(["filters", "name", "$eq"], "john")
152
+ # Result: {"filters": {"name": {"$eq": "john"}}}
153
+ ```
154
+
155
+ #### Parsing Functions
156
+
157
+ - **`parse_params_to_dict(params)`**: Parse FastAPI QueryParams or dict into structured nested dictionary
158
+ - **`infer_type(value)`**: Smart type inference from string values (supports int, float, bool, datetime, lists)
159
+ - **`parse_key_string_to_list(s)`**: Parse bracketed notation strings like `"filters[name][$eq]"`
160
+ - **`parse_list_to_dict(lst, value)`**: Convert key list and value into nested dictionary structure
161
+
162
+ ### Serialization Utilities (`core.serialization`)
163
+
164
+ Functions for converting data types to serializable formats.
165
+
166
+ ```python
167
+ from oplnk_python_utils.core.serialization import datetime_serializer
168
+ from datetime import datetime
169
+
170
+ # Serialize datetime objects for JSON/cache storage
171
+ data = {
172
+ "created_at": datetime.now(),
173
+ "users": [
174
+ {"last_login": datetime.now(), "name": "John"},
175
+ {"last_login": datetime.now(), "name": "Jane"}
176
+ ],
177
+ "metadata": {
178
+ "processed_at": datetime.now()
179
+ }
180
+ }
181
+
182
+ serialized = datetime_serializer(data)
183
+ # All datetime objects converted to ISO strings recursively
184
+ # Non-datetime values remain unchanged
185
+ ```
186
+
187
+ #### Serialization Functions
188
+
189
+ - **`datetime_serializer(data)`**: Recursively convert datetime objects to ISO format strings in any data structure
190
+
191
+ ## Cache Module
192
+
193
+ Advanced caching utilities with Redis integration and intelligent cache key management.
194
+
195
+ ```python
196
+ from oplnk_python_utils.cache import generate_cache_id, get_or_set_cache, invalidate_cache_keys
197
+
198
+ # Generate consistent cache keys from complex data
199
+ cache_key = generate_cache_id({
200
+ "filters": {"name": "John", "status": "active"},
201
+ "sort": ["created_at:desc"]
202
+ })
203
+ # Automatically normalizes data internally and creates MD5 hash
204
+
205
+ # Get from cache or execute function and cache result
206
+ async def expensive_operation(query):
207
+ # Some expensive database operation
208
+ return await db.collection.find(query).to_list()
209
+
210
+ result = await get_or_set_cache(
211
+ cache_key="user_query_abc123",
212
+ fetch_function=expensive_operation,
213
+ query={"status": "active"},
214
+ expiration=3600 # 1 hour
215
+ )
216
+
217
+ # Cache with automatic serialization of datetime objects
218
+ from datetime import datetime
219
+ async def get_user_data():
220
+ return {
221
+ "user": "john",
222
+ "last_login": datetime.now(),
223
+ "preferences": {"theme": "dark"}
224
+ }
225
+
226
+ cached_data = await get_or_set_cache(
227
+ cache_key="user_john_data",
228
+ fetch_function=get_user_data,
229
+ expiration=1800
230
+ )
231
+
232
+ # Invalidate multiple cache keys by pattern
233
+ invalidated_count = await invalidate_cache_keys("user_*")
234
+ print(f"Invalidated {invalidated_count} cache entries")
235
+ ```
236
+
237
+ ### Cache Functions
238
+
239
+ - **`generate_cache_id(data, prefix="")`**: Generate MD5 hash from normalized data for consistent cache keys
240
+ - **`get_or_set_cache(cache_key, fetch_function, expiration=3600, **kwargs)`\*\*: Get cached result or execute function and cache with automatic datetime serialization
241
+ - **`invalidate_cache_keys(pattern)`**: Delete cache keys matching pattern (supports wildcards)
242
+
243
+ ## Query Module
244
+
245
+ The query module provides a MongoDB aggregation pipeline builder with advanced features.
246
+
247
+ ### Usage
248
+
249
+ ```python
250
+ from oplnk_python_utils.query import QueryBuilder
251
+ from pymongo import MongoClient
252
+
253
+ # Initialize MongoDB
254
+ db = MongoClient()["mydb"]
255
+ collection = db["users"]
256
+
257
+ # Create query builder
258
+ query_builder = QueryBuilder(
259
+ collection_prefix="myapp",
260
+ relations=[
261
+ ["User", "user_id", "myapp", "users"],
262
+ ["Role", "role_ids", "myapp", "roles"]
263
+ ]
264
+ )
265
+
266
+ # Build query
267
+ query = {
268
+ "filters": {
269
+ "name": {"$containsi": "john"},
270
+ "age": {"$gte": 18},
271
+ "$or": [
272
+ {"status": {"$eq": "active"}},
273
+ {"last_login": {"$gte": "2023-01-01"}}
274
+ ]
275
+ },
276
+ "sort": ["name:asc", "age:desc"],
277
+ "pagination": {"page": 1, "pageSize": 10},
278
+ "populate": ["role"],
279
+ "fields": ["name", "email", "age"]
280
+ }
281
+
282
+ # Method 1: Using parsed query dictionary
283
+ results = query_builder.get_query_response(
284
+ db_collection=collection,
285
+ query=query,
286
+ cache_client=redis_client,
287
+ cache_key_prefix="users",
288
+ use_cache=True
289
+ )
290
+
291
+ # Method 2: Using raw query parameters (like from FastAPI request)
292
+ raw_params = {
293
+ "filters[name][$containsi]": "john",
294
+ "filters[age][$gte]": "18",
295
+ "sort[0]": "name:asc",
296
+ "pagination[page]": "1",
297
+ "pagination[pageSize]": "10"
298
+ }
299
+
300
+ # Option A: Parse manually then use get_query_response
301
+ from oplnk_python_utils.core.parsing import parse_params_to_dict
302
+ parsed_query = parse_params_to_dict(raw_params)
303
+ results = query_builder.get_query_response(
304
+ db_collection=collection,
305
+ query=parsed_query,
306
+ cache_client=redis_client,
307
+ cache_key_prefix="users",
308
+ use_cache=True
309
+ )
310
+
311
+ # Option B: Use read_query with automatic parsing (recommended)
312
+ results = query_builder.read_query(
313
+ db_collection=collection,
314
+ params=raw_params, # Will be parsed automatically using core.parsing
315
+ cache_client=redis_client,
316
+ cache_key_prefix="users",
317
+ use_cache=True
318
+ )
319
+
320
+ print(results)
321
+ # {
322
+ # "data": [...],
323
+ # "meta": {"totalCount": 150}
324
+ # }
325
+ ```
326
+
327
+ ### QueryBuilder Methods
328
+
329
+ #### Pipeline Builders
330
+
331
+ - **`build_filter_pipeline(filters)`**: Build MongoDB filter pipeline
332
+ - **`build_sort_pipeline(sort_params)`**: Build sort pipeline
333
+ - **`build_fields_pipeline(fields_params)`**: Build field projection pipeline
334
+ - **`build_distinct_pipeline(distinct_params)`**: Build distinct pipeline
335
+ - **`build_pagination_pipeline(pagination_params)`**: Build pagination pipeline
336
+ - **`build_populate_pipeline(populate_params)`**: Build population pipeline
337
+
338
+ #### Aggregation Builders
339
+
340
+ - **`make_aggregation_query(query, base_pipeline=None)`**: Build complete aggregation pipeline
341
+ - **`make_count_aggregation_query(query, base_pipeline=None)`**: Build count aggregation pipeline
342
+
343
+ #### Query Execution
344
+
345
+ - **`get_query_response(db_collection, query, ...)`**: Execute query with optional caching
346
+ - **`read_query(db_collection, params=None, query=None, ...)`**: Complete query method with parameter parsing
347
+
348
+ #### Core Module Integration
349
+
350
+ The QueryBuilder class leverages utilities from the core module for enhanced functionality:
351
+
352
+ - **Parameter parsing**: Uses `core.parsing.parse_params_to_dict()` for automatic query parameter parsing
353
+ - **Data normalization**: Uses `core.data.normalize()` for consistent cache key generation
354
+ - **Datetime serialization**: Uses `core.serialization.datetime_serializer()` for cache-friendly data
355
+
356
+ See the [Core Module](#core-module) section for detailed documentation of these utilities.
357
+
358
+ ### Supported Filter Operators
359
+
360
+ #### Text Search
361
+
362
+ - `$contains`, `$containsi` (case-insensitive)
363
+ - `$notContains`, `$notContainsi`
364
+ - `$startsWith`, `$startsWithi`
365
+ - `$endsWith`, `$endsWithi`
366
+ - `$eqi` (case-insensitive equality), `$nei`
367
+
368
+ #### Comparison
369
+
370
+ - `$eq`, `$ne`, `$lt`, `$lte`, `$gt`, `$gte`
371
+ - `$in`, `$notIn`
372
+ - `$between` (requires array with 2 elements)
373
+
374
+ #### Logical
375
+
376
+ - `$and`, `$or`
377
+
378
+ #### Existence
379
+
380
+ - `$exists`, `$notExists`
381
+ - `$null`, `$notNull`
382
+
383
+ #### Arrays
384
+
385
+ - `$listContains`, `$listNotContains`
386
+
387
+ ### Query Structure
388
+
389
+ ```python
390
+ query = {
391
+ "filters": {
392
+ # Filter conditions
393
+ "field": {"$operator": "value"},
394
+ "$or": [
395
+ {"field1": {"$eq": "value1"}},
396
+ {"field2": {"$gt": 10}}
397
+ ]
398
+ },
399
+ "sort": ["field1:asc", "field2:desc"],
400
+ "pagination": {
401
+ "page": 1,
402
+ "pageSize": 20
403
+ },
404
+ "populate": ["relation1", "relation2"], # or {"relation1": True}
405
+ "fields": ["field1", "field2", "field3"],
406
+ "distinct": ["field1", "field2"]
407
+ }
408
+ ```
409
+
410
+ ## Integration with Existing CRUD
411
+
412
+ ### Before (Original CRUD + Endpoints)
413
+
414
+ ```python
415
+ # CRUD Class - 30+ lines of complex query logic
416
+ class CRUD:
417
+ def __init__(self, collection, relations=[]):
418
+ # ... original implementation
419
+
420
+ def read_query(self, query, pipeline=None, pipeline_count=None, use_cache=False):
421
+ # ... 30+ lines of complex caching and query logic
422
+
423
+ # Endpoint - Manual parameter parsing
424
+ from src.utils import parse_params_to_dict
425
+
426
+ @router.get("/query")
427
+ async def get_data(request: Request):
428
+ params = request.query_params
429
+ query = parse_params_to_dict(params) # Manual parsing
430
+ return crud.read_query(query, pipeline=base_pipeline, use_cache=True)
431
+ ```
432
+
433
+ ### After (Using Utils Package)
434
+
435
+ ```python
436
+ # CRUD Class - Now super clean!
437
+ from oplnk_python_utils.query import QueryBuilder
438
+
439
+ class CRUD(QueryBuilder):
440
+ def __init__(self, collection, relations=[]):
441
+ self.collection = collection
442
+ self.db = db[self.collection]
443
+ super().__init__(collection_prefix=PREFIX, relations=relations)
444
+
445
+ def read_query(self, params=None, query=None, pipeline=None, pipeline_count=None, use_cache=False):
446
+ # Just 6 lines! All complexity abstracted away
447
+ return super().read_query(
448
+ db_collection=self.db, params=params, query=query,
449
+ pipeline=pipeline, pipeline_count=pipeline_count,
450
+ use_cache=use_cache, cache_client=redis,
451
+ cache_key_prefix=f"app:{self.collection}"
452
+ )
453
+
454
+ # Endpoint - Even simpler with explicit imports!
455
+ from oplnk_python_utils.core.parsing import parse_params_to_dict
456
+
457
+ @router.get("/query")
458
+ async def get_data(request: Request):
459
+ # Option 1: Manual parsing (explicit control)
460
+ query = parse_params_to_dict(request.query_params)
461
+ return crud.read_query(query=query, pipeline=base_pipeline, use_cache=True)
462
+
463
+ # Option 2: Automatic parsing (convenience)
464
+ return crud.read_query(
465
+ params=request.query_params, # Parsed automatically
466
+ pipeline=base_pipeline,
467
+ use_cache=True
468
+ )
469
+ ```
470
+
471
+ ## Advanced Features
472
+
473
+ ### Custom Relation Definitions
474
+
475
+ ```python
476
+ relations = [
477
+ # Basic relation: [Type, field_name, prefix, collection_name]
478
+ ["User", "user_id", "myapp", "users"],
479
+
480
+ # Array relation
481
+ ["Role", "role_ids", "myapp", "roles"],
482
+
483
+ # Custom alias: [Type, field_name, prefix, collection_name, alias]
484
+ ["Department", "dept_id", "myapp", "departments", "department"],
485
+
486
+ # Special Users relation (built-in handling)
487
+ ["Users", "person", "", ""]
488
+ ]
489
+ ```
490
+
491
+ ### Custom Pipelines
492
+
493
+ ```python
494
+ # Custom base pipeline
495
+ base_pipeline = [
496
+ {"$match": {"deleted": False}},
497
+ {"$lookup": {...}}
498
+ ]
499
+
500
+ results = query_builder.get_query_response(
501
+ db_collection=collection,
502
+ query=query,
503
+ pipeline=base_pipeline, # Custom data pipeline
504
+ pipeline_count=custom_count_pipeline # Custom count pipeline
505
+ )
506
+ ```
507
+
508
+ ### Smart Text Search
509
+
510
+ The query builder automatically handles mixed-type searching:
511
+
512
+ ```python
513
+ # Search for "10" will match:
514
+ # - String fields containing "10"
515
+ # - Numeric fields equal to 10
516
+ # - Numeric fields in range 10.0-10.999
517
+ # - Numbers starting with 10 (100, 1000, etc.)
518
+ filters = {
519
+ "amount": {"$containsi": "10"}
520
+ }
521
+ ```
522
+
523
+ ## Dependencies
524
+
525
+ - `pymongo`: MongoDB driver
526
+ - `redis`: Redis client
527
+ - `fastapi`: For HTTP exceptions
528
+ - `bson`: For ObjectId handling
529
+
530
+ ## License
531
+
532
+ This package is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
533
+
534
+ ## Contributing
535
+
536
+ This package is developed and maintained by OpenLink SpA. For questions or support, please contact [contacto@openlink.cl](mailto:contacto@openlink.cl).
537
+
538
+ ## Repository
539
+
540
+ Source code is available at: [https://github.com/openlinkspa/oplnk-python-utils](https://github.com/openlinkspa/oplnk-python-utils)