oplnk-python-utils 1.1.3__tar.gz → 1.1.5__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.
Files changed (21) hide show
  1. {oplnk_python_utils-1.1.3/oplnk_python_utils.egg-info → oplnk_python_utils-1.1.5}/PKG-INFO +6 -121
  2. {oplnk_python_utils-1.1.3 → oplnk_python_utils-1.1.5}/README.md +5 -120
  3. {oplnk_python_utils-1.1.3 → oplnk_python_utils-1.1.5}/oplnk_python_utils/__init__.py +3 -2
  4. {oplnk_python_utils-1.1.3 → oplnk_python_utils-1.1.5}/oplnk_python_utils/cache/cache_utils.py +57 -5
  5. {oplnk_python_utils-1.1.3 → oplnk_python_utils-1.1.5}/oplnk_python_utils/core/parsing.py +11 -90
  6. {oplnk_python_utils-1.1.3 → oplnk_python_utils-1.1.5}/oplnk_python_utils/query/query_builder.py +30 -282
  7. {oplnk_python_utils-1.1.3 → oplnk_python_utils-1.1.5/oplnk_python_utils.egg-info}/PKG-INFO +6 -121
  8. {oplnk_python_utils-1.1.3 → oplnk_python_utils-1.1.5}/setup.py +2 -2
  9. {oplnk_python_utils-1.1.3 → oplnk_python_utils-1.1.5}/LICENSE +0 -0
  10. {oplnk_python_utils-1.1.3 → oplnk_python_utils-1.1.5}/MANIFEST.in +0 -0
  11. {oplnk_python_utils-1.1.3 → oplnk_python_utils-1.1.5}/oplnk_python_utils/cache/__init__.py +0 -0
  12. {oplnk_python_utils-1.1.3 → oplnk_python_utils-1.1.5}/oplnk_python_utils/core/__init__.py +0 -0
  13. {oplnk_python_utils-1.1.3 → oplnk_python_utils-1.1.5}/oplnk_python_utils/core/data.py +0 -0
  14. {oplnk_python_utils-1.1.3 → oplnk_python_utils-1.1.5}/oplnk_python_utils/core/serialization.py +0 -0
  15. {oplnk_python_utils-1.1.3 → oplnk_python_utils-1.1.5}/oplnk_python_utils/query/__init__.py +0 -0
  16. {oplnk_python_utils-1.1.3 → oplnk_python_utils-1.1.5}/oplnk_python_utils.egg-info/SOURCES.txt +0 -0
  17. {oplnk_python_utils-1.1.3 → oplnk_python_utils-1.1.5}/oplnk_python_utils.egg-info/dependency_links.txt +0 -0
  18. {oplnk_python_utils-1.1.3 → oplnk_python_utils-1.1.5}/oplnk_python_utils.egg-info/not-zip-safe +0 -0
  19. {oplnk_python_utils-1.1.3 → oplnk_python_utils-1.1.5}/oplnk_python_utils.egg-info/requires.txt +0 -0
  20. {oplnk_python_utils-1.1.3 → oplnk_python_utils-1.1.5}/oplnk_python_utils.egg-info/top_level.txt +0 -0
  21. {oplnk_python_utils-1.1.3 → oplnk_python_utils-1.1.5}/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
3
+ Version: 1.1.5
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
@@ -149,50 +149,15 @@ keys = parse_key_string_to_list("filters[name][$eq]")
149
149
 
150
150
  nested_dict = parse_list_to_dict(["filters", "name", "$eq"], "john")
151
151
  # Result: {"filters": {"name": {"$eq": "john"}}}
152
-
153
- # Enhanced field path handling for MongoDB aggregation (NEW)
154
- from oplnk_python_utils.core.parsing import (
155
- normalize_field_path,
156
- validate_field_path,
157
- build_aggregation_field_expression
158
- )
159
-
160
- # Normalize field paths for MongoDB aggregation
161
- normalized = normalize_field_path("establishment_type[0].name")
162
- # Result: "establishment_type.0.name" - MongoDB compatible path
163
-
164
- normalized = normalize_field_path("user.addresses[1].street")
165
- # Result: "user.addresses.1.street" - Array notation converted to dot notation
166
-
167
- # Validate field paths for security and correctness
168
- valid = validate_field_path("user.profile.name") # Returns: True
169
- valid = validate_field_path("user$invalid") # Returns: False
170
- valid = validate_field_path("user.settings.theme") # Returns: True
171
- valid = validate_field_path("") # Returns: False
172
-
173
- # Build proper aggregation field expressions
174
- field_expr = build_aggregation_field_expression("user.name")
175
- # Result: "$user.name" - Ready for MongoDB aggregation pipeline
176
-
177
- field_expr = build_aggregation_field_expression("establishment_type.0.name")
178
- # Result: "$establishment_type.0.name" - Handles normalized array paths
179
152
  ```
180
153
 
181
154
  #### Parsing Functions
182
155
 
183
- **Main Functions:**
184
-
185
156
  - **`parse_params_to_dict(params)`**: Parse FastAPI QueryParams or dict into structured nested dictionary
186
157
  - **`infer_type(value)`**: Smart type inference from string values (supports int, float, bool, datetime, lists)
187
158
  - **`parse_key_string_to_list(s)`**: Parse bracketed notation strings like `"filters[name][$eq]"`
188
159
  - **`parse_list_to_dict(lst, value)`**: Convert key list and value into nested dictionary structure
189
160
 
190
- **Field Path Utilities (NEW):**
191
-
192
- - **`normalize_field_path(field)`**: Normalize field paths for MongoDB aggregation with dot notation and array support
193
- - **`validate_field_path(field)`**: Validate if a field path is valid for MongoDB aggregation pipelines
194
- - **`build_aggregation_field_expression(field)`**: Build proper MongoDB aggregation field expressions with $ prefix
195
-
196
161
  ### Serialization Utilities (`core.serialization`)
197
162
 
198
163
  Functions for converting data types to serializable formats.
@@ -386,7 +351,7 @@ GET /users?clear_cache=true
386
351
 
387
352
  ## Query Module
388
353
 
389
- The query module provides a MongoDB aggregation pipeline builder with advanced features including enhanced support for nested fields, dot notation, and array notation.
354
+ The query module provides a MongoDB aggregation pipeline builder with advanced features.
390
355
 
391
356
  ### Usage
392
357
 
@@ -466,42 +431,16 @@ print(results)
466
431
  # "data": [...],
467
432
  # "meta": {"totalCount": 150}
468
433
  # }
469
-
470
- # Enhanced support for nested fields and array notation (NEW)
471
- nested_query = {
472
- "filters": {
473
- "user.profile.name": {"$containsi": "john"}, # Dot notation
474
- "establishment_type.0.name": {"$eq": "restaurant"}, # Array index notation
475
- "addresses[1].street": {"$startsWith": "Main"}, # Bracket notation (auto-normalized)
476
- "tags": {"$listContains": "featured"}, # Array contains
477
- "meta.settings.theme": {"$eq": "dark"} # Deep nested fields
478
- },
479
- "sort": [
480
- "user.profile.created_at:desc", # Sort by nested field
481
- "establishment_type.0.priority:asc" # Sort by array element
482
- ],
483
- "fields": [
484
- "user.profile.name", # Project nested fields
485
- "addresses.0.street", # Project specific array element
486
- "meta.settings" # Project nested objects
487
- ]
488
- }
489
-
490
- # All field paths are automatically validated and normalized for MongoDB compatibility
491
- results = query_builder.get_query_response(
492
- db_collection=collection,
493
- query=nested_query
494
- )
495
434
  ```
