oplnk-python-utils 1.2.0__tar.gz → 1.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. {oplnk_python_utils-1.2.0/oplnk_python_utils.egg-info → oplnk_python_utils-1.2.1}/PKG-INFO +55 -7
  2. {oplnk_python_utils-1.2.0 → oplnk_python_utils-1.2.1}/README.md +54 -6
  3. {oplnk_python_utils-1.2.0 → oplnk_python_utils-1.2.1}/oplnk_python_utils/__init__.py +1 -1
  4. {oplnk_python_utils-1.2.0 → oplnk_python_utils-1.2.1}/oplnk_python_utils/query/query_builder.py +73 -2
  5. {oplnk_python_utils-1.2.0 → oplnk_python_utils-1.2.1/oplnk_python_utils.egg-info}/PKG-INFO +55 -7
  6. {oplnk_python_utils-1.2.0 → oplnk_python_utils-1.2.1}/setup.py +1 -1
  7. {oplnk_python_utils-1.2.0 → oplnk_python_utils-1.2.1}/LICENSE +0 -0
  8. {oplnk_python_utils-1.2.0 → oplnk_python_utils-1.2.1}/MANIFEST.in +0 -0
  9. {oplnk_python_utils-1.2.0 → oplnk_python_utils-1.2.1}/oplnk_python_utils/cache/__init__.py +0 -0
  10. {oplnk_python_utils-1.2.0 → oplnk_python_utils-1.2.1}/oplnk_python_utils/cache/cache_utils.py +0 -0
  11. {oplnk_python_utils-1.2.0 → oplnk_python_utils-1.2.1}/oplnk_python_utils/core/__init__.py +0 -0
  12. {oplnk_python_utils-1.2.0 → oplnk_python_utils-1.2.1}/oplnk_python_utils/core/data.py +0 -0
  13. {oplnk_python_utils-1.2.0 → oplnk_python_utils-1.2.1}/oplnk_python_utils/core/parsing.py +0 -0
  14. {oplnk_python_utils-1.2.0 → oplnk_python_utils-1.2.1}/oplnk_python_utils/core/serialization.py +0 -0
  15. {oplnk_python_utils-1.2.0 → oplnk_python_utils-1.2.1}/oplnk_python_utils/query/__init__.py +0 -0
  16. {oplnk_python_utils-1.2.0 → oplnk_python_utils-1.2.1}/oplnk_python_utils/reports/__init__.py +0 -0
  17. {oplnk_python_utils-1.2.0 → oplnk_python_utils-1.2.1}/oplnk_python_utils.egg-info/SOURCES.txt +0 -0
  18. {oplnk_python_utils-1.2.0 → oplnk_python_utils-1.2.1}/oplnk_python_utils.egg-info/dependency_links.txt +0 -0
  19. {oplnk_python_utils-1.2.0 → oplnk_python_utils-1.2.1}/oplnk_python_utils.egg-info/not-zip-safe +0 -0
  20. {oplnk_python_utils-1.2.0 → oplnk_python_utils-1.2.1}/oplnk_python_utils.egg-info/requires.txt +0 -0
  21. {oplnk_python_utils-1.2.0 → oplnk_python_utils-1.2.1}/oplnk_python_utils.egg-info/top_level.txt +0 -0
  22. {oplnk_python_utils-1.2.0 → oplnk_python_utils-1.2.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oplnk-python-utils
3
- Version: 1.2.0
3
+ Version: 1.2.1
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
@@ -623,7 +623,7 @@ relations = [
623
623
  ]
624
624
  ```
625
625
 
626
- ### Reserved Audit Fields (NEW in v1.2.0)
626
+ ### Reserved Audit Fields (v1.2.1)
627
627
 
628
628
  The QueryBuilder automatically handles common audit fields without requiring manual configuration:
629
629
 
@@ -642,12 +642,11 @@ query = {
642
642
  "populate": ["creation_user", "modification_user"]
643
643
  }
644
644
 
645
- # Result will include:
645
+ # Result will include (original ID automatically removed, replaced by populated object):
646
646
  # {
647
647
  # "data": [{
648
648
  # "_id": "123",
649
- # "creation_user": "user_id_string",
650
- # "creation_user_info": { // Automatically populated
649
+ # "creation_user": { // Replaces the original ID field
651
650
  # "_id": "user_id_string",
652
651
  # "name": "John Doe",
653
652
  # "person": "person_id"
@@ -657,11 +656,60 @@ query = {
657
656
  ```
658
657
 
659
658
  **Reserved fields:**
660
- - `creation_user` → populated as `creation_user_info`
661
- - `modification_user` → populated as `modification_user_info`
659
+
660
+ - `creation_user` → automatically populated, original ID is excluded
661
+ - `modification_user` → automatically populated, original ID is excluded
662
662
 
663
663
  These fields are only populated when explicitly requested via the `populate` parameter, following the Strapi philosophy of on-demand loading.
664
664
 
665
+ ### Populate Field Sanitization (v1.2.1)
666
+
667
+ By default, when you populate a relation, the original ID field is automatically removed from the response (Strapi-like behavior):
668
+
669
+ ```python
670
+ # Request with populate - establishment_id will be automatically excluded:
671
+ query = {
672
+ "populate": ["establishment"]
673
+ }
674
+
675
+ # Response - establishment_id is not present:
676
+ # {
677
+ # "data": [{
678
+ # "_id": "123",
679
+ # "name": "Product",
680
+ # "establishment": { // Populated object replaces the ID
681
+ # "_id": "est_123",
682
+ # "name": "Main Office"
683
+ # }
684
+ # // establishment_id is NOT here
685
+ # }]
686
+ # }
687
+ ```
688
+
689
+ **To keep both the ID and populated object, explicitly request it in `fields`:**
690
+
691
+ ```python
692
+ query = {
693
+ "populate": ["establishment"],
694
+ "fields": ["name", "establishment_id"] # Request the ID explicitly
695
+ }
696
+
697
+ # Response - both ID and populated object are present:
698
+ # {
699
+ # "data": [{
700
+ # "_id": "123",
701
+ # "name": "Product",
702
+ # "establishment_id": "est_123", // Now included
703
+ # "establishment": {
704
+ # "_id": "est_123",
705
+ # "name": "Main Office"
706
+ # }
707
+ # }]
708
+ # }
709
+ ```
710
+
711
+ **For audit fields:** The original ID is always excluded (no option to keep it, since there's no `creation_user_id` field).
712
+
665
713
  ### Custom Pipelines
666
714
 
667
715
  ```python
@@ -581,7 +581,7 @@ relations = [
581
581
  ]
582
582
  ```
583
583
 
584
- ### Reserved Audit Fields (NEW in v1.2.0)
584
+ ### Reserved Audit Fields (v1.2.1)
585
585
 
586
586
  The QueryBuilder automatically handles common audit fields without requiring manual configuration:
587
587
 
@@ -600,12 +600,11 @@ query = {
600
600
  "populate": ["creation_user", "modification_user"]
601
601
  }
602
602
 
603
- # Result will include:
603
+ # Result will include (original ID automatically removed, replaced by populated object):
604
604
  # {
605
605
  # "data": [{
606
606
  # "_id": "123",
607
- # "creation_user": "user_id_string",
608
- # "creation_user_info": { // Automatically populated
607
+ # "creation_user": { // Replaces the original ID field
609
608
  # "_id": "user_id_string",
610
609
  # "name": "John Doe",
611
610
  # "person": "person_id"
@@ -615,11 +614,60 @@ query = {
615
614
  ```
616
615
 
617
616
  **Reserved fields:**
618
- - `creation_user` → populated as `creation_user_info`
619
- - `modification_user` → populated as `modification_user_info`
617
+
618
+ - `creation_user` → automatically populated, original ID is excluded
619
+ - `modification_user` → automatically populated, original ID is excluded
620
620
 
621
621
  These fields are only populated when explicitly requested via the `populate` parameter, following the Strapi philosophy of on-demand loading.
622
622
 
623
+ ### Populate Field Sanitization (v1.2.1)
624
+
625
+ By default, when you populate a relation, the original ID field is automatically removed from the response (Strapi-like behavior):
626
+
627
+ ```python
628
+ # Request with populate - establishment_id will be automatically excluded:
629
+ query = {
630
+ "populate": ["establishment"]
631
+ }
632
+
633
+ # Response - establishment_id is not present:
634
+ # {
635
+ # "data": [{
636
+ # "_id": "123",
637
+ # "name": "Product",
638
+ # "establishment": { // Populated object replaces the ID
639
+ # "_id": "est_123",
640
+ # "name": "Main Office"
641
+ # }
642
+ # // establishment_id is NOT here
643
+ # }]
644
+ # }
645
+ ```
646
+
647
+ **To keep both the ID and populated object, explicitly request it in `fields`:**
648
+
649
+ ```python
650
+ query = {
651
+ "populate": ["establishment"],
652
+ "fields": ["name", "establishment_id"] # Request the ID explicitly
653
+ }
654
+
655
+ # Response - both ID and populated object are present:
656
+ # {
657
+ # "data": [{
658
+ # "_id": "123",
659
+ # "name": "Product",
660
+ # "establishment_id": "est_123", // Now included
661
+ # "establishment": {
662
+ # "_id": "est_123",
663
+ # "name": "Main Office"
664
+ # }
665
+ # }]
666
+ # }
667
+ ```
668
+
669
+ **For audit fields:** The original ID is always excluded (no option to keep it, since there's no `creation_user_id` field).
670
+
623
671
  ### Custom Pipelines
624
672
 
625
673
  ```python
@@ -7,7 +7,7 @@ from . import core
7
7
  from . import cache
8
8
  from . import query
9
9
 
10
- __version__ = "1.2.0"
10
+ __version__ = "1.2.1"
11
11
  __author__ = "OpenLink SpA"
12
12
 
13
13
  __all__ = ["core", "cache", "query", "reports", "__version__"]
@@ -21,7 +21,7 @@ RESERVED_AUDIT_FIELDS = {
21
21
  "creation_user": {
22
22
  "collection": "users",
23
23
  "field_name": "creation_user",
24
- "alias": "creation_user_info",
24
+ "alias": "creation_user",
25
25
  "field_type": "id",
26
26
  "project_exclude": {
27
27
  "role": 0,
@@ -49,7 +49,7 @@ RESERVED_AUDIT_FIELDS = {
49
49
  "modification_user": {
50
50
  "collection": "users",
51
51
  "field_name": "modification_user",
52
- "alias": "modification_user_info",
52
+ "alias": "modification_user",
53
53
  "field_type": "id",
54
54
  "project_exclude": {
55
55
  "role": 0,
@@ -778,6 +778,48 @@ class QueryBuilder:
778
778
 
779
779
  return pipeline_copy
780
780
 
781
+ def _get_populated_field_mappings(self, query_populate: Any) -> Dict[str, str]:
782
+ """
783
+ Build a mapping of populated field names to their original ID field names.
784
+
785
+ For example: {"establishment": "establishment_id", "creation_user": "_original_creation_user"}
786
+
787
+ Args:
788
+ query_populate: The populate parameter (list or dict)
789
+
790
+ Returns:
791
+ Dict mapping populated field names to their original ID fields
792
+ """
793
+ mappings = {}
794
+
795
+ if isinstance(query_populate, list):
796
+ for field in query_populate:
797
+ if field in RESERVED_AUDIT_FIELDS:
798
+ # Audit fields: map to the temporary original field
799
+ mappings[field] = f"_original_{field}"
800
+ else:
801
+ # Normal relations: try to find the ID field
802
+ for relation in self.relations:
803
+ relation_name = relation[1]
804
+ # Remove _id or _ids suffix to get base name
805
+ base_name = relation_name.replace("_ids", "").replace("_id", "")
806
+ if base_name == field:
807
+ mappings[field] = relation_name
808
+ break
809
+ elif isinstance(query_populate, dict):
810
+ for field in query_populate.keys():
811
+ if field in RESERVED_AUDIT_FIELDS:
812
+ mappings[field] = f"_original_{field}"
813
+ else:
814
+ for relation in self.relations:
815
+ relation_name = relation[1]
816
+ base_name = relation_name.replace("_ids", "").replace("_id", "")
817
+ if base_name == field:
818
+ mappings[field] = relation_name
819
+ break
820
+
821
+ return mappings
822
+
781
823
  def sanitize_result(
782
824
  self, query: Dict[str, Any], data_result: Any, meta_result: Any
783
825
  ) -> Dict[str, Any]:
@@ -800,6 +842,23 @@ class QueryBuilder:
800
842
  if not meta_result or "totalCount" not in meta_result:
801
843
  meta_result = {"totalCount": 0}
802
844
 
845
+ # Sanitize populate fields: remove original ID fields when populated
846
+ if "populate" in query and isinstance(data_result, list):
847
+ fields_requested = query.get("fields", [])
848
+ populated_mappings = self._get_populated_field_mappings(query["populate"])
849
+
850
+ for doc in data_result:
851
+ for populated_field, original_id_field in populated_mappings.items():
852
+ # If it's a reserved audit field (starts with _original_), always remove the temp field
853
+ if original_id_field.startswith("_original_"):
854
+ if original_id_field in doc:
855
+ del doc[original_id_field]
856
+ # For normal relations, only remove if not explicitly requested in fields
857
+ else:
858
+ if original_id_field not in fields_requested:
859
+ if original_id_field in doc:
860
+ del doc[original_id_field]
861
+
803
862
  return {"data": data_result, "meta": meta_result}
804
863
 
805
864
  def get_reserved_field_lookup(self, field_name: str) -> List[Dict[str, Any]]:
@@ -821,6 +880,12 @@ class QueryBuilder:
821
880
  field_config = RESERVED_AUDIT_FIELDS[field_name]
822
881
 
823
882
  lookup_pipeline = [
883
+ # Save the original ID before lookup (will be removed during sanitization)
884
+ {
885
+ "$addFields": {
886
+ f"_original_{field_config['field_name']}": f"${field_config['field_name']}"
887
+ }
888
+ },
824
889
  {
825
890
  "$lookup": {
826
891
  "from": field_config["collection"],
@@ -878,7 +943,13 @@ class QueryBuilder:
878
943
  else:
879
944
  let_value = {"lid": f"${relation_field_name}"}
880
945
 
946
+ # Save the original ID field before lookup (will be sanitized unless requested in fields)
881
947
  lookup_relation = [
948
+ {
949
+ "$addFields": {
950
+ f"_original_{relation_field_name}": f"${relation_field_name}"
951
+ }
952
+ },
882
953
  {
883
954
  "$lookup": {
884
955
  "from": f"{_prefix}{collection_name_relation}",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oplnk-python-utils
3
- Version: 1.2.0
3
+ Version: 1.2.1
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
@@ -623,7 +623,7 @@ relations = [
623
623
  ]
624
624
  ```
625
625
 
626
- ### Reserved Audit Fields (NEW in v1.2.0)
626
+ ### Reserved Audit Fields (v1.2.1)
627
627
 
628
628
  The QueryBuilder automatically handles common audit fields without requiring manual configuration:
629
629
 
@@ -642,12 +642,11 @@ query = {
642
642
  "populate": ["creation_user", "modification_user"]
643
643
  }
644
644
 
645
- # Result will include:
645
+ # Result will include (original ID automatically removed, replaced by populated object):
646
646
  # {
647
647
  # "data": [{
648
648
  # "_id": "123",
649
- # "creation_user": "user_id_string",
650
- # "creation_user_info": { // Automatically populated
649
+ # "creation_user": { // Replaces the original ID field
651
650
  # "_id": "user_id_string",
652
651
  # "name": "John Doe",
653
652
  # "person": "person_id"
@@ -657,11 +656,60 @@ query = {
657
656
  ```
658
657
 
659
658
  **Reserved fields:**
660
- - `creation_user` → populated as `creation_user_info`
661
- - `modification_user` → populated as `modification_user_info`
659
+
660
+ - `creation_user` → automatically populated, original ID is excluded
661
+ - `modification_user` → automatically populated, original ID is excluded
662
662
 
663
663
  These fields are only populated when explicitly requested via the `populate` parameter, following the Strapi philosophy of on-demand loading.
664
664
 
665
+ ### Populate Field Sanitization (v1.2.1)
666
+
667
+ By default, when you populate a relation, the original ID field is automatically removed from the response (Strapi-like behavior):
668
+
669
+ ```python
670
+ # Request with populate - establishment_id will be automatically excluded:
671
+ query = {
672
+ "populate": ["establishment"]
673
+ }
674
+
675
+ # Response - establishment_id is not present:
676
+ # {
677
+ # "data": [{
678
+ # "_id": "123",
679
+ # "name": "Product",
680
+ # "establishment": { // Populated object replaces the ID
681
+ # "_id": "est_123",
682
+ # "name": "Main Office"
683
+ # }
684
+ # // establishment_id is NOT here
685
+ # }]
686
+ # }
687
+ ```
688
+
689
+ **To keep both the ID and populated object, explicitly request it in `fields`:**
690
+
691
+ ```python
692
+ query = {
693
+ "populate": ["establishment"],
694
+ "fields": ["name", "establishment_id"] # Request the ID explicitly
695
+ }
696
+
697
+ # Response - both ID and populated object are present:
698
+ # {
699
+ # "data": [{
700
+ # "_id": "123",
701
+ # "name": "Product",
702
+ # "establishment_id": "est_123", // Now included
703
+ # "establishment": {
704
+ # "_id": "est_123",
705
+ # "name": "Main Office"
706
+ # }
707
+ # }]
708
+ # }
709
+ ```
710
+
711
+ **For audit fields:** The original ID is always excluded (no option to keep it, since there's no `creation_user_id` field).
712
+
665
713
  ### Custom Pipelines
666
714
 
667
715
  ```python
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="oplnk-python-utils",
5
- version="1.2.0",
5
+ version="1.2.1",
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",