oplnk-python-utils 1.1.4__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.
- {oplnk_python_utils-1.1.4/oplnk_python_utils.egg-info → oplnk_python_utils-1.1.5}/PKG-INFO +6 -121
- {oplnk_python_utils-1.1.4 → oplnk_python_utils-1.1.5}/README.md +5 -120
- {oplnk_python_utils-1.1.4 → oplnk_python_utils-1.1.5}/oplnk_python_utils/__init__.py +3 -2
- {oplnk_python_utils-1.1.4 → oplnk_python_utils-1.1.5}/oplnk_python_utils/cache/cache_utils.py +57 -5
- {oplnk_python_utils-1.1.4 → oplnk_python_utils-1.1.5}/oplnk_python_utils/core/parsing.py +11 -90
- {oplnk_python_utils-1.1.4 → oplnk_python_utils-1.1.5}/oplnk_python_utils/query/query_builder.py +30 -286
- {oplnk_python_utils-1.1.4 → oplnk_python_utils-1.1.5/oplnk_python_utils.egg-info}/PKG-INFO +6 -121
- {oplnk_python_utils-1.1.4 → oplnk_python_utils-1.1.5}/setup.py +2 -2
- {oplnk_python_utils-1.1.4 → oplnk_python_utils-1.1.5}/LICENSE +0 -0
- {oplnk_python_utils-1.1.4 → oplnk_python_utils-1.1.5}/MANIFEST.in +0 -0
- {oplnk_python_utils-1.1.4 → oplnk_python_utils-1.1.5}/oplnk_python_utils/cache/__init__.py +0 -0
- {oplnk_python_utils-1.1.4 → oplnk_python_utils-1.1.5}/oplnk_python_utils/core/__init__.py +0 -0
- {oplnk_python_utils-1.1.4 → oplnk_python_utils-1.1.5}/oplnk_python_utils/core/data.py +0 -0
- {oplnk_python_utils-1.1.4 → oplnk_python_utils-1.1.5}/oplnk_python_utils/core/serialization.py +0 -0
- {oplnk_python_utils-1.1.4 → oplnk_python_utils-1.1.5}/oplnk_python_utils/query/__init__.py +0 -0
- {oplnk_python_utils-1.1.4 → oplnk_python_utils-1.1.5}/oplnk_python_utils.egg-info/SOURCES.txt +0 -0
- {oplnk_python_utils-1.1.4 → oplnk_python_utils-1.1.5}/oplnk_python_utils.egg-info/dependency_links.txt +0 -0
- {oplnk_python_utils-1.1.4 → oplnk_python_utils-1.1.5}/oplnk_python_utils.egg-info/not-zip-safe +0 -0
- {oplnk_python_utils-1.1.4 → oplnk_python_utils-1.1.5}/oplnk_python_utils.egg-info/requires.txt +0 -0
- {oplnk_python_utils-1.1.4 → oplnk_python_utils-1.1.5}/oplnk_python_utils.egg-info/top_level.txt +0 -0
- {oplnk_python_utils-1.1.4 → 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
|
+
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
|
|
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
|
|
502
|
-
- **`build_sort_pipeline(sort_params)`**: Build sort pipeline
|
|
503
|
-
- **`build_fields_pipeline(fields_params)`**: Build field projection pipeline
|
|
504
|
-
- **`build_distinct_pipeline(distinct_params)`**: Build distinct pipeline
|
|
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
|
|
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
|
|
460
|
-
- **`build_sort_pipeline(sort_params)`**: Build sort pipeline
|
|
461
|
-
- **`build_fields_pipeline(fields_params)`**: Build field projection pipeline
|
|
462
|
-
- **`build_distinct_pipeline(distinct_params)`**: Build distinct pipeline
|
|
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.
|
|
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__"]
|
{oplnk_python_utils-1.1.4 → oplnk_python_utils-1.1.5}/oplnk_python_utils/cache/cache_utils.py
RENAMED
|
@@ -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
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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,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
|
{oplnk_python_utils-1.1.4 → oplnk_python_utils-1.1.5}/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
|
|
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
|
-
|
|
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
|
|
484
|
-
|
|
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
|
|
493
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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[
|
|
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
|
-
|
|
553
|
-
|
|
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
|
-
|
|
574
|
-
"""
|
|
575
|
-
fields_pipeline = {}
|
|
496
|
+
"""Build fields projection pipeline"""
|
|
497
|
+
fields_pipeline = {field: 1 for field in fields_params}
|
|
576
498
|
|
|
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}")
|
|
589
|
-
|
|
590
|
-
# Process populate_params if provided
|
|
591
499
|
if populate_params:
|
|
592
500
|
for field in populate_params:
|
|
593
|
-
|
|
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,66 +508,14 @@ class QueryBuilder:
|
|
|
611
508
|
include_project: bool = True,
|
|
612
509
|
pipeline: Optional[List] = None,
|
|
613
510
|
) -> List[Dict[str, Any]]:
|
|
614
|
-
"""
|
|
615
|
-
|
|
616
|
-
"""
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
for field in distinct_params:
|
|
623
|
-
try:
|
|
624
|
-
# Use utility function to build proper aggregation expression
|
|
625
|
-
field_expression = build_aggregation_field_expression(field)
|
|
626
|
-
|
|
627
|
-
# For MongoDB $group stage, we need to use a safe key name (no dots)
|
|
628
|
-
# We'll use the original field name as key, but replace dots with underscores for the group
|
|
629
|
-
safe_key = (
|
|
630
|
-
field.replace(".", "_").replace("[", "_").replace("]", "_")
|
|
631
|
-
)
|
|
632
|
-
group_id[safe_key] = field_expression
|
|
633
|
-
|
|
634
|
-
# Build nested structure for the projection to maintain original field structure
|
|
635
|
-
parts = field.split(".")
|
|
636
|
-
current = project_fields
|
|
637
|
-
for i, part in enumerate(parts[:-1]):
|
|
638
|
-
if part not in current:
|
|
639
|
-
current[part] = {}
|
|
640
|
-
current = current[part]
|
|
641
|
-
|
|
642
|
-
# Set the final value
|
|
643
|
-
current[parts[-1]] = f"$_id.{safe_key}"
|
|
644
|
-
|
|
645
|
-
except ValueError as e:
|
|
646
|
-
# Log error and skip invalid field
|
|
647
|
-
print(f"Warning: Skipping invalid field '{field}' in distinct: {e}")
|
|
648
|
-
continue
|
|
649
|
-
|
|
650
|
-
if not group_id:
|
|
651
|
-
raise ValueError("No valid fields provided for distinct operation")
|
|
652
|
-
|
|
653
|
-
group_stage = {"$group": {"_id": group_id}}
|
|
654
|
-
|
|
655
|
-
if include_project:
|
|
656
|
-
# Use $project to create the nested structure
|
|
657
|
-
project_stage = {"$project": project_fields}
|
|
658
|
-
return [group_stage, project_stage]
|
|
659
|
-
else:
|
|
660
|
-
return [group_stage]
|
|
661
|
-
|
|
662
|
-
except Exception as e:
|
|
663
|
-
# Fallback to original behavior for backwards compatibility
|
|
664
|
-
print(
|
|
665
|
-
f"Warning: Error in enhanced distinct pipeline, falling back to original: {e}"
|
|
666
|
-
)
|
|
667
|
-
group_id = {field: f"${field}" for field in distinct_params}
|
|
668
|
-
group_stage = {"$group": {"_id": group_id}}
|
|
669
|
-
if include_project:
|
|
670
|
-
replace_root_stage = {"$replaceRoot": {"newRoot": "$_id"}}
|
|
671
|
-
return [group_stage, replace_root_stage]
|
|
672
|
-
else:
|
|
673
|
-
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]
|
|
674
519
|
|
|
675
520
|
def build_pagination_pipeline(
|
|
676
521
|
self, pagination_params: Dict[str, Any], pipeline: Optional[List] = None
|
|
@@ -1038,9 +883,6 @@ class QueryBuilder:
|
|
|
1038
883
|
cache_key_prefix: str = "",
|
|
1039
884
|
use_cache: bool = False,
|
|
1040
885
|
clear_cache: bool = False,
|
|
1041
|
-
debug: bool = False,
|
|
1042
|
-
start_time: float = 0,
|
|
1043
|
-
parsing_time: float = 0,
|
|
1044
886
|
) -> Dict[str, Any]:
|
|
1045
887
|
"""
|
|
1046
888
|
Get query response with optional caching support
|
|
@@ -1054,64 +896,16 @@ class QueryBuilder:
|
|
|
1054
896
|
cache_key_prefix: Prefix for cache keys
|
|
1055
897
|
use_cache: Whether to use caching
|
|
1056
898
|
clear_cache: Whether to clear cache before query
|
|
1057
|
-
debug: Whether to enable debug output
|
|
1058
|
-
start_time: Start time of the query
|
|
1059
|
-
parsing_time: Time spent parsing parameters
|
|
1060
899
|
"""
|
|
1061
900
|
try:
|
|
1062
|
-
# Build pipelines
|
|
1063
|
-
pipeline_build_start = time.time()
|
|
1064
901
|
base_pipeline = self.make_aggregation_query(query, pipeline)
|
|
1065
902
|
count_pipeline = self.make_count_aggregation_query(query, pipeline_count)
|
|
1066
|
-
pipeline_build_time = time.time() - pipeline_build_start
|
|
1067
|
-
|
|
1068
|
-
if debug:
|
|
1069
|
-
print("🔵 [DEBUG] Pipeline de agregación construido:")
|
|
1070
|
-
pprint(base_pipeline)
|
|
1071
|
-
print("🔵 [DEBUG] Pipeline de count construido:")
|
|
1072
|
-
pprint(count_pipeline)
|
|
1073
|
-
|
|
1074
|
-
# Debug filters
|
|
1075
|
-
if "filters" in query:
|
|
1076
|
-
filter_pipeline = self.build_filter_pipeline(query["filters"])
|
|
1077
|
-
print("🔵 [DEBUG] Filtros aplicados:")
|
|
1078
|
-
pprint(filter_pipeline)
|
|
1079
|
-
|
|
1080
|
-
# Debug special operations
|
|
1081
|
-
if "populate" in query:
|
|
1082
|
-
print("🔵 [DEBUG] Relaciones a popular:")
|
|
1083
|
-
pprint(query["populate"])
|
|
1084
|
-
|
|
1085
|
-
if "distinct" in query:
|
|
1086
|
-
print("🔵 [DEBUG] Campos distinct:")
|
|
1087
|
-
pprint(query["distinct"])
|
|
1088
|
-
|
|
1089
|
-
if "sort" in query:
|
|
1090
|
-
print("🔵 [DEBUG] Ordenamiento aplicado:")
|
|
1091
|
-
pprint(query["sort"])
|
|
1092
|
-
|
|
1093
|
-
if "pagination" in query:
|
|
1094
|
-
print("🔵 [DEBUG] Paginación aplicada:")
|
|
1095
|
-
pprint(query["pagination"])
|
|
1096
|
-
|
|
1097
|
-
# Start MongoDB query timing
|
|
1098
|
-
mongodb_start_time = time.time()
|
|
1099
903
|
|
|
1100
904
|
# Get data
|
|
1101
905
|
data_result = list(db_collection.aggregate(base_pipeline))
|
|
1102
906
|
|
|
1103
907
|
# Get count
|
|
1104
908
|
if use_cache and cache_client:
|
|
1105
|
-
if debug:
|
|
1106
|
-
print("🔵 [DEBUG] Configuración de cache:")
|
|
1107
|
-
pprint(
|
|
1108
|
-
{
|
|
1109
|
-
"cache_key_prefix": cache_key_prefix,
|
|
1110
|
-
"clear_cache": clear_cache,
|
|
1111
|
-
"use_cache": use_cache,
|
|
1112
|
-
}
|
|
1113
|
-
)
|
|
1114
|
-
|
|
1115
909
|
# Import cache utilities
|
|
1116
910
|
from ..cache import (
|
|
1117
911
|
generate_cache_id,
|
|
@@ -1125,10 +919,6 @@ class QueryBuilder:
|
|
|
1125
919
|
query_hash = generate_cache_id(query_for_hash)
|
|
1126
920
|
count_key = f"{cache_key_prefix}:query:count:{query_hash}"
|
|
1127
921
|
|
|
1128
|
-
if debug:
|
|
1129
|
-
print("🔵 [DEBUG] Cache key generada:")
|
|
1130
|
-
print(f"count_key: {count_key}")
|
|
1131
|
-
|
|
1132
922
|
# Get or set meta from cache
|
|
1133
923
|
meta_result = get_or_set_cache(
|
|
1134
924
|
cache_client,
|
|
@@ -1145,54 +935,8 @@ class QueryBuilder:
|
|
|
1145
935
|
else:
|
|
1146
936
|
meta_result = list(db_collection.aggregate(count_pipeline))
|
|
1147
937
|
|
|
1148
|
-
|
|
1149
|
-
mongodb_time = time.time() - mongodb_start_time
|
|
1150
|
-
|
|
1151
|
-
if debug:
|
|
1152
|
-
print("🔵 [DEBUG] Resultado de query de datos:")
|
|
1153
|
-
print(f"Registros encontrados: {len(data_result)}")
|
|
1154
|
-
if data_result:
|
|
1155
|
-
print("Muestra del primer registro:")
|
|
1156
|
-
pprint(data_result[0])
|
|
1157
|
-
if len(data_result) > 1:
|
|
1158
|
-
print(f"... y {len(data_result) - 1} registros más")
|
|
1159
|
-
else:
|
|
1160
|
-
print("Sin datos encontrados")
|
|
1161
|
-
|
|
1162
|
-
print("🔵 [DEBUG] Resultado de count:")
|
|
1163
|
-
pprint(meta_result)
|
|
1164
|
-
|
|
1165
|
-
result = self.sanitize_result(query, data_result, meta_result)
|
|
1166
|
-
|
|
1167
|
-
if debug:
|
|
1168
|
-
# Calculate total time
|
|
1169
|
-
total_time = time.time() - start_time
|
|
1170
|
-
|
|
1171
|
-
print("🔵 [DEBUG] Respuesta final sanitizada:")
|
|
1172
|
-
preview_data = result.get("data", [])
|
|
1173
|
-
data_preview = {
|
|
1174
|
-
"data_count": len(preview_data),
|
|
1175
|
-
"first_record": preview_data[0] if preview_data else None,
|
|
1176
|
-
"remaining_records": (
|
|
1177
|
-
f"... y {len(preview_data) - 1} más"
|
|
1178
|
-
if len(preview_data) > 1
|
|
1179
|
-
else "ninguno más"
|
|
1180
|
-
),
|
|
1181
|
-
"meta": result.get("meta", {}),
|
|
1182
|
-
"has_pagination": "pagination" in query,
|
|
1183
|
-
}
|
|
1184
|
-
pprint(data_preview)
|
|
1185
|
-
|
|
1186
|
-
print("🔵 [DEBUG] ⏱️ MÉTRICAS DE PERFORMANCE:")
|
|
1187
|
-
print(f" 📊 Tiempo de parsing/pre-consulta: {parsing_time:.3f}s")
|
|
1188
|
-
print(f" 🗄️ Tiempo real de consulta MongoDB: {mongodb_time:.3f}s")
|
|
1189
|
-
print(f" 🚀 Tiempo total de procesamiento: {total_time:.3f}s")
|
|
1190
|
-
print("=" * 60)
|
|
1191
|
-
|
|
1192
|
-
return result
|
|
938
|
+
return self.sanitize_result(query, data_result, meta_result)
|
|
1193
939
|
except Exception as e:
|
|
1194
|
-
if debug:
|
|
1195
|
-
print(f"🔴 [DEBUG] Error en get_query_response: {str(e)}")
|
|
1196
940
|
raise HTTPException(
|
|
1197
941
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
1198
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
|
+
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
|
|
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
|
|
502
|
-
- **`build_sort_pipeline(sort_params)`**: Build sort pipeline
|
|
503
|
-
- **`build_fields_pipeline(fields_params)`**: Build field projection pipeline
|
|
504
|
-
- **`build_distinct_pipeline(distinct_params)`**: Build distinct pipeline
|
|
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.
|
|
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"
|
|
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(),
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{oplnk_python_utils-1.1.4 → oplnk_python_utils-1.1.5}/oplnk_python_utils/core/serialization.py
RENAMED
|
File without changes
|
|
File without changes
|
{oplnk_python_utils-1.1.4 → oplnk_python_utils-1.1.5}/oplnk_python_utils.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{oplnk_python_utils-1.1.4 → oplnk_python_utils-1.1.5}/oplnk_python_utils.egg-info/not-zip-safe
RENAMED
|
File without changes
|
{oplnk_python_utils-1.1.4 → oplnk_python_utils-1.1.5}/oplnk_python_utils.egg-info/requires.txt
RENAMED
|
File without changes
|
{oplnk_python_utils-1.1.4 → oplnk_python_utils-1.1.5}/oplnk_python_utils.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|