496
435
 
497
436
  ### QueryBuilder Methods
498
437
 
499
438
  #### Pipeline Builders
500
439
 
501
- - **`build_filter_pipeline(filters)`**: Build MongoDB filter pipeline with enhanced support for nested fields and array notation
502
- - **`build_sort_pipeline(sort_params)`**: Build sort pipeline with dot notation and array index support
503
- - **`build_fields_pipeline(fields_params)`**: Build field projection pipeline with nested field validation
504
- - **`build_distinct_pipeline(distinct_params)`**: Build distinct pipeline with proper field expression handling
440
+ - **`build_filter_pipeline(filters)`**: Build MongoDB filter pipeline
441
+ - **`build_sort_pipeline(sort_params)`**: Build sort pipeline
442
+ - **`build_fields_pipeline(fields_params)`**: Build field projection pipeline
443
+ - **`build_distinct_pipeline(distinct_params)`**: Build distinct pipeline
505
444
  - **`build_pagination_pipeline(pagination_params)`**: Build pagination pipeline
506
445
  - **`build_populate_pipeline(populate_params)`**: Build population pipeline
507
446
 
@@ -520,7 +459,6 @@ results = query_builder.get_query_response(
520
459
  The QueryBuilder class leverages utilities from the core module for enhanced functionality:
521
460
 
522
461
  - **Parameter parsing**: Uses `core.parsing.parse_params_to_dict()` for automatic query parameter parsing
523
- - **Field path processing**: Uses `core.parsing.normalize_field_path()`, `validate_field_path()`, and `build_aggregation_field_expression()` for safe nested field handling
524
462
  - **Data normalization**: Uses `core.data.normalize()` for consistent cache key generation
525
463
  - **Datetime serialization**: Uses `core.serialization.datetime_serializer()` for cache-friendly data
526
464
 
@@ -659,59 +597,6 @@ async def create_user(request: Request, user_data: dict):
659
597
 
660
598
  ## Advanced Features
661
599
 
662
- ### Enhanced Nested Field Support (NEW)
663
-
664
- The query builder now provides comprehensive support for nested fields, dot notation, and array notation with automatic validation and normalization.
665
-
666
- #### Supported Field Notations
667
-
668
- ```python
669
- # Dot notation for nested objects
670
- "user.profile.name" # Access nested object properties
671
- "settings.theme.colors.primary" # Deep nesting support
672
-
673
- # Array notation (both styles supported)
674
- "addresses[0].street" # Bracket notation - automatically normalized
675
- "addresses.0.street" # Dot notation - MongoDB native format
676
- "items[2].details.price" # Mixed notation support
677
-
678
- # Complex nested structures
679
- "metadata.tags[1].category.name" # Arrays within nested objects
680
- "stores.0.products[5].variants.color" # Multiple levels of nesting
681
- ```
682
-
683
- #### Automatic Field Validation
684
-
685
- All field paths are automatically validated for:
686
-
687
- - **Security**: Prevents injection of invalid MongoDB operators
688
- - **Syntax**: Ensures MongoDB-compatible field syntax
689
- - **Normalization**: Converts bracket notation to dot notation
690
- - **Error Handling**: Graceful handling of invalid paths with logging
691
-
692
- ```python
693
- # Examples of validation in action
694
- query = {
695
- "filters": {
696
- "user.profile.name": {"$eq": "John"}, # ✅ Valid - passes through
697
- "user$invalid": {"$eq": "test"}, # ❌ Invalid - skipped with warning
698
- "settings[0].theme": {"$eq": "dark"}, # ✅ Valid - normalized to "settings.0.theme"
699
- "": {"$eq": "empty"}, # ❌ Invalid - skipped with warning
700
- "normal_field": {"$eq": "value"} # ✅ Valid - standard field access
701
- }
702
- }
703
-
704
- # Invalid fields are automatically filtered out with helpful logging
705
- # Valid fields continue to work normally
706
- ```
707
-
708
- #### Performance Optimizations
709
-
710
- - **Smart Field Processing**: Only processes fields that need normalization
711
- - **Validation Caching**: Avoids repeated validation of the same field paths
712
- - **Error Recovery**: Continues processing even when individual fields are invalid
713
- - **Minimal Overhead**: Validation adds negligible performance impact
714
-
715
600
  ### Custom Relation Definitions
716
601
 
717
602
  ```python
@@ -107,50 +107,15 @@ keys = parse_key_string_to_list("filters[name][$eq]")
107
107
 
108
108
  nested_dict = parse_list_to_dict(["filters", "name", "$eq"], "john")
109
109
  # Result: {"filters": {"name": {"$eq": "john"}}}
110
-
111
- # Enhanced field path handling for MongoDB aggregation (NEW)
112
- from oplnk_python_utils.core.parsing import (
113
- normalize_field_path,
114
- validate_field_path,
115
- build_aggregation_field_expression
116
- )
117
-
118
- # Normalize field paths for MongoDB aggregation
119
- normalized = normalize_field_path("establishment_type[0].name")
120
- # Result: "establishment_type.0.name" - MongoDB compatible path
121
-
122
- normalized = normalize_field_path("user.addresses[1].street")
123
- # Result: "user.addresses.1.street" - Array notation converted to dot notation
124
-
125
- # Validate field paths for security and correctness
126
- valid = validate_field_path("user.profile.name") # Returns: True
127
- valid = validate_field_path("user$invalid") # Returns: False
128
- valid = validate_field_path("user.settings.theme") # Returns: True
129
- valid = validate_field_path("") # Returns: False
130
-
131
- # Build proper aggregation field expressions
132
- field_expr = build_aggregation_field_expression("user.name")
133
- # Result: "$user.name" - Ready for MongoDB aggregation pipeline
134
-
135
- field_expr = build_aggregation_field_expression("establishment_type.0.name")
136
- # Result: "$establishment_type.0.name" - Handles normalized array paths
137
110
  ```
138
111
 
139
112
  #### Parsing Functions
140
113
 
141
- **Main Functions:**
142
-
143
114
  - **`parse_params_to_dict(params)`**: Parse FastAPI QueryParams or dict into structured nested dictionary
144
115
  - **`infer_type(value)`**: Smart type inference from string values (supports int, float, bool, datetime, lists)
145
116
  - **`parse_key_string_to_list(s)`**: Parse bracketed notation strings like `"filters[name][$eq]"`
146
117
  - **`parse_list_to_dict(lst, value)`**: Convert key list and value into nested dictionary structure
147
118
 
148
- **Field Path Utilities (NEW):**
149
-
150
- - **`normalize_field_path(field)`**: Normalize field paths for MongoDB aggregation with dot notation and array support
151
- - **`validate_field_path(field)`**: Validate if a field path is valid for MongoDB aggregation pipelines
152
- - **`build_aggregation_field_expression(field)`**: Build proper MongoDB aggregation field expressions with $ prefix
153
-
154
119
  ### Serialization Utilities (`core.serialization`)
155
120
 
156
121
  Functions for converting data types to serializable formats.
@@ -344,7 +309,7 @@ GET /users?clear_cache=true
344
309
 
345
310
  ## Query Module
346
311
 
347
- The query module provides a MongoDB aggregation pipeline builder with advanced features including enhanced support for nested fields, dot notation, and array notation.
312
+ The query module provides a MongoDB aggregation pipeline builder with advanced features.
348
313
 
349
314
  ### Usage
350
315
 
@@ -424,42 +389,16 @@ print(results)
424
389
  # "data": [...],
425
390
  # "meta": {"totalCount": 150}
426
391
  # }
427
-
428
- # Enhanced support for nested fields and array notation (NEW)
429
- nested_query = {
430
- "filters": {
431
- "user.profile.name": {"$containsi": "john"}, # Dot notation
432
- "establishment_type.0.name": {"$eq": "restaurant"}, # Array index notation
433
- "addresses[1].street": {"$startsWith": "Main"}, # Bracket notation (auto-normalized)
434
- "tags": {"$listContains": "featured"}, # Array contains
435
- "meta.settings.theme": {"$eq": "dark"} # Deep nested fields
436
- },
437
- "sort": [
438
- "user.profile.created_at:desc", # Sort by nested field
439
- "establishment_type.0.priority:asc" # Sort by array element
440
- ],
441
- "fields": [
442
- "user.profile.name", # Project nested fields
443
- "addresses.0.street", # Project specific array element
444
- "meta.settings" # Project nested objects
445
- ]
446
- }
447
-
448
- # All field paths are automatically validated and normalized for MongoDB compatibility
449
- results = query_builder.get_query_response(
450
- db_collection=collection,
451
- query=nested_query
452
- )
453
392
  ```
454
393
 
455
394
  ### QueryBuilder Methods
456
395
 
457
396
  #### Pipeline Builders
458
397
 
459
- - **`build_filter_pipeline(filters)`**: Build MongoDB filter pipeline with enhanced support for nested fields and array notation
460
- - **`build_sort_pipeline(sort_params)`**: Build sort pipeline with dot notation and array index support
461
- - **`build_fields_pipeline(fields_params)`**: Build field projection pipeline with nested field validation
462
- - **`build_distinct_pipeline(distinct_params)`**: Build distinct pipeline with proper field expression handling
398
+ - **`build_filter_pipeline(filters)`**: Build MongoDB filter pipeline
399
+ - **`build_sort_pipeline(sort_params)`**: Build sort pipeline
400
+ - **`build_fields_pipeline(fields_params)`**: Build field projection pipeline
401
+ - **`build_distinct_pipeline(distinct_params)`**: Build distinct pipeline
463
402
  - **`build_pagination_pipeline(pagination_params)`**: Build pagination pipeline
464
403
  - **`build_populate_pipeline(populate_params)`**: Build population pipeline
465
404
 
@@ -478,7 +417,6 @@ results = query_builder.get_query_response(
478
417
  The QueryBuilder class leverages utilities from the core module for enhanced functionality:
479
418
 
480
419
  - **Parameter parsing**: Uses `core.parsing.parse_params_to_dict()` for automatic query parameter parsing
481
- - **Field path processing**: Uses `core.parsing.normalize_field_path()`, `validate_field_path()`, and `build_aggregation_field_expression()` for safe nested field handling
482
420
  - **Data normalization**: Uses `core.data.normalize()` for consistent cache key generation
483
421
  - **Datetime serialization**: Uses `core.serialization.datetime_serializer()` for cache-friendly data
484
422
 
@@ -617,59 +555,6 @@ async def create_user(request: Request, user_data: dict):
617
555
 
618
556
  ## Advanced Features
619
557
 
620
- ### Enhanced Nested Field Support (NEW)
621
-
622
- The query builder now provides comprehensive support for nested fields, dot notation, and array notation with automatic validation and normalization.
623
-
624
- #### Supported Field Notations
625
-
626
- ```python
627
- # Dot notation for nested objects
628
- "user.profile.name" # Access nested object properties
629
- "settings.theme.colors.primary" # Deep nesting support
630
-
631
- # Array notation (both styles supported)
632
- "addresses[0].street" # Bracket notation - automatically normalized
633
- "addresses.0.street" # Dot notation - MongoDB native format
634
- "items[2].details.price" # Mixed notation support
635
-
636
- # Complex nested structures
637
- "metadata.tags[1].category.name" # Arrays within nested objects
638
- "stores.0.products[5].variants.color" # Multiple levels of nesting
639
- ```
640
-
641
- #### Automatic Field Validation
642
-
643
- All field paths are automatically validated for:
644
-
645
- - **Security**: Prevents injection of invalid MongoDB operators
646
- - **Syntax**: Ensures MongoDB-compatible field syntax
647
- - **Normalization**: Converts bracket notation to dot notation
648
- - **Error Handling**: Graceful handling of invalid paths with logging
649
-
650
- ```python
651
- # Examples of validation in action
652
- query = {
653
- "filters": {
654
- "user.profile.name": {"$eq": "John"}, # ✅ Valid - passes through
655
- "user$invalid": {"$eq": "test"}, # ❌ Invalid - skipped with warning
656
- "settings[0].theme": {"$eq": "dark"}, # ✅ Valid - normalized to "settings.0.theme"
657
- "": {"$eq": "empty"}, # ❌ Invalid - skipped with warning
658
- "normal_field": {"$eq": "value"} # ✅ Valid - standard field access
659
- }
660
- }
661
-
662
- # Invalid fields are automatically filtered out with helpful logging
663
- # Valid fields continue to work normally
664
- ```
665
-
666
- #### Performance Optimizations
667
-
668
- - **Smart Field Processing**: Only processes fields that need normalization
669
- - **Validation Caching**: Avoids repeated validation of the same field paths
670
- - **Error Recovery**: Continues processing even when individual fields are invalid
671
- - **Minimal Overhead**: Validation adds negligible performance impact
672
-
673
558
  ### Custom Relation Definitions
674
559
 
675
560
  ```python
@@ -6,8 +6,9 @@ OpenLink Python Utils - Reusable utilities for caching, data manipulation, parsi
6
6
  from . import core
7
7
  from . import cache
8
8
  from . import query
9
+ from . import reports
9
10
 
10
- __version__ = "1.1.3"
11
+ __version__ = "1.1.5"
11
12
  __author__ = "OpenLink SpA"
12
13
 
13
- __all__ = ["core", "cache", "query", "__version__"]
14
+ __all__ = ["core", "cache", "query", "reports", "__version__"]
@@ -98,12 +98,15 @@ def invalidate_cache_keys(redis_client, pattern: str) -> int:
98
98
  return total_deleted
99
99
 
100
100
 
101
- def build_cache_key(
101
+ async def build_cache_key(
102
102
  request: Request, api_name: str, collection: str, endpoint_type: str
103
103
  ) -> str:
104
104
  """
105
105
  Builds a cache key based on the request, api name, collection, and endpoint type.
106
106
 
107
+ Incorporates HTTP method, URL path, path parameters and query parameters to
108
+ avoid collisions between routes that differ only by path params.
109
+
107
110
  Args:
108
111
  request: FastAPI request object
109
112
  api_name: Name of the API
@@ -113,8 +116,45 @@ def build_cache_key(
113
116
  Returns:
114
117
  Cache key string
115
118
  """
116
- query_hashed = generate_cache_id(request.query_params)
117
- return f"{api_name}:{collection}:{endpoint_type}:{query_hashed}"
119
+ # Ensure plain dicts for hashing
120
+ query_params_dict = dict(request.query_params) if request.query_params else {}
121
+ path_params_dict = (
122
+ dict(request.path_params) if getattr(request, "path_params", None) else {}
123
+ )
124
+
125
+ key_material = {
126
+ "method": request.method,
127
+ "path": request.url.path,
128
+ "path_params": path_params_dict,
129
+ "query": query_params_dict,
130
+ }
131
+
132
+ # Include request body for methods where it affects the response identity
133
+ if request.method in {"POST", "PUT"}:
134
+ content_type = (request.headers.get("content-type") or "").split(";")[0].lower()
135
+ body_hash: Optional[str] = None
136
+ if content_type == "application/json":
137
+ try:
138
+ json_body = await request.json()
139
+ normalized_body = normalize(json_body)
140
+ encoded_body = jsonable_encoder(
141
+ normalized_body, custom_encoder={ObjectId: str}
142
+ )
143
+ key_material["body"] = encoded_body
144
+ except Exception:
145
+ body_bytes = await request.body()
146
+ if body_bytes:
147
+ body_hash = hashlib.sha256(body_bytes).hexdigest()
148
+ else:
149
+ body_bytes = await request.body()
150
+ if body_bytes:
151
+ body_hash = hashlib.sha256(body_bytes).hexdigest()
152
+
153
+ if body_hash:
154
+ key_material["body_hash"] = body_hash
155
+
156
+ hashed = generate_cache_id(key_material)
157
+ return f"{api_name}:{collection}:{endpoint_type}:{hashed}"
118
158
 
119
159
 
120
160
  def cache_response(
@@ -159,11 +199,23 @@ def cache_response(
159
199
  raise ValueError("Request object not found in endpoint parameters.")
160
200
 
161
201
  query_params = request.query_params
162
- if query_params.get("clear_cache", False):
202
+ clear_cache_value = query_params.get("clear_cache")
203
+ if isinstance(clear_cache_value, str):
204
+ clear_cache_flag = clear_cache_value.lower() in {
205
+ "1",
206
+ "true",
207
+ "yes",
208
+ "y",
209
+ }
210
+ else:
211
+ clear_cache_flag = bool(clear_cache_value)
212
+ if clear_cache_flag:
163
213
  invalidate_tag(redis_client, f"{collection}")
164
214
 
165
215
  # Construir clave
166
- cache_key = build_cache_key(request, api_name, collection, endpoint_type)
216
+ cache_key = await build_cache_key(
217
+ request, api_name, collection, endpoint_type
218
+ )
167
219
  if redis_client.exists(cache_key):
168
220
  return json.loads(redis_client.get(cache_key))
169
221
 
@@ -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, "items"):
105
- param_dict = dict(params.items()) if hasattr(params, "items") else dict(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,83 +111,4 @@ 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
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
114
+ return result
@@ -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
7
6
  from copy import deepcopy
8
- from typing import List, Dict, Any, Optional, Union
7
+ from typing import List, Dict, Any, Optional, Union, Callable
8
+ from datetime import datetime
9
+ from bson import ObjectId
9
10
  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,
14
17
  parse_params_to_dict,
15
- normalize_field_path,
16
- validate_field_path,
17
- build_aggregation_field_expression,
18
18
  )
19
19
 
20
20
 
@@ -46,7 +46,6 @@ class QueryBuilder:
46
46
  cache_client=None,
47
47
  cache_key_prefix: str = "",
48
48
  clear_cache: bool = False,
49
- debug: bool = False,
50
49
  ) -> Dict[str, Any]:
51
50
  """
52
51
  Complete query method that handles parameter parsing and query execution.
@@ -61,25 +60,8 @@ class QueryBuilder:
61
60
  cache_client: Cache client (Redis)
62
61
  cache_key_prefix: Prefix for cache keys
63
62
  clear_cache: Whether to clear cache before query
64
- debug: Whether to enable debug output
65
63
  """
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
-
81
64
  # Parse params if provided, otherwise use query directly
82
- parsing_start_time = time.time()
83
65
  if params is not None:
84
66
  parsed_query = parse_params_to_dict(params)
85
67
  elif query is not None:
@@ -91,13 +73,6 @@ class QueryBuilder:
91
73
  if "clear_cache" in parsed_query:
92
74
  clear_cache = parsed_query.pop("clear_cache", False)
93
75
 
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
-
101
76
  return self.get_query_response(
102
77
  db_collection,
103
78
  parsed_query,
@@ -107,9 +82,6 @@ class QueryBuilder:
107
82
  cache_key_prefix=cache_key_prefix,
108
83
  use_cache=use_cache,
109
84
  clear_cache=clear_cache,
110
- debug=debug,
111
- start_time=start_time,
112
- parsing_time=parsing_time,
113
85
  )
114
86
 
115
87
  def build_filter_pipeline(self, filters: Dict[str, Any]) -> Dict[str, Any]:
@@ -449,9 +421,6 @@ class QueryBuilder:
449
421
  field_name = list(condition.keys())[0]
450
422
  field_conditions = condition[field_name]
451
423
 
452
- # Normalize field name for dot notation support
453
- normalized_field_name = normalize_field_path(field_name)
454
-
455
424
  if isinstance(field_conditions, dict):
456
425
  operator = list(field_conditions.keys())[0]
457
426
  value = field_conditions[operator]
@@ -474,68 +443,38 @@ class QueryBuilder:
474
443
  # Expand this condition into multiple alternatives
475
444
  value_type, str_value = get_value_type_and_string(value)
476
445
  expanded_conditions = expand_text_search_condition(
477
- normalized_field_name, operator, str_value
446
+ field_name, operator, str_value
478
447
  )
479
448
 
480
449
  # Add all expanded conditions to the $or
481
450
  or_conditions.extend(expanded_conditions)
482
451
  else:
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
- )
452
+ # Regular condition, process normally
453
+ sub_pipeline = self.build_filter_pipeline(condition)
490
454
  or_conditions.append(sub_pipeline)
491
455
  else:
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
- )
456
+ # Regular condition, process normally
457
+ sub_pipeline = self.build_filter_pipeline(condition)
499
458
  or_conditions.append(sub_pipeline)
500
459
 
501
460
  pipeline["$or"] = or_conditions
502
461
  case "$and":
503
462
  and_conditions = []
504
463
  for condition in conditions:
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)
464
+ sub_pipeline = self.build_filter_pipeline(condition)
514
465
  and_conditions.append(sub_pipeline)
515
466
  pipeline["$and"] = and_conditions
516
467
  case _:
517
468
  try:
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)
469
+ sub_pipeline = build_sub_pipeline(conditions, field)
529
470
 
530
471
  if isinstance(sub_pipeline, dict):
531
472
  if "_field" in sub_pipeline:
532
- new_field = (
533
- f"{normalized_field}.{sub_pipeline['_field']}"
534
- )
473
+ new_field = f"{field}.{sub_pipeline['_field']}"
535
474
  pipeline[new_field] = sub_pipeline["_conditions"]
536
475
  continue
537
476
 
538
- pipeline[normalized_field] = sub_pipeline
477
+ pipeline[field] = sub_pipeline
539
478
  except (ValueError, TypeError) as e:
540
479
  # Log the error and skip this filter condition
541
480
  print(f"Error processing filter for field '{field}': {e}")
@@ -544,64 +483,22 @@ class QueryBuilder:
544
483
  return pipeline
545
484
 
546
485
  def build_sort_pipeline(self, sort_params: List[str]) -> Dict[str, Any]:
547
- """
548
- Build sort pipeline from sort parameters with support for dot notation and array notation.
549
- """
486
+ """Build sort pipeline from sort parameters"""
550
487
  sort_pipeline = {}
551
488
  for item in sort_params:
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
-
489
+ field, order = item.split(":")
490
+ sort_pipeline[field] = 1 if order == "asc" else -1
567
491
  return {"$sort": sort_pipeline}
568
492
 
569
493
  def build_fields_pipeline(
570
494
  self, fields_params: List[str], populate_params: Optional[List] = None
571
495
  ) -> Dict[str, Any]:
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}")
496
+ """Build fields projection pipeline"""
497
+ fields_pipeline = {field: 1 for field in fields_params}
589
498
 
590
- # Process populate_params if provided
591
499
  if populate_params:
592
500
  for field in populate_params:
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
- )
501
+ fields_pipeline[field] = 1
605
502
 
606
503
  return {"$project": fields_pipeline}
607
504
 
@@ -611,62 +508,14 @@ class QueryBuilder:
611
508
  include_project: bool = True,
612
509
  pipeline: Optional[List] = None,
613
510
  ) -> List[Dict[str, Any]]:
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
- for field in distinct_params:
621
- try:
622
- # Use utility function to build proper aggregation expression
623
- field_expression = build_aggregation_field_expression(field)
624
-
625
- # Create nested structure for fields with dot notation
626
- parts = field.split(".")
627
- d = group_id
628
- for i, part in enumerate(parts[:-1]):
629
- # If a non-dict value is at the path, it can't be extended
630
- if part in d and not isinstance(d[part], dict):
631
- # To avoid overwriting, you could add a unique suffix, or handle as an error
632
- # For now, let's just print a warning and continue
633
- print(
634
- f"Warning: conflicting path in distinct fields for '{field}' at part '{part}'."
635
- )
636
- d = d.setdefault(f"{part}_{i}", {})
637
- else:
638
- d = d.setdefault(part, {})
639
-
640
- d[parts[-1]] = field_expression
641
-
642
- except ValueError as e:
643
- # Log error and skip invalid field
644
- print(f"Warning: Skipping invalid field '{field}' in distinct: {e}")
645
- continue
646
-
647
- if not group_id:
648
- raise ValueError("No valid fields provided for distinct operation")
649
-
650
- group_stage = {"$group": {"_id": group_id}}
651
-
652
- if include_project:
653
- replace_root_stage = {"$replaceRoot": {"newRoot": "$_id"}}
654
- return [group_stage, replace_root_stage]
655
- else:
656
- return [group_stage]
657
-
658
- except Exception as e:
659
- # Fallback to original behavior for backwards compatibility
660
- print(
661
- f"Warning: Error in enhanced distinct pipeline, falling back to original: {e}"
662
- )
663
- group_id = {field: f"${field}" for field in distinct_params}
664
- group_stage = {"$group": {"_id": group_id}}
665
- if include_project:
666
- replace_root_stage = {"$replaceRoot": {"newRoot": "$_id"}}
667
- return [group_stage, replace_root_stage]
668
- else:
669
- return [group_stage]
511
+ """Build distinct pipeline"""
512
+ group_id = {field: f"${field}" for field in distinct_params}
513
+ group_stage = {"$group": {"_id": group_id}}
514
+ if include_project:
515
+ replace_root_stage = {"$replaceRoot": {"newRoot": "$_id"}}
516
+ return [group_stage, replace_root_stage]
517
+ else:
518
+ return [group_stage]
670
519
 
671
520
  def build_pagination_pipeline(
672
521
  self, pagination_params: Dict[str, Any], pipeline: Optional[List] = None
@@ -1034,9 +883,6 @@ class QueryBuilder:
1034
883
  cache_key_prefix: str = "",
1035
884
  use_cache: bool = False,
1036
885
  clear_cache: bool = False,
1037
- debug: bool = False,
1038
- start_time: float = 0,
1039
- parsing_time: float = 0,
1040
886
  ) -> Dict[str, Any]:
1041
887
  """
1042
888
  Get query response with optional caching support
@@ -1050,64 +896,16 @@ class QueryBuilder:
1050
896
  cache_key_prefix: Prefix for cache keys
1051
897
  use_cache: Whether to use caching
1052
898
  clear_cache: Whether to clear cache before query
1053
- debug: Whether to enable debug output
1054
- start_time: Start time of the query
1055
- parsing_time: Time spent parsing parameters
1056
899
  """
1057
900
  try:
1058
- # Build pipelines
1059
- pipeline_build_start = time.time()
1060
901
  base_pipeline = self.make_aggregation_query(query, pipeline)
1061
902
  count_pipeline = self.make_count_aggregation_query(query, pipeline_count)
1062
- pipeline_build_time = time.time() - pipeline_build_start
1063
-
1064
- if debug:
1065
- print("🔵 [DEBUG] Pipeline de agregación construido:")
1066
- pprint(base_pipeline)
1067
- print("🔵 [DEBUG] Pipeline de count construido:")
1068
- pprint(count_pipeline)
1069
-
1070
- # Debug filters
1071
- if "filters" in query:
1072
- filter_pipeline = self.build_filter_pipeline(query["filters"])
1073
- print("🔵 [DEBUG] Filtros aplicados:")
1074
- pprint(filter_pipeline)
1075
-
1076
- # Debug special operations
1077
- if "populate" in query:
1078
- print("🔵 [DEBUG] Relaciones a popular:")
1079
- pprint(query["populate"])
1080
-
1081
- if "distinct" in query:
1082
- print("🔵 [DEBUG] Campos distinct:")
1083
- pprint(query["distinct"])
1084
-
1085
- if "sort" in query:
1086
- print("🔵 [DEBUG] Ordenamiento aplicado:")
1087
- pprint(query["sort"])
1088
-
1089
- if "pagination" in query:
1090
- print("🔵 [DEBUG] Paginación aplicada:")
1091
- pprint(query["pagination"])
1092
-
1093
- # Start MongoDB query timing
1094
- mongodb_start_time = time.time()
1095
903
 
1096
904
  # Get data
1097
905
  data_result = list(db_collection.aggregate(base_pipeline))
1098
906
 
1099
907
  # Get count
1100
908
  if use_cache and cache_client:
1101
- if debug:
1102
- print("🔵 [DEBUG] Configuración de cache:")
1103
- pprint(
1104
- {
1105
- "cache_key_prefix": cache_key_prefix,
1106
- "clear_cache": clear_cache,
1107
- "use_cache": use_cache,
1108
- }
1109
- )
1110
-
1111
909
  # Import cache utilities
1112
910
  from ..cache import (
1113
911
  generate_cache_id,
@@ -1121,10 +919,6 @@ class QueryBuilder:
1121
919
  query_hash = generate_cache_id(query_for_hash)
1122
920
  count_key = f"{cache_key_prefix}:query:count:{query_hash}"
1123
921
 
1124
- if debug:
1125
- print("🔵 [DEBUG] Cache key generada:")
1126
- print(f"count_key: {count_key}")
1127
-
1128
922
  # Get or set meta from cache
1129
923
  meta_result = get_or_set_cache(
1130
924
  cache_client,
@@ -1141,54 +935,8 @@ class QueryBuilder:
1141
935
  else:
1142
936
  meta_result = list(db_collection.aggregate(count_pipeline))
1143
937
 
1144
- # End MongoDB query timing
1145
- mongodb_time = time.time() - mongodb_start_time
1146
-
1147
- if debug:
1148
- print("🔵 [DEBUG] Resultado de query de datos:")
1149
- print(f"Registros encontrados: {len(data_result)}")
1150
- if data_result:
1151
- print("Muestra del primer registro:")
1152
- pprint(data_result[0])
1153
- if len(data_result) > 1:
1154
- print(f"... y {len(data_result) - 1} registros más")
1155
- else:
1156
- print("Sin datos encontrados")
1157
-
1158
- print("🔵 [DEBUG] Resultado de count:")
1159
- pprint(meta_result)
1160
-
1161
- result = self.sanitize_result(query, data_result, meta_result)
1162
-
1163
- if debug:
1164
- # Calculate total time
1165
- total_time = time.time() - start_time
1166
-
1167
- print("🔵 [DEBUG] Respuesta final sanitizada:")
1168
- preview_data = result.get("data", [])
1169
- data_preview = {
1170
- "data_count": len(preview_data),
1171
- "first_record": preview_data[0] if preview_data else None,
1172
- "remaining_records": (
1173
- f"... y {len(preview_data) - 1} más"
1174
- if len(preview_data) > 1
1175
- else "ninguno más"
1176
- ),
1177
- "meta": result.get("meta", {}),
1178
- "has_pagination": "pagination" in query,
1179
- }
1180
- pprint(data_preview)
1181
-
1182
- print("🔵 [DEBUG] ⏱️ MÉTRICAS DE PERFORMANCE:")
1183
- print(f" 📊 Tiempo de parsing/pre-consulta: {parsing_time:.3f}s")
1184
- print(f" 🗄️ Tiempo real de consulta MongoDB: {mongodb_time:.3f}s")
1185
- print(f" 🚀 Tiempo total de procesamiento: {total_time:.3f}s")
1186
- print("=" * 60)
1187
-
1188
- return result
938
+ return self.sanitize_result(query, data_result, meta_result)
1189
939
  except Exception as e:
1190
- if debug:
1191
- print(f"🔴 [DEBUG] Error en get_query_response: {str(e)}")
1192
940
  raise HTTPException(
1193
941
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
1194
942
  detail=str(e),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oplnk-python-utils
3
- Version: 1.1.3
3
+ Version: 1.1.5
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
@@ -149,50 +149,15 @@ keys = parse_key_string_to_list("filters[name][$eq]")
149
149
 
150
150
  nested_dict = parse_list_to_dict(["filters", "name", "$eq"], "john")
151
151
  # Result: {"filters": {"name": {"$eq": "john"}}}
152
-
153
- # Enhanced field path handling for MongoDB aggregation (NEW)
154
- from oplnk_python_utils.core.parsing import (
155
- normalize_field_path,
156
- validate_field_path,
157
- build_aggregation_field_expression
158
- )
159
-
160
- # Normalize field paths for MongoDB aggregation
161
- normalized = normalize_field_path("establishment_type[0].name")
162
- # Result: "establishment_type.0.name" - MongoDB compatible path
163
-
164
- normalized = normalize_field_path("user.addresses[1].street")
165
- # Result: "user.addresses.1.street" - Array notation converted to dot notation
166
-
167
- # Validate field paths for security and correctness
168
- valid = validate_field_path("user.profile.name") # Returns: True
169
- valid = validate_field_path("user$invalid") # Returns: False
170
- valid = validate_field_path("user.settings.theme") # Returns: True
171
- valid = validate_field_path("") # Returns: False
172
-
173
- # Build proper aggregation field expressions
174
- field_expr = build_aggregation_field_expression("user.name")
175
- # Result: "$user.name" - Ready for MongoDB aggregation pipeline
176
-
177
- field_expr = build_aggregation_field_expression("establishment_type.0.name")
178
- # Result: "$establishment_type.0.name" - Handles normalized array paths
179
152
  ```
180
153
 
181
154
  #### Parsing Functions
182
155
 
183
- **Main Functions:**
184
-
185
156
  - **`parse_params_to_dict(params)`**: Parse FastAPI QueryParams or dict into structured nested dictionary
186
157
  - **`infer_type(value)`**: Smart type inference from string values (supports int, float, bool, datetime, lists)
187
158
  - **`parse_key_string_to_list(s)`**: Parse bracketed notation strings like `"filters[name][$eq]"`
188
159
  - **`parse_list_to_dict(lst, value)`**: Convert key list and value into nested dictionary structure
189
160
 
190
- **Field Path Utilities (NEW):**
191
-
192
- - **`normalize_field_path(field)`**: Normalize field paths for MongoDB aggregation with dot notation and array support
193
- - **`validate_field_path(field)`**: Validate if a field path is valid for MongoDB aggregation pipelines
194
- - **`build_aggregation_field_expression(field)`**: Build proper MongoDB aggregation field expressions with $ prefix
195
-
196
161
  ### Serialization Utilities (`core.serialization`)
197
162
 
198
163
  Functions for converting data types to serializable formats.
@@ -386,7 +351,7 @@ GET /users?clear_cache=true
386
351
 
387
352
  ## Query Module
388
353
 
389
- The query module provides a MongoDB aggregation pipeline builder with advanced features including enhanced support for nested fields, dot notation, and array notation.
354
+ The query module provides a MongoDB aggregation pipeline builder with advanced features.
390
355
 
391
356
  ### Usage
392
357
 
@@ -466,42 +431,16 @@ print(results)
466
431
  # "data": [...],
467
432
  # "meta": {"totalCount": 150}
468
433
  # }
469
-
470
- # Enhanced support for nested fields and array notation (NEW)
471
- nested_query = {
472
- "filters": {
473
- "user.profile.name": {"$containsi": "john"}, # Dot notation
474
- "establishment_type.0.name": {"$eq": "restaurant"}, # Array index notation
475
- "addresses[1].street": {"$startsWith": "Main"}, # Bracket notation (auto-normalized)
476
- "tags": {"$listContains": "featured"}, # Array contains
477
- "meta.settings.theme": {"$eq": "dark"} # Deep nested fields
478
- },
479
- "sort": [
480
- "user.profile.created_at:desc", # Sort by nested field
481
- "establishment_type.0.priority:asc" # Sort by array element
482
- ],
483
- "fields": [
484
- "user.profile.name", # Project nested fields
485
- "addresses.0.street", # Project specific array element
486
- "meta.settings" # Project nested objects
487
- ]
488
- }
489
-
490
- # All field paths are automatically validated and normalized for MongoDB compatibility
491
- results = query_builder.get_query_response(
492
- db_collection=collection,
493
- query=nested_query
494
- )
495
434
  ```
496
435
 
497
436
  ### QueryBuilder Methods
498
437
 
499
438
  #### Pipeline Builders
500
439
 
501
- - **`build_filter_pipeline(filters)`**: Build MongoDB filter pipeline with enhanced support for nested fields and array notation
502
- - **`build_sort_pipeline(sort_params)`**: Build sort pipeline with dot notation and array index support
503
- - **`build_fields_pipeline(fields_params)`**: Build field projection pipeline with nested field validation
504
- - **`build_distinct_pipeline(distinct_params)`**: Build distinct pipeline with proper field expression handling
440
+ - **`build_filter_pipeline(filters)`**: Build MongoDB filter pipeline
441
+ - **`build_sort_pipeline(sort_params)`**: Build sort pipeline
442
+ - **`build_fields_pipeline(fields_params)`**: Build field projection pipeline
443
+ - **`build_distinct_pipeline(distinct_params)`**: Build distinct pipeline
505
444
  - **`build_pagination_pipeline(pagination_params)`**: Build pagination pipeline
506
445
  - **`build_populate_pipeline(populate_params)`**: Build population pipeline
507
446
 
@@ -520,7 +459,6 @@ results = query_builder.get_query_response(
520
459
  The QueryBuilder class leverages utilities from the core module for enhanced functionality:
521
460
 
522
461
  - **Parameter parsing**: Uses `core.parsing.parse_params_to_dict()` for automatic query parameter parsing
523
- - **Field path processing**: Uses `core.parsing.normalize_field_path()`, `validate_field_path()`, and `build_aggregation_field_expression()` for safe nested field handling
524
462
  - **Data normalization**: Uses `core.data.normalize()` for consistent cache key generation
525
463
  - **Datetime serialization**: Uses `core.serialization.datetime_serializer()` for cache-friendly data
526
464
 
@@ -659,59 +597,6 @@ async def create_user(request: Request, user_data: dict):
659
597
 
660
598
  ## Advanced Features
661
599
 
662
- ### Enhanced Nested Field Support (NEW)
663
-
664
- The query builder now provides comprehensive support for nested fields, dot notation, and array notation with automatic validation and normalization.
665
-
666
- #### Supported Field Notations
667
-
668
- ```python
669
- # Dot notation for nested objects
670
- "user.profile.name" # Access nested object properties
671
- "settings.theme.colors.primary" # Deep nesting support
672
-
673
- # Array notation (both styles supported)
674
- "addresses[0].street" # Bracket notation - automatically normalized
675
- "addresses.0.street" # Dot notation - MongoDB native format
676
- "items[2].details.price" # Mixed notation support
677
-
678
- # Complex nested structures
679
- "metadata.tags[1].category.name" # Arrays within nested objects
680
- "stores.0.products[5].variants.color" # Multiple levels of nesting
681
- ```
682
-
683
- #### Automatic Field Validation
684
-
685
- All field paths are automatically validated for:
686
-
687
- - **Security**: Prevents injection of invalid MongoDB operators
688
- - **Syntax**: Ensures MongoDB-compatible field syntax
689
- - **Normalization**: Converts bracket notation to dot notation
690
- - **Error Handling**: Graceful handling of invalid paths with logging
691
-
692
- ```python
693
- # Examples of validation in action
694
- query = {
695
- "filters": {
696
- "user.profile.name": {"$eq": "John"}, # ✅ Valid - passes through
697
- "user$invalid": {"$eq": "test"}, # ❌ Invalid - skipped with warning
698
- "settings[0].theme": {"$eq": "dark"}, # ✅ Valid - normalized to "settings.0.theme"
699
- "": {"$eq": "empty"}, # ❌ Invalid - skipped with warning
700
- "normal_field": {"$eq": "value"} # ✅ Valid - standard field access
701
- }
702
- }
703
-
704
- # Invalid fields are automatically filtered out with helpful logging
705
- # Valid fields continue to work normally
706
- ```
707
-
708
- #### Performance Optimizations
709
-
710
- - **Smart Field Processing**: Only processes fields that need normalization
711
- - **Validation Caching**: Avoids repeated validation of the same field paths
712
- - **Error Recovery**: Continues processing even when individual fields are invalid
713
- - **Minimal Overhead**: Validation adds negligible performance impact
714
-
715
600
  ### Custom Relation Definitions
716
601
 
717
602
  ```python
@@ -2,11 +2,11 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="oplnk-python-utils",
5
- version="1.1.3",
5
+ version="1.1.5",
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",
9
- long_description=open("README.md", encoding="utf-8").read(),
9
+ long_description=open("README.md").read(),
10
10
  long_description_content_type="text/markdown",
11
11
  url="https://github.com/openlinkspa/oplnk-python-utils",
12
12
  packages=find_packages(),