oplnk-python-utils 1.1.0__tar.gz → 1.1.2__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.
- {oplnk_python_utils-1.1.0/oplnk_python_utils.egg-info → oplnk_python_utils-1.1.2}/PKG-INFO +121 -6
- {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/README.md +120 -5
- {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/oplnk_python_utils/__init__.py +1 -1
- {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/oplnk_python_utils/core/parsing.py +90 -11
- {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/oplnk_python_utils/query/query_builder.py +267 -31
- {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2/oplnk_python_utils.egg-info}/PKG-INFO +121 -6
- {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/setup.py +2 -2
- {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/LICENSE +0 -0
- {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/MANIFEST.in +0 -0
- {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/oplnk_python_utils/cache/__init__.py +0 -0
- {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/oplnk_python_utils/cache/cache_utils.py +0 -0
- {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/oplnk_python_utils/core/__init__.py +0 -0
- {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/oplnk_python_utils/core/data.py +0 -0
- {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/oplnk_python_utils/core/serialization.py +0 -0
- {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/oplnk_python_utils/query/__init__.py +0 -0
- {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/oplnk_python_utils.egg-info/SOURCES.txt +0 -0
- {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/oplnk_python_utils.egg-info/dependency_links.txt +0 -0
- {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/oplnk_python_utils.egg-info/not-zip-safe +0 -0
- {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/oplnk_python_utils.egg-info/requires.txt +0 -0
- {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/oplnk_python_utils.egg-info/top_level.txt +0 -0
- {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/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
|
+
Version: 1.1.2
|
|
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,15 +149,50 @@ 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
|
|
152
179
|
```
|
|
153
180
|
|
|
154
181
|
#### Parsing Functions
|
|
155
182
|
|
|
183
|
+
**Main Functions:**
|
|
184
|
+
|
|
156
185
|
- **`parse_params_to_dict(params)`**: Parse FastAPI QueryParams or dict into structured nested dictionary
|
|
157
186
|
- **`infer_type(value)`**: Smart type inference from string values (supports int, float, bool, datetime, lists)
|
|
158
187
|
- **`parse_key_string_to_list(s)`**: Parse bracketed notation strings like `"filters[name][$eq]"`
|
|
159
188
|
- **`parse_list_to_dict(lst, value)`**: Convert key list and value into nested dictionary structure
|
|
160
189
|
|
|
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
|
+
|
|
161
196
|
### Serialization Utilities (`core.serialization`)
|
|
162
197
|
|
|
163
198
|
Functions for converting data types to serializable formats.
|
|
@@ -351,7 +386,7 @@ GET /users?clear_cache=true
|
|
|
351
386
|
|
|
352
387
|
## Query Module
|
|
353
388
|
|
|
354
|
-
The query module provides a MongoDB aggregation pipeline builder with advanced features.
|
|
389
|
+
The query module provides a MongoDB aggregation pipeline builder with advanced features including enhanced support for nested fields, dot notation, and array notation.
|
|
355
390
|
|
|
356
391
|
### Usage
|
|
357
392
|
|
|
@@ -431,16 +466,42 @@ print(results)
|
|
|
431
466
|
# "data": [...],
|
|
432
467
|
# "meta": {"totalCount": 150}
|
|
433
468
|
# }
|
|
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
|
+
)
|
|
434
495
|
```
|
|
435
496
|
|
|
436
497
|
### QueryBuilder Methods
|
|
437
498
|
|
|
438
499
|
#### Pipeline Builders
|
|
439
500
|
|
|
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
|
|
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
|
|
444
505
|
- **`build_pagination_pipeline(pagination_params)`**: Build pagination pipeline
|
|
445
506
|
- **`build_populate_pipeline(populate_params)`**: Build population pipeline
|
|
446
507
|
|
|
@@ -459,6 +520,7 @@ print(results)
|
|
|
459
520
|
The QueryBuilder class leverages utilities from the core module for enhanced functionality:
|
|
460
521
|
|
|
461
522
|
- **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
|
|
462
524
|
- **Data normalization**: Uses `core.data.normalize()` for consistent cache key generation
|
|
463
525
|
- **Datetime serialization**: Uses `core.serialization.datetime_serializer()` for cache-friendly data
|
|
464
526
|
|
|
@@ -597,6 +659,59 @@ async def create_user(request: Request, user_data: dict):
|
|
|
597
659
|
|
|
598
660
|
## Advanced Features
|
|
599
661
|
|
|
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
|
+
|
|
600
715
|
### Custom Relation Definitions
|
|
601
716
|
|
|
602
717
|
```python
|
|
@@ -107,15 +107,50 @@ 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
|
|
110
137
|
```
|
|
111
138
|
|
|
112
139
|
#### Parsing Functions
|
|
113
140
|
|
|
141
|
+
**Main Functions:**
|
|
142
|
+
|
|
114
143
|
- **`parse_params_to_dict(params)`**: Parse FastAPI QueryParams or dict into structured nested dictionary
|
|
115
144
|
- **`infer_type(value)`**: Smart type inference from string values (supports int, float, bool, datetime, lists)
|
|
116
145
|
- **`parse_key_string_to_list(s)`**: Parse bracketed notation strings like `"filters[name][$eq]"`
|
|
117
146
|
- **`parse_list_to_dict(lst, value)`**: Convert key list and value into nested dictionary structure
|
|
118
147
|
|
|
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
|
+
|
|
119
154
|
### Serialization Utilities (`core.serialization`)
|
|
120
155
|
|
|
121
156
|
Functions for converting data types to serializable formats.
|
|
@@ -309,7 +344,7 @@ GET /users?clear_cache=true
|
|
|
309
344
|
|
|
310
345
|
## Query Module
|
|
311
346
|
|
|
312
|
-
The query module provides a MongoDB aggregation pipeline builder with advanced features.
|
|
347
|
+
The query module provides a MongoDB aggregation pipeline builder with advanced features including enhanced support for nested fields, dot notation, and array notation.
|
|
313
348
|
|
|
314
349
|
### Usage
|
|
315
350
|
|
|
@@ -389,16 +424,42 @@ print(results)
|
|
|
389
424
|
# "data": [...],
|
|
390
425
|
# "meta": {"totalCount": 150}
|
|
391
426
|
# }
|
|
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
|
+
)
|
|
392
453
|
```
|
|
393
454
|
|
|
394
455
|
### QueryBuilder Methods
|
|
395
456
|
|
|
396
457
|
#### Pipeline Builders
|
|
397
458
|
|
|
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
|
|
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
|
|
402
463
|
- **`build_pagination_pipeline(pagination_params)`**: Build pagination pipeline
|
|
403
464
|
- **`build_populate_pipeline(populate_params)`**: Build population pipeline
|
|
404
465
|
|
|
@@ -417,6 +478,7 @@ print(results)
|
|
|
417
478
|
The QueryBuilder class leverages utilities from the core module for enhanced functionality:
|
|
418
479
|
|
|
419
480
|
- **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
|
|
420
482
|
- **Data normalization**: Uses `core.data.normalize()` for consistent cache key generation
|
|
421
483
|
- **Datetime serialization**: Uses `core.serialization.datetime_serializer()` for cache-friendly data
|
|
422
484
|
|
|
@@ -555,6 +617,59 @@ async def create_user(request: Request, user_data: dict):
|
|
|
555
617
|
|
|
556
618
|
## Advanced Features
|
|
557
619
|
|
|
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
|
+
|
|
558
673
|
### Custom Relation Definitions
|
|
559
674
|
|
|
560
675
|
```python
|
|
@@ -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,
|
|
105
|
-
param_dict = dict(params.items()) if hasattr(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,4 +111,83 @@ 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
|
|
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
|
{oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/oplnk_python_utils/query/query_builder.py
RENAMED
|
@@ -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
|
|
6
7
|
from copy import deepcopy
|
|
7
|
-
from typing import List, Dict, Any, Optional, Union
|
|
8
|
-
from datetime import datetime
|
|
9
|
-
from bson import ObjectId
|
|
8
|
+
from typing import List, Dict, Any, Optional, Union
|
|
10
9
|
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,
|
|
17
14
|
parse_params_to_dict,
|
|
15
|
+
normalize_field_path,
|
|
16
|
+
validate_field_path,
|
|
17
|
+
build_aggregation_field_expression,
|
|
18
18
|
)
|
|
19
19
|
|
|
20
20
|
|
|
@@ -46,6 +46,7 @@ class QueryBuilder:
|
|
|
46
46
|
cache_client=None,
|
|
47
47
|
cache_key_prefix: str = "",
|
|
48
48
|
clear_cache: bool = False,
|
|
49
|
+
debug: bool = False,
|
|
49
50
|
) -> Dict[str, Any]:
|
|
50
51
|
"""
|
|
51
52
|
Complete query method that handles parameter parsing and query execution.
|
|
@@ -60,8 +61,25 @@ class QueryBuilder:
|
|
|
60
61
|
cache_client: Cache client (Redis)
|
|
61
62
|
cache_key_prefix: Prefix for cache keys
|
|
62
63
|
clear_cache: Whether to clear cache before query
|
|
64
|
+
debug: Whether to enable debug output
|
|
63
65
|
"""
|
|
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
|
+
|
|
64
81
|
# Parse params if provided, otherwise use query directly
|
|
82
|
+
parsing_start_time = time.time()
|
|
65
83
|
if params is not None:
|
|
66
84
|
parsed_query = parse_params_to_dict(params)
|
|
67
85
|
elif query is not None:
|
|
@@ -73,7 +91,14 @@ class QueryBuilder:
|
|
|
73
91
|
if "clear_cache" in parsed_query:
|
|
74
92
|
clear_cache = parsed_query.pop("clear_cache", False)
|
|
75
93
|
|
|
76
|
-
|
|
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
|
+
return self.get_query_response(
|
|
77
102
|
db_collection,
|
|
78
103
|
parsed_query,
|
|
79
104
|
pipeline,
|
|
@@ -82,6 +107,9 @@ class QueryBuilder:
|
|
|
82
107
|
cache_key_prefix=cache_key_prefix,
|
|
83
108
|
use_cache=use_cache,
|
|
84
109
|
clear_cache=clear_cache,
|
|
110
|
+
debug=debug,
|
|
111
|
+
start_time=start_time,
|
|
112
|
+
parsing_time=parsing_time,
|
|
85
113
|
)
|
|
86
114
|
|
|
87
115
|
def build_filter_pipeline(self, filters: Dict[str, Any]) -> Dict[str, Any]:
|
|
@@ -421,6 +449,9 @@ class QueryBuilder:
|
|
|
421
449
|
field_name = list(condition.keys())[0]
|
|
422
450
|
field_conditions = condition[field_name]
|
|
423
451
|
|
|
452
|
+
# Normalize field name for dot notation support
|
|
453
|
+
normalized_field_name = normalize_field_path(field_name)
|
|
454
|
+
|
|
424
455
|
if isinstance(field_conditions, dict):
|
|
425
456
|
operator = list(field_conditions.keys())[0]
|
|
426
457
|
value = field_conditions[operator]
|
|
@@ -443,38 +474,68 @@ class QueryBuilder:
|
|
|
443
474
|
# Expand this condition into multiple alternatives
|
|
444
475
|
value_type, str_value = get_value_type_and_string(value)
|
|
445
476
|
expanded_conditions = expand_text_search_condition(
|
|
446
|
-
|
|
477
|
+
normalized_field_name, operator, str_value
|
|
447
478
|
)
|
|
448
479
|
|
|
449
480
|
# Add all expanded conditions to the $or
|
|
450
481
|
or_conditions.extend(expanded_conditions)
|
|
451
482
|
else:
|
|
452
|
-
# Regular condition, process normally
|
|
453
|
-
|
|
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
|
+
)
|
|
454
490
|
or_conditions.append(sub_pipeline)
|
|
455
491
|
else:
|
|
456
|
-
# Regular condition, process normally
|
|
457
|
-
|
|
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
|
+
)
|
|
458
499
|
or_conditions.append(sub_pipeline)
|
|
459
500
|
|
|
460
501
|
pipeline["$or"] = or_conditions
|
|
461
502
|
case "$and":
|
|
462
503
|
and_conditions = []
|
|
463
504
|
for condition in conditions:
|
|
464
|
-
|
|
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)
|
|
465
514
|
and_conditions.append(sub_pipeline)
|
|
466
515
|
pipeline["$and"] = and_conditions
|
|
467
516
|
case _:
|
|
468
517
|
try:
|
|
469
|
-
|
|
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)
|
|
470
529
|
|
|
471
530
|
if isinstance(sub_pipeline, dict):
|
|
472
531
|
if "_field" in sub_pipeline:
|
|
473
|
-
new_field =
|
|
532
|
+
new_field = (
|
|
533
|
+
f"{normalized_field}.{sub_pipeline['_field']}"
|
|
534
|
+
)
|
|
474
535
|
pipeline[new_field] = sub_pipeline["_conditions"]
|
|
475
536
|
continue
|
|
476
537
|
|
|
477
|
-
pipeline[
|
|
538
|
+
pipeline[normalized_field] = sub_pipeline
|
|
478
539
|
except (ValueError, TypeError) as e:
|
|
479
540
|
# Log the error and skip this filter condition
|
|
480
541
|
print(f"Error processing filter for field '{field}': {e}")
|
|
@@ -483,22 +544,64 @@ class QueryBuilder:
|
|
|
483
544
|
return pipeline
|
|
484
545
|
|
|
485
546
|
def build_sort_pipeline(self, sort_params: List[str]) -> Dict[str, Any]:
|
|
486
|
-
"""
|
|
547
|
+
"""
|
|
548
|
+
Build sort pipeline from sort parameters with support for dot notation and array notation.
|
|
549
|
+
"""
|
|
487
550
|
sort_pipeline = {}
|
|
488
551
|
for item in sort_params:
|
|
489
|
-
|
|
490
|
-
|
|
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
|
+
|
|
491
567
|
return {"$sort": sort_pipeline}
|
|
492
568
|
|
|
493
569
|
def build_fields_pipeline(
|
|
494
570
|
self, fields_params: List[str], populate_params: Optional[List] = None
|
|
495
571
|
) -> Dict[str, Any]:
|
|
496
|
-
"""
|
|
497
|
-
|
|
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}")
|
|
498
589
|
|
|
590
|
+
# Process populate_params if provided
|
|
499
591
|
if populate_params:
|
|
500
592
|
for field in populate_params:
|
|
501
|
-
|
|
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
|
+
)
|
|
502
605
|
|
|
503
606
|
return {"$project": fields_pipeline}
|
|
504
607
|
|
|
@@ -508,14 +611,46 @@ class QueryBuilder:
|
|
|
508
611
|
include_project: bool = True,
|
|
509
612
|
pipeline: Optional[List] = None,
|
|
510
613
|
) -> List[Dict[str, Any]]:
|
|
511
|
-
"""
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|
+
# Use original field name as key for the result
|
|
625
|
+
group_id[field] = field_expression
|
|
626
|
+
except ValueError as e:
|
|
627
|
+
# Log error and skip invalid field
|
|
628
|
+
print(f"Warning: Skipping invalid field '{field}' in distinct: {e}")
|
|
629
|
+
continue
|
|
630
|
+
|
|
631
|
+
if not group_id:
|
|
632
|
+
raise ValueError("No valid fields provided for distinct operation")
|
|
633
|
+
|
|
634
|
+
group_stage = {"$group": {"_id": group_id}}
|
|
635
|
+
|
|
636
|
+
if include_project:
|
|
637
|
+
replace_root_stage = {"$replaceRoot": {"newRoot": "$_id"}}
|
|
638
|
+
return [group_stage, replace_root_stage]
|
|
639
|
+
else:
|
|
640
|
+
return [group_stage]
|
|
641
|
+
|
|
642
|
+
except Exception as e:
|
|
643
|
+
# Fallback to original behavior for backwards compatibility
|
|
644
|
+
print(
|
|
645
|
+
f"Warning: Error in enhanced distinct pipeline, falling back to original: {e}"
|
|
646
|
+
)
|
|
647
|
+
group_id = {field: f"${field}" for field in distinct_params}
|
|
648
|
+
group_stage = {"$group": {"_id": group_id}}
|
|
649
|
+
if include_project:
|
|
650
|
+
replace_root_stage = {"$replaceRoot": {"newRoot": "$_id"}}
|
|
651
|
+
return [group_stage, replace_root_stage]
|
|
652
|
+
else:
|
|
653
|
+
return [group_stage]
|
|
519
654
|
|
|
520
655
|
def build_pagination_pipeline(
|
|
521
656
|
self, pagination_params: Dict[str, Any], pipeline: Optional[List] = None
|
|
@@ -883,6 +1018,9 @@ class QueryBuilder:
|
|
|
883
1018
|
cache_key_prefix: str = "",
|
|
884
1019
|
use_cache: bool = False,
|
|
885
1020
|
clear_cache: bool = False,
|
|
1021
|
+
debug: bool = False,
|
|
1022
|
+
start_time: float = 0,
|
|
1023
|
+
parsing_time: float = 0,
|
|
886
1024
|
) -> Dict[str, Any]:
|
|
887
1025
|
"""
|
|
888
1026
|
Get query response with optional caching support
|
|
@@ -896,16 +1034,64 @@ class QueryBuilder:
|
|
|
896
1034
|
cache_key_prefix: Prefix for cache keys
|
|
897
1035
|
use_cache: Whether to use caching
|
|
898
1036
|
clear_cache: Whether to clear cache before query
|
|
1037
|
+
debug: Whether to enable debug output
|
|
1038
|
+
start_time: Start time of the query
|
|
1039
|
+
parsing_time: Time spent parsing parameters
|
|
899
1040
|
"""
|
|
900
1041
|
try:
|
|
1042
|
+
# Build pipelines
|
|
1043
|
+
pipeline_build_start = time.time()
|
|
901
1044
|
base_pipeline = self.make_aggregation_query(query, pipeline)
|
|
902
1045
|
count_pipeline = self.make_count_aggregation_query(query, pipeline_count)
|
|
1046
|
+
pipeline_build_time = time.time() - pipeline_build_start
|
|
1047
|
+
|
|
1048
|
+
if debug:
|
|
1049
|
+
print("🔵 [DEBUG] Pipeline de agregación construido:")
|
|
1050
|
+
pprint(base_pipeline)
|
|
1051
|
+
print("🔵 [DEBUG] Pipeline de count construido:")
|
|
1052
|
+
pprint(count_pipeline)
|
|
1053
|
+
|
|
1054
|
+
# Debug filters
|
|
1055
|
+
if "filters" in query:
|
|
1056
|
+
filter_pipeline = self.build_filter_pipeline(query["filters"])
|
|
1057
|
+
print("🔵 [DEBUG] Filtros aplicados:")
|
|
1058
|
+
pprint(filter_pipeline)
|
|
1059
|
+
|
|
1060
|
+
# Debug special operations
|
|
1061
|
+
if "populate" in query:
|
|
1062
|
+
print("🔵 [DEBUG] Relaciones a popular:")
|
|
1063
|
+
pprint(query["populate"])
|
|
1064
|
+
|
|
1065
|
+
if "distinct" in query:
|
|
1066
|
+
print("🔵 [DEBUG] Campos distinct:")
|
|
1067
|
+
pprint(query["distinct"])
|
|
1068
|
+
|
|
1069
|
+
if "sort" in query:
|
|
1070
|
+
print("🔵 [DEBUG] Ordenamiento aplicado:")
|
|
1071
|
+
pprint(query["sort"])
|
|
1072
|
+
|
|
1073
|
+
if "pagination" in query:
|
|
1074
|
+
print("🔵 [DEBUG] Paginación aplicada:")
|
|
1075
|
+
pprint(query["pagination"])
|
|
1076
|
+
|
|
1077
|
+
# Start MongoDB query timing
|
|
1078
|
+
mongodb_start_time = time.time()
|
|
903
1079
|
|
|
904
1080
|
# Get data
|
|
905
1081
|
data_result = list(db_collection.aggregate(base_pipeline))
|
|
906
1082
|
|
|
907
1083
|
# Get count
|
|
908
1084
|
if use_cache and cache_client:
|
|
1085
|
+
if debug:
|
|
1086
|
+
print("🔵 [DEBUG] Configuración de cache:")
|
|
1087
|
+
pprint(
|
|
1088
|
+
{
|
|
1089
|
+
"cache_key_prefix": cache_key_prefix,
|
|
1090
|
+
"clear_cache": clear_cache,
|
|
1091
|
+
"use_cache": use_cache,
|
|
1092
|
+
}
|
|
1093
|
+
)
|
|
1094
|
+
|
|
909
1095
|
# Import cache utilities
|
|
910
1096
|
from ..cache import (
|
|
911
1097
|
generate_cache_id,
|
|
@@ -919,6 +1105,10 @@ class QueryBuilder:
|
|
|
919
1105
|
query_hash = generate_cache_id(query_for_hash)
|
|
920
1106
|
count_key = f"{cache_key_prefix}:query:count:{query_hash}"
|
|
921
1107
|
|
|
1108
|
+
if debug:
|
|
1109
|
+
print("🔵 [DEBUG] Cache key generada:")
|
|
1110
|
+
print(f"count_key: {count_key}")
|
|
1111
|
+
|
|
922
1112
|
# Get or set meta from cache
|
|
923
1113
|
meta_result = get_or_set_cache(
|
|
924
1114
|
cache_client,
|
|
@@ -935,8 +1125,54 @@ class QueryBuilder:
|
|
|
935
1125
|
else:
|
|
936
1126
|
meta_result = list(db_collection.aggregate(count_pipeline))
|
|
937
1127
|
|
|
938
|
-
|
|
1128
|
+
# End MongoDB query timing
|
|
1129
|
+
mongodb_time = time.time() - mongodb_start_time
|
|
1130
|
+
|
|
1131
|
+
if debug:
|
|
1132
|
+
print("🔵 [DEBUG] Resultado de query de datos:")
|
|
1133
|
+
print(f"Registros encontrados: {len(data_result)}")
|
|
1134
|
+
if data_result:
|
|
1135
|
+
print("Muestra del primer registro:")
|
|
1136
|
+
pprint(data_result[0])
|
|
1137
|
+
if len(data_result) > 1:
|
|
1138
|
+
print(f"... y {len(data_result) - 1} registros más")
|
|
1139
|
+
else:
|
|
1140
|
+
print("Sin datos encontrados")
|
|
1141
|
+
|
|
1142
|
+
print("🔵 [DEBUG] Resultado de count:")
|
|
1143
|
+
pprint(meta_result)
|
|
1144
|
+
|
|
1145
|
+
result = self.sanitize_result(query, data_result, meta_result)
|
|
1146
|
+
|
|
1147
|
+
if debug:
|
|
1148
|
+
# Calculate total time
|
|
1149
|
+
total_time = time.time() - start_time
|
|
1150
|
+
|
|
1151
|
+
print("🔵 [DEBUG] Respuesta final sanitizada:")
|
|
1152
|
+
preview_data = result.get("data", [])
|
|
1153
|
+
data_preview = {
|
|
1154
|
+
"data_count": len(preview_data),
|
|
1155
|
+
"first_record": preview_data[0] if preview_data else None,
|
|
1156
|
+
"remaining_records": (
|
|
1157
|
+
f"... y {len(preview_data) - 1} más"
|
|
1158
|
+
if len(preview_data) > 1
|
|
1159
|
+
else "ninguno más"
|
|
1160
|
+
),
|
|
1161
|
+
"meta": result.get("meta", {}),
|
|
1162
|
+
"has_pagination": "pagination" in query,
|
|
1163
|
+
}
|
|
1164
|
+
pprint(data_preview)
|
|
1165
|
+
|
|
1166
|
+
print("🔵 [DEBUG] ⏱️ MÉTRICAS DE PERFORMANCE:")
|
|
1167
|
+
print(f" 📊 Tiempo de parsing/pre-consulta: {parsing_time:.3f}s")
|
|
1168
|
+
print(f" 🗄️ Tiempo real de consulta MongoDB: {mongodb_time:.3f}s")
|
|
1169
|
+
print(f" 🚀 Tiempo total de procesamiento: {total_time:.3f}s")
|
|
1170
|
+
print("=" * 60)
|
|
1171
|
+
|
|
1172
|
+
return result
|
|
939
1173
|
except Exception as e:
|
|
1174
|
+
if debug:
|
|
1175
|
+
print(f"🔴 [DEBUG] Error en get_query_response: {str(e)}")
|
|
940
1176
|
raise HTTPException(
|
|
941
1177
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
942
1178
|
detail=str(e),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: oplnk-python-utils
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.2
|
|
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,15 +149,50 @@ 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
|
|
152
179
|
```
|
|
153
180
|
|
|
154
181
|
#### Parsing Functions
|
|
155
182
|
|
|
183
|
+
**Main Functions:**
|
|
184
|
+
|
|
156
185
|
- **`parse_params_to_dict(params)`**: Parse FastAPI QueryParams or dict into structured nested dictionary
|
|
157
186
|
- **`infer_type(value)`**: Smart type inference from string values (supports int, float, bool, datetime, lists)
|
|
158
187
|
- **`parse_key_string_to_list(s)`**: Parse bracketed notation strings like `"filters[name][$eq]"`
|
|
159
188
|
- **`parse_list_to_dict(lst, value)`**: Convert key list and value into nested dictionary structure
|
|
160
189
|
|
|
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
|
+
|
|
161
196
|
### Serialization Utilities (`core.serialization`)
|
|
162
197
|
|
|
163
198
|
Functions for converting data types to serializable formats.
|
|
@@ -351,7 +386,7 @@ GET /users?clear_cache=true
|
|
|
351
386
|
|
|
352
387
|
## Query Module
|
|
353
388
|
|
|
354
|
-
The query module provides a MongoDB aggregation pipeline builder with advanced features.
|
|
389
|
+
The query module provides a MongoDB aggregation pipeline builder with advanced features including enhanced support for nested fields, dot notation, and array notation.
|
|
355
390
|
|
|
356
391
|
### Usage
|
|
357
392
|
|
|
@@ -431,16 +466,42 @@ print(results)
|
|
|
431
466
|
# "data": [...],
|
|
432
467
|
# "meta": {"totalCount": 150}
|
|
433
468
|
# }
|
|
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
|
+
)
|
|
434
495
|
```
|
|
435
496
|
|
|
436
497
|
### QueryBuilder Methods
|
|
437
498
|
|
|
438
499
|
#### Pipeline Builders
|
|
439
500
|
|
|
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
|
|
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
|
|
444
505
|
- **`build_pagination_pipeline(pagination_params)`**: Build pagination pipeline
|
|
445
506
|
- **`build_populate_pipeline(populate_params)`**: Build population pipeline
|
|
446
507
|
|
|
@@ -459,6 +520,7 @@ print(results)
|
|
|
459
520
|
The QueryBuilder class leverages utilities from the core module for enhanced functionality:
|
|
460
521
|
|
|
461
522
|
- **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
|
|
462
524
|
- **Data normalization**: Uses `core.data.normalize()` for consistent cache key generation
|
|
463
525
|
- **Datetime serialization**: Uses `core.serialization.datetime_serializer()` for cache-friendly data
|
|
464
526
|
|
|
@@ -597,6 +659,59 @@ async def create_user(request: Request, user_data: dict):
|
|
|
597
659
|
|
|
598
660
|
## Advanced Features
|
|
599
661
|
|
|
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
|
+
|
|
600
715
|
### Custom Relation Definitions
|
|
601
716
|
|
|
602
717
|
```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.
|
|
5
|
+
version="1.1.2",
|
|
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").read(),
|
|
9
|
+
long_description=open("README.md", encoding="utf-8").read(),
|
|
10
10
|
long_description_content_type="text/markdown",
|
|
11
11
|
url="https://github.com/openlinkspa/oplnk-python-utils",
|
|
12
12
|
packages=find_packages(),
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/oplnk_python_utils/cache/cache_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/oplnk_python_utils/core/serialization.py
RENAMED
|
File without changes
|
|
File without changes
|
{oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/oplnk_python_utils.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/oplnk_python_utils.egg-info/not-zip-safe
RENAMED
|
File without changes
|
{oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/oplnk_python_utils.egg-info/requires.txt
RENAMED
|
File without changes
|
{oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/oplnk_python_utils.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|