oplnk-python-utils 1.2.1__py3-none-any.whl → 1.2.3__py3-none-any.whl

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.
@@ -7,7 +7,7 @@ from . import core
7
7
  from . import cache
8
8
  from . import query
9
9
 
10
- __version__ = "1.2.1"
10
+ __version__ = "1.2.3"
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",
24
+ "alias": "creation_user_info",
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",
52
+ "alias": "modification_user_info",
53
53
  "field_type": "id",
54
54
  "project_exclude": {
55
55
  "role": 0,
@@ -778,47 +778,46 @@ class QueryBuilder:
778
778
 
779
779
  return pipeline_copy
780
780
 
781
- def _get_populated_field_mappings(self, query_populate: Any) -> Dict[str, str]:
781
+ def _get_populated_fields_list(self, query_populate: Any) -> List[str]:
782
782
  """
783
- Build a mapping of populated field names to their original ID field names.
783
+ Get list of field names that are being populated.
784
784
 
785
- For example: {"establishment": "establishment_id", "creation_user": "_original_creation_user"}
785
+ For example: ["establishment", "creation_user"]
786
786
 
787
787
  Args:
788
788
  query_populate: The populate parameter (list or dict)
789
789
 
790
790
  Returns:
791
- Dict mapping populated field names to their original ID fields
791
+ List of field names being populated
792
792
  """
793
- mappings = {}
793
+ populated = []
794
794
 
795
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
796
+ populated = query_populate
809
797
  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
798
+ populated = list(query_populate.keys())
799
+
800
+ return populated
801
+
802
+ def _get_id_field_for_relation(self, relation_name: str) -> Optional[str]:
803
+ """
804
+ Get the ID field name for a given relation.
820
805
 
821
- return mappings
806
+ For example: "establishment" -> "establishment_id"
807
+
808
+ Args:
809
+ relation_name: The relation name (e.g., "establishment")
810
+
811
+ Returns:
812
+ The ID field name (e.g., "establishment_id") or None if not found
813
+ """
814
+ for relation in self.relations:
815
+ field_name = relation[1]
816
+ # Remove _id or _ids suffix to get base name
817
+ base_name = field_name.replace("_ids", "").replace("_id", "")
818
+ if base_name == relation_name:
819
+ return field_name
820
+ return None
822
821
 
823
822
  def sanitize_result(
824
823
  self, query: Dict[str, Any], data_result: Any, meta_result: Any
@@ -842,22 +841,31 @@ class QueryBuilder:
842
841
  if not meta_result or "totalCount" not in meta_result:
843
842
  meta_result = {"totalCount": 0}
844
843
 
845
- # Sanitize populate fields: remove original ID fields when populated
844
+ # Sanitize populate fields: remove original ID fields when populated (unless requested in fields)
846
845
  if "populate" in query and isinstance(data_result, list):
847
846
  fields_requested = query.get("fields", [])
848
- populated_mappings = self._get_populated_field_mappings(query["populate"])
847
+ populated_fields = self._get_populated_fields_list(query["populate"])
849
848
 
850
849
  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]
850
+ # For audit fields: remove original if populated version exists and not requested in fields
851
+ for field_name, field_config in RESERVED_AUDIT_FIELDS.items():
852
+ if field_name in populated_fields:
853
+ # If the populated alias exists and original field is not explicitly requested
854
+ if (
855
+ field_config["alias"] in doc
856
+ and field_name not in fields_requested
857
+ ):
858
+ if field_name in doc:
859
+ del doc[field_name]
860
+
861
+ # For normal relations: remove original ID unless explicitly requested in fields
862
+ for populated_field in populated_fields:
863
+ if populated_field not in RESERVED_AUDIT_FIELDS:
864
+ # Try to find the ID field for this relation
865
+ id_field = self._get_id_field_for_relation(populated_field)
866
+ if id_field and id_field not in fields_requested:
867
+ if id_field in doc:
868
+ del doc[id_field]
861
869
 
862
870
  return {"data": data_result, "meta": meta_result}
863
871
 
@@ -880,12 +888,6 @@ class QueryBuilder:
880
888
  field_config = RESERVED_AUDIT_FIELDS[field_name]
881
889
 
882
890
  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
- },
889
891
  {
890
892
  "$lookup": {
891
893
  "from": field_config["collection"],
@@ -943,13 +945,7 @@ class QueryBuilder:
943
945
  else:
944
946
  let_value = {"lid": f"${relation_field_name}"}
945
947
 
946
- # Save the original ID field before lookup (will be sanitized unless requested in fields)
947
948
  lookup_relation = [
948
- {
949
- "$addFields": {
950
- f"_original_{relation_field_name}": f"${relation_field_name}"
951
- }
952
- },
953
949
  {
954
950
  "$lookup": {
955
951
  "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.1
3
+ Version: 1.2.3
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
@@ -642,11 +642,11 @@ query = {
642
642
  "populate": ["creation_user", "modification_user"]
643
643
  }
644
644
 
645
- # Result will include (original ID automatically removed, replaced by populated object):
645
+ # Result will include (original ID removed, replaced by populated object):
646
646
  # {
647
647
  # "data": [{
648
648
  # "_id": "123",
649
- # "creation_user": { // Replaces the original ID field
649
+ # "creation_user_info": { // Populated object
650
650
  # "_id": "user_id_string",
651
651
  # "name": "John Doe",
652
652
  # "person": "person_id"
@@ -657,49 +657,69 @@ query = {
657
657
 
658
658
  **Reserved fields:**
659
659
 
660
- - `creation_user` → automatically populated, original ID is excluded
661
- - `modification_user` → automatically populated, original ID is excluded
660
+ - `creation_user` → automatically populated as `creation_user_info`, original ID is excluded
661
+ - `modification_user` → automatically populated as `modification_user_info`, 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
665
  ### Populate Field Sanitization (v1.2.1)
666
666
 
667
- By default, when you populate a relation, the original ID field is automatically removed from the response (Strapi-like behavior):
667
+ The behavior of original ID fields depends on whether they are being populated:
668
+
669
+ #### Without populate - ID field shown as-is (from database):
668
670
 
669
671
  ```python
670
- # Request with populate - establishment_id will be automatically excluded:
672
+ # No populate - field shown normally from database
673
+ query = {
674
+ "fields": ["name", "establishment_id"]
675
+ }
676
+
677
+ # Response:
678
+ # {
679
+ # "data": [{
680
+ # "_id": "123",
681
+ # "name": "Product",
682
+ # "establishment_id": "est_123" // Shown normally
683
+ # }]
684
+ # }
685
+ ```
686
+
687
+ #### With populate - ID field removed by default:
688
+
689
+ ```python
690
+ # With populate but no explicit fields request
671
691
  query = {
672
692
  "populate": ["establishment"]
673
693
  }
674
694
 
675
- # Response - establishment_id is not present:
695
+ # Response - establishment_id automatically excluded:
676
696
  # {
677
697
  # "data": [{
678
698
  # "_id": "123",
679
699
  # "name": "Product",
680
- # "establishment": { // Populated object replaces the ID
700
+ # "establishment": { // Populated object
681
701
  # "_id": "est_123",
682
702
  # "name": "Main Office"
683
703
  # }
684
- # // establishment_id is NOT here
685
704
  # }]
686
705
  # }
687
706
  ```
688
707
 
689
- **To keep both the ID and populated object, explicitly request it in `fields`:**
708
+ #### With populate AND explicit fields request - show both:
690
709
 
691
710
  ```python
711
+ # Request both populate and the original ID field explicitly
692
712
  query = {
693
713
  "populate": ["establishment"],
694
- "fields": ["name", "establishment_id"] # Request the ID explicitly
714
+ "fields": ["name", "establishment_id"] # Explicitly request the ID
695
715
  }
696
716
 
697
- # Response - both ID and populated object are present:
717
+ # Response - both are present:
698
718
  # {
699
719
  # "data": [{
700
720
  # "_id": "123",
701
721
  # "name": "Product",
702
- # "establishment_id": "est_123", // Now included
722
+ # "establishment_id": "est_123", // Shown because explicitly requested
703
723
  # "establishment": {
704
724
  # "_id": "est_123",
705
725
  # "name": "Main Office"
@@ -708,7 +728,32 @@ query = {
708
728
  # }
709
729
  ```
710
730
 
711
- **For audit fields:** The original ID is always excluded (no option to keep it, since there's no `creation_user_id` field).
731
+ #### Audit fields behavior:
732
+
733
+ ```python
734
+ # Without populate - original field shown
735
+ query = {
736
+ "fields": ["name", "creation_user"]
737
+ }
738
+ # Response:
739
+ # { "creation_user": "user_123" }
740
+
741
+ # With populate - original field removed, *_info shown instead
742
+ query = {
743
+ "populate": ["creation_user"]
744
+ }
745
+ # Response:
746
+ # { "creation_user_info": { "_id": "user_123", ... } }
747
+ # Note: creation_user field is NOT shown
748
+
749
+ # With populate AND explicit request for original - show both
750
+ query = {
751
+ "populate": ["creation_user"],
752
+ "fields": ["creation_user"] # Explicitly request the ID
753
+ }
754
+ # Response:
755
+ # { "creation_user": "user_123", "creation_user_info": { "_id": "user_123", ... } }
756
+ ```
712
757
 
713
758
  ### Custom Pipelines
714
759
 
@@ -1,4 +1,4 @@
1
- oplnk_python_utils/__init__.py,sha256=y5h3Ev7ozXiFlvQFoxcft0QOhznbgQBY6DaDZPeJNxg,354
1
+ oplnk_python_utils/__init__.py,sha256=NWhzsplSiDWjvhirbjOldb804fggxDmskwnOFLs1sww,354
2
2
  oplnk_python_utils/cache/__init__.py,sha256=UtCAGUH8WinNMeh-MIOLPZeRMUn5_6Jn_QC2iKWvIN0,646
3
3
  oplnk_python_utils/cache/cache_utils.py,sha256=Ln4sRgM1UELekCfMYZdNdUVecsshaqrcoUrvHRfj4jE,8597
4
4
  oplnk_python_utils/core/__init__.py,sha256=HmT7BR9WCGcu47Bd-eMdSSTImKnzsaalzYVThgTuyMA,253
@@ -6,10 +6,10 @@ oplnk_python_utils/core/data.py,sha256=BwOIGeS4c7gGOOUcx-2RpFJsznjAtBVETRv63A_3P
6
6
  oplnk_python_utils/core/parsing.py,sha256=EdI_uQJWalXL7yen5iyQ6J5PRLgz6Xi_szWaUDOhlgQ,5190
7
7
  oplnk_python_utils/core/serialization.py,sha256=B4g3x6yGjmXySNtOHqqFrq8AlguYz4gldxCJY21UNdQ,1164
8
8
  oplnk_python_utils/query/__init__.py,sha256=dd-PRf5fkoQvAbcONumwrRveS_Ejnw-3Q0hA7ZcF6Ic,147
9
- oplnk_python_utils/query/query_builder.py,sha256=4V-d1hcgdU_4aiuqTuCv9WYZKeGxIUCcJY1Ji4HunJo,60300
9
+ oplnk_python_utils/query/query_builder.py,sha256=ax78f0g_2KFXScsPs8pN97NxcT_paG6mzZDWIe7yv9w,59770
10
10
  oplnk_python_utils/reports/__init__.py,sha256=CPxWaRgXt9sHK_6mQwsXnUxg9yzJ2GHSlQ4lVNpkanY,94
11
- oplnk_python_utils-1.2.1.dist-info/licenses/LICENSE,sha256=JgkAUZElZ6gNf7C2aZPq5-GEjUG2t_yYAKvuhy1OKy0,1090
12
- oplnk_python_utils-1.2.1.dist-info/METADATA,sha256=EIMDkUX9sU7eflp7cFnZs72S4_PrmUhDs7g-BwPWjH8,24003
13
- oplnk_python_utils-1.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
- oplnk_python_utils-1.2.1.dist-info/top_level.txt,sha256=TPZ-emvWspdR-OxTNOvQt-cDrZ4m7P4RMnxsXHOeorc,19
15
- oplnk_python_utils-1.2.1.dist-info/RECORD,,
11
+ oplnk_python_utils-1.2.3.dist-info/licenses/LICENSE,sha256=JgkAUZElZ6gNf7C2aZPq5-GEjUG2t_yYAKvuhy1OKy0,1090
12
+ oplnk_python_utils-1.2.3.dist-info/METADATA,sha256=QExNyfvgs-Dzp5zCekv41fvmDtbdsVJMZAXnjVCwhG8,24924
13
+ oplnk_python_utils-1.2.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
+ oplnk_python_utils-1.2.3.dist-info/top_level.txt,sha256=TPZ-emvWspdR-OxTNOvQt-cDrZ4m7P4RMnxsXHOeorc,19
15
+ oplnk_python_utils-1.2.3.dist-info/RECORD,,