datamaker-py 0.7.0__tar.gz → 0.9.0__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.
- {datamaker_py-0.7.0 → datamaker_py-0.9.0}/PKG-INFO +1 -1
- {datamaker_py-0.7.0 → datamaker_py-0.9.0}/pyproject.toml +1 -1
- {datamaker_py-0.7.0 → datamaker_py-0.9.0}/src/datamaker/main.py +288 -1
- {datamaker_py-0.7.0 → datamaker_py-0.9.0}/src/datamaker/routes/__init__.py +2 -0
- datamaker_py-0.9.0/src/datamaker/routes/keymaps.py +221 -0
- datamaker_py-0.9.0/src/datamaker/routes/masking_policies.py +130 -0
- datamaker_py-0.9.0/src/datamaker/routes/plans.py +72 -0
- {datamaker_py-0.7.0 → datamaker_py-0.9.0}/src/datamaker_py.egg-info/PKG-INFO +1 -1
- {datamaker_py-0.7.0 → datamaker_py-0.9.0}/src/datamaker_py.egg-info/SOURCES.txt +3 -0
- {datamaker_py-0.7.0 → datamaker_py-0.9.0}/tests/test_routes.py +132 -0
- {datamaker_py-0.7.0 → datamaker_py-0.9.0}/LICENSE +0 -0
- {datamaker_py-0.7.0 → datamaker_py-0.9.0}/README.md +0 -0
- {datamaker_py-0.7.0 → datamaker_py-0.9.0}/setup.cfg +0 -0
- {datamaker_py-0.7.0 → datamaker_py-0.9.0}/src/datamaker/__init__.py +0 -0
- {datamaker_py-0.7.0 → datamaker_py-0.9.0}/src/datamaker/core.py +0 -0
- {datamaker_py-0.7.0 → datamaker_py-0.9.0}/src/datamaker/error.py +0 -0
- {datamaker_py-0.7.0 → datamaker_py-0.9.0}/src/datamaker/routes/api_keys.py +0 -0
- {datamaker_py-0.7.0 → datamaker_py-0.9.0}/src/datamaker/routes/base.py +0 -0
- {datamaker_py-0.7.0 → datamaker_py-0.9.0}/src/datamaker/routes/connections.py +0 -0
- {datamaker_py-0.7.0 → datamaker_py-0.9.0}/src/datamaker/routes/custom_types.py +0 -0
- {datamaker_py-0.7.0 → datamaker_py-0.9.0}/src/datamaker/routes/export_and_validation.py +0 -0
- {datamaker_py-0.7.0 → datamaker_py-0.9.0}/src/datamaker/routes/folders_and_utils.py +0 -0
- {datamaker_py-0.7.0 → datamaker_py-0.9.0}/src/datamaker/routes/generation.py +0 -0
- {datamaker_py-0.7.0 → datamaker_py-0.9.0}/src/datamaker/routes/projects.py +0 -0
- {datamaker_py-0.7.0 → datamaker_py-0.9.0}/src/datamaker/routes/scenario_files.py +0 -0
- {datamaker_py-0.7.0 → datamaker_py-0.9.0}/src/datamaker/routes/sets.py +0 -0
- {datamaker_py-0.7.0 → datamaker_py-0.9.0}/src/datamaker/routes/teams.py +0 -0
- {datamaker_py-0.7.0 → datamaker_py-0.9.0}/src/datamaker/routes/templates.py +0 -0
- {datamaker_py-0.7.0 → datamaker_py-0.9.0}/src/datamaker/routes/users.py +0 -0
- {datamaker_py-0.7.0 → datamaker_py-0.9.0}/src/datamaker/template.py +0 -0
- {datamaker_py-0.7.0 → datamaker_py-0.9.0}/src/datamaker_py.egg-info/dependency_links.txt +0 -0
- {datamaker_py-0.7.0 → datamaker_py-0.9.0}/src/datamaker_py.egg-info/requires.txt +0 -0
- {datamaker_py-0.7.0 → datamaker_py-0.9.0}/src/datamaker_py.egg-info/top_level.txt +0 -0
- {datamaker_py-0.7.0 → datamaker_py-0.9.0}/tests/test_integration.py +0 -0
- {datamaker_py-0.7.0 → datamaker_py-0.9.0}/tests/test_main.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from dotenv import load_dotenv
|
|
3
|
-
from typing import Optional, Dict
|
|
3
|
+
from typing import Optional, Dict, List
|
|
4
4
|
from .routes.base import BaseClient
|
|
5
5
|
from .routes.generation import GenerationClient
|
|
6
6
|
from .routes.templates import TemplatesClient
|
|
@@ -22,6 +22,9 @@ from .routes.folders_and_utils import (
|
|
|
22
22
|
from .routes.export_and_validation import ExportClient, ValidationClient
|
|
23
23
|
from .routes.scenario_files import ScenarioFilesClient
|
|
24
24
|
from .routes.sets import SetsClient
|
|
25
|
+
from .routes.plans import PlansClient
|
|
26
|
+
from .routes.masking_policies import MaskingPoliciesClient
|
|
27
|
+
from .routes.keymaps import KeyMapsClient
|
|
25
28
|
|
|
26
29
|
load_dotenv()
|
|
27
30
|
|
|
@@ -70,6 +73,11 @@ class DataMaker:
|
|
|
70
73
|
api_key, default_headers, base_url, verify
|
|
71
74
|
)
|
|
72
75
|
self._sets = SetsClient(api_key, default_headers, base_url, verify)
|
|
76
|
+
self._keymaps = KeyMapsClient(api_key, default_headers, base_url, verify)
|
|
77
|
+
self._plans = PlansClient(api_key, default_headers, base_url, verify)
|
|
78
|
+
self._masking_policies = MaskingPoliciesClient(
|
|
79
|
+
api_key, default_headers, base_url, verify
|
|
80
|
+
)
|
|
73
81
|
|
|
74
82
|
# Maintain backward compatibility
|
|
75
83
|
self.api_key = self._generation.api_key
|
|
@@ -793,8 +801,272 @@ class DataMaker:
|
|
|
793
801
|
project_id=project_id,
|
|
794
802
|
)
|
|
795
803
|
|
|
804
|
+
def get_keymaps(self, project_id: Optional[str] = None):
|
|
805
|
+
"""List key maps for a project: one row per (mapName, object).
|
|
806
|
+
|
|
807
|
+
Args:
|
|
808
|
+
project_id: Optional project ID. Falls back to DATAMAKER_PROJECT_ID.
|
|
809
|
+
|
|
810
|
+
Returns:
|
|
811
|
+
A list of dictionaries with mapName, object, entryCount, updatedAt.
|
|
812
|
+
"""
|
|
813
|
+
return self._keymaps.get_keymaps(project_id)
|
|
814
|
+
|
|
815
|
+
def keymap_put(
|
|
816
|
+
self,
|
|
817
|
+
map_name: str,
|
|
818
|
+
object: str,
|
|
819
|
+
entries: Dict[str, str],
|
|
820
|
+
run_id: Optional[str] = None,
|
|
821
|
+
project_id: Optional[str] = None,
|
|
822
|
+
):
|
|
823
|
+
"""Record old-to-new key mappings in a named key map (batch upsert).
|
|
824
|
+
|
|
825
|
+
Use after creating records in a target system to remember which source
|
|
826
|
+
key became which target key. Writing the same old key again overwrites
|
|
827
|
+
its new key. Batches are capped at 5000 entries per call.
|
|
828
|
+
|
|
829
|
+
Args:
|
|
830
|
+
map_name: Logical map name, e.g. "sap-material-migration".
|
|
831
|
+
object: The domain object type, e.g. "Material".
|
|
832
|
+
entries: Mapping of old key to new key.
|
|
833
|
+
run_id: Optional run/job id that minted these keys.
|
|
834
|
+
project_id: Optional project ID. Falls back to DATAMAKER_PROJECT_ID.
|
|
835
|
+
|
|
836
|
+
Returns:
|
|
837
|
+
A dictionary with mapName, object and upserted (count).
|
|
838
|
+
|
|
839
|
+
Example:
|
|
840
|
+
>>> dm = DataMaker()
|
|
841
|
+
>>> dm.keymap_put(
|
|
842
|
+
... "sap-material-migration",
|
|
843
|
+
... "Material",
|
|
844
|
+
... {"MAT-001": "700001", "MAT-002": "700002"},
|
|
845
|
+
... )
|
|
846
|
+
"""
|
|
847
|
+
return self._keymaps.keymap_put(
|
|
848
|
+
map_name=map_name,
|
|
849
|
+
object=object,
|
|
850
|
+
entries=entries,
|
|
851
|
+
run_id=run_id,
|
|
852
|
+
project_id=project_id,
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
def keymap_lookup(
|
|
856
|
+
self,
|
|
857
|
+
map_name: str,
|
|
858
|
+
object: str,
|
|
859
|
+
old_keys: List[str],
|
|
860
|
+
project_id: Optional[str] = None,
|
|
861
|
+
):
|
|
862
|
+
"""Translate source-system keys to target-system keys (batch lookup).
|
|
863
|
+
|
|
864
|
+
Use when generating or migrating data that references records migrated
|
|
865
|
+
earlier. Lookups are capped at 5000 keys per call.
|
|
866
|
+
|
|
867
|
+
Args:
|
|
868
|
+
map_name: The key map to look up in.
|
|
869
|
+
object: The domain object type, e.g. "Material".
|
|
870
|
+
old_keys: Source-system keys to translate.
|
|
871
|
+
project_id: Optional project ID. Falls back to DATAMAKER_PROJECT_ID.
|
|
872
|
+
|
|
873
|
+
Returns:
|
|
874
|
+
A dictionary with mappings (found) and missing (no mapping yet).
|
|
875
|
+
|
|
876
|
+
Example:
|
|
877
|
+
>>> dm = DataMaker()
|
|
878
|
+
>>> result = dm.keymap_lookup(
|
|
879
|
+
... "sap-material-migration", "Material", ["MAT-001", "MAT-999"]
|
|
880
|
+
... )
|
|
881
|
+
"""
|
|
882
|
+
return self._keymaps.keymap_lookup(
|
|
883
|
+
map_name=map_name,
|
|
884
|
+
object=object,
|
|
885
|
+
old_keys=old_keys,
|
|
886
|
+
project_id=project_id,
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
def get_keymap_entries(
|
|
890
|
+
self,
|
|
891
|
+
map_name: str,
|
|
892
|
+
object: Optional[str] = None,
|
|
893
|
+
page: int = 1,
|
|
894
|
+
page_size: int = 100,
|
|
895
|
+
project_id: Optional[str] = None,
|
|
896
|
+
):
|
|
897
|
+
"""Fetch a page of a key map's entries for inspection.
|
|
898
|
+
|
|
899
|
+
Args:
|
|
900
|
+
map_name: The key map to read.
|
|
901
|
+
object: Optional domain object type filter.
|
|
902
|
+
page: 1-based page number.
|
|
903
|
+
page_size: Entries per page (server-capped at 500).
|
|
904
|
+
project_id: Optional project ID. Falls back to DATAMAKER_PROJECT_ID.
|
|
905
|
+
|
|
906
|
+
Returns:
|
|
907
|
+
A dictionary with entries, total, page and pageSize.
|
|
908
|
+
"""
|
|
909
|
+
return self._keymaps.get_keymap_entries(
|
|
910
|
+
map_name=map_name,
|
|
911
|
+
object=object,
|
|
912
|
+
page=page,
|
|
913
|
+
page_size=page_size,
|
|
914
|
+
project_id=project_id,
|
|
915
|
+
)
|
|
916
|
+
|
|
917
|
+
def delete_keymap(
|
|
918
|
+
self,
|
|
919
|
+
map_name: str,
|
|
920
|
+
object: Optional[str] = None,
|
|
921
|
+
project_id: Optional[str] = None,
|
|
922
|
+
):
|
|
923
|
+
"""Drop a key map (all its entries), optionally one object type only.
|
|
924
|
+
|
|
925
|
+
Args:
|
|
926
|
+
map_name: The key map to delete.
|
|
927
|
+
object: Optional domain object type to scope the delete to.
|
|
928
|
+
project_id: Optional project ID. Falls back to DATAMAKER_PROJECT_ID.
|
|
929
|
+
|
|
930
|
+
Returns:
|
|
931
|
+
Confirmation response with the deleted entry count.
|
|
932
|
+
"""
|
|
933
|
+
return self._keymaps.delete_keymap(
|
|
934
|
+
map_name=map_name,
|
|
935
|
+
object=object,
|
|
936
|
+
project_id=project_id,
|
|
937
|
+
)
|
|
938
|
+
|
|
796
939
|
# =================== PROPERTY ACCESS TO CLIENTS ===================
|
|
797
940
|
# For advanced users who want direct access to specific clients
|
|
941
|
+
|
|
942
|
+
# =================== PLAN METHODS ===================
|
|
943
|
+
def get_plans(self):
|
|
944
|
+
"""List the plans in the caller's active project.
|
|
945
|
+
|
|
946
|
+
Returns:
|
|
947
|
+
A list of plan dictionaries.
|
|
948
|
+
"""
|
|
949
|
+
return self._plans.get_plans()
|
|
950
|
+
|
|
951
|
+
def get_plan(self, plan_id: str):
|
|
952
|
+
"""Get a single plan by ID, including its full spec and history.
|
|
953
|
+
|
|
954
|
+
Args:
|
|
955
|
+
plan_id: The unique identifier of the plan.
|
|
956
|
+
|
|
957
|
+
Returns:
|
|
958
|
+
The plan dictionary.
|
|
959
|
+
"""
|
|
960
|
+
return self._plans.get_plan(plan_id)
|
|
961
|
+
|
|
962
|
+
def update_plan(self, plan_id: str, **fields):
|
|
963
|
+
"""Update a plan.
|
|
964
|
+
|
|
965
|
+
Args:
|
|
966
|
+
plan_id: The unique identifier of the plan.
|
|
967
|
+
**fields: Fields to change, e.g. title, summary, status.
|
|
968
|
+
|
|
969
|
+
Returns:
|
|
970
|
+
The updated plan dictionary.
|
|
971
|
+
"""
|
|
972
|
+
return self._plans.update_plan(plan_id, **fields)
|
|
973
|
+
|
|
974
|
+
def delete_plan(self, plan_id: str):
|
|
975
|
+
"""Delete a plan.
|
|
976
|
+
|
|
977
|
+
Args:
|
|
978
|
+
plan_id: The unique identifier of the plan.
|
|
979
|
+
|
|
980
|
+
Returns:
|
|
981
|
+
``{"success": True}``. Note this differs from the other resources,
|
|
982
|
+
which answer ``{"message": ...}``.
|
|
983
|
+
"""
|
|
984
|
+
return self._plans.delete_plan(plan_id)
|
|
985
|
+
|
|
986
|
+
# =================== MASKING POLICY METHODS ===================
|
|
987
|
+
def get_masking_policies(self, project_id: Optional[str] = None):
|
|
988
|
+
"""Get the masking policies in the caller's project/team scope.
|
|
989
|
+
|
|
990
|
+
Args:
|
|
991
|
+
project_id: Optional project ID to scope the listing to. Falls back
|
|
992
|
+
to the DATAMAKER_PROJECT_ID env var.
|
|
993
|
+
|
|
994
|
+
Returns:
|
|
995
|
+
A list of masking policy dictionaries.
|
|
996
|
+
"""
|
|
997
|
+
return self._masking_policies.get_masking_policies(project_id)
|
|
998
|
+
|
|
999
|
+
def get_masking_policy(self, policy_id: str):
|
|
1000
|
+
"""Get a single masking policy by ID.
|
|
1001
|
+
|
|
1002
|
+
Args:
|
|
1003
|
+
policy_id: The unique identifier of the policy.
|
|
1004
|
+
|
|
1005
|
+
Returns:
|
|
1006
|
+
The masking policy dictionary.
|
|
1007
|
+
"""
|
|
1008
|
+
return self._masking_policies.get_masking_policy(policy_id)
|
|
1009
|
+
|
|
1010
|
+
def create_masking_policy(
|
|
1011
|
+
self,
|
|
1012
|
+
name: str,
|
|
1013
|
+
fields,
|
|
1014
|
+
description: Optional[str] = None,
|
|
1015
|
+
consistent: Optional[bool] = None,
|
|
1016
|
+
reversible: Optional[bool] = None,
|
|
1017
|
+
key_map_name: Optional[str] = None,
|
|
1018
|
+
project_id: Optional[str] = None,
|
|
1019
|
+
):
|
|
1020
|
+
"""Create a masking policy.
|
|
1021
|
+
|
|
1022
|
+
Args:
|
|
1023
|
+
name: The policy name.
|
|
1024
|
+
fields: The field rule list.
|
|
1025
|
+
description: Optional description.
|
|
1026
|
+
consistent: Same input masks to the same output. Omit to take the
|
|
1027
|
+
API's default.
|
|
1028
|
+
reversible: Record the mapping so masking can be reversed. Needs
|
|
1029
|
+
key_map_name to be useful.
|
|
1030
|
+
key_map_name: The KeyMap a reversible policy mints mappings into.
|
|
1031
|
+
project_id: Project to create it in. Falls back to
|
|
1032
|
+
DATAMAKER_PROJECT_ID.
|
|
1033
|
+
|
|
1034
|
+
Returns:
|
|
1035
|
+
The created masking policy dictionary.
|
|
1036
|
+
"""
|
|
1037
|
+
return self._masking_policies.create_masking_policy(
|
|
1038
|
+
name=name,
|
|
1039
|
+
fields=fields,
|
|
1040
|
+
description=description,
|
|
1041
|
+
consistent=consistent,
|
|
1042
|
+
reversible=reversible,
|
|
1043
|
+
key_map_name=key_map_name,
|
|
1044
|
+
project_id=project_id,
|
|
1045
|
+
)
|
|
1046
|
+
|
|
1047
|
+
def update_masking_policy(self, policy_id: str, **fields):
|
|
1048
|
+
"""Update a masking policy.
|
|
1049
|
+
|
|
1050
|
+
Args:
|
|
1051
|
+
policy_id: The unique identifier of the policy.
|
|
1052
|
+
**fields: Fields to change. Use API spellings, e.g. keyMapName.
|
|
1053
|
+
|
|
1054
|
+
Returns:
|
|
1055
|
+
The updated masking policy dictionary.
|
|
1056
|
+
"""
|
|
1057
|
+
return self._masking_policies.update_masking_policy(policy_id, **fields)
|
|
1058
|
+
|
|
1059
|
+
def delete_masking_policy(self, policy_id: str):
|
|
1060
|
+
"""Delete a masking policy.
|
|
1061
|
+
|
|
1062
|
+
Args:
|
|
1063
|
+
policy_id: The unique identifier of the policy.
|
|
1064
|
+
|
|
1065
|
+
Returns:
|
|
1066
|
+
``{"message": "Masking policy deleted"}``.
|
|
1067
|
+
"""
|
|
1068
|
+
return self._masking_policies.delete_masking_policy(policy_id)
|
|
1069
|
+
|
|
798
1070
|
@property
|
|
799
1071
|
def generation(self):
|
|
800
1072
|
"""Access to generation client."""
|
|
@@ -884,3 +1156,18 @@ class DataMaker:
|
|
|
884
1156
|
def sets(self):
|
|
885
1157
|
"""Access to sets client."""
|
|
886
1158
|
return self._sets
|
|
1159
|
+
|
|
1160
|
+
@property
|
|
1161
|
+
def keymaps(self):
|
|
1162
|
+
"""Access to key maps client."""
|
|
1163
|
+
return self._keymaps
|
|
1164
|
+
|
|
1165
|
+
@property
|
|
1166
|
+
def plans(self):
|
|
1167
|
+
"""Access to plans client."""
|
|
1168
|
+
return self._plans
|
|
1169
|
+
|
|
1170
|
+
@property
|
|
1171
|
+
def masking_policies(self):
|
|
1172
|
+
"""Access to masking policies client."""
|
|
1173
|
+
return self._masking_policies
|
|
@@ -13,6 +13,7 @@ from .folders_and_utils import TemplateFoldersClient, ShortcutsClient, FeedbackC
|
|
|
13
13
|
from .export_and_validation import ExportClient, ValidationClient
|
|
14
14
|
from .scenario_files import ScenarioFilesClient
|
|
15
15
|
from .sets import SetsClient
|
|
16
|
+
from .keymaps import KeyMapsClient
|
|
16
17
|
|
|
17
18
|
__all__ = [
|
|
18
19
|
"BaseClient",
|
|
@@ -34,4 +35,5 @@ __all__ = [
|
|
|
34
35
|
"ValidationClient",
|
|
35
36
|
"ScenarioFilesClient",
|
|
36
37
|
"SetsClient",
|
|
38
|
+
"KeyMapsClient",
|
|
37
39
|
]
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Client for key map operations - old-to-new key mappings for migrations.
|
|
2
|
+
|
|
3
|
+
A "key map" records which source-system key became which target-system key
|
|
4
|
+
during a migration (e.g. legacy material number to new material number).
|
|
5
|
+
Unlike a set (one JSON blob), every mapping is its own row on the server, so
|
|
6
|
+
lookups are indexed and batch writes from parallel load workers are
|
|
7
|
+
concurrency-safe. Entries are unique per (project, map name, object, old key)
|
|
8
|
+
and the last write wins for the new key.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from typing import Dict, List, Optional
|
|
13
|
+
from .base import BaseClient
|
|
14
|
+
from ..error import DataMakerError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class KeyMapsClient(BaseClient):
|
|
18
|
+
"""Client for key map operations (old-to-new key mappings)."""
|
|
19
|
+
|
|
20
|
+
def get_keymaps(self, project_id: Optional[str] = None) -> List[Dict]:
|
|
21
|
+
"""List key maps for a project: one row per (mapName, object).
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
project_id: Optional project ID to scope the listing to. Falls back
|
|
25
|
+
to the DATAMAKER_PROJECT_ID env var.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
A list of dictionaries with ``mapName``, ``object``, ``entryCount``
|
|
29
|
+
and ``updatedAt``.
|
|
30
|
+
"""
|
|
31
|
+
project_id = project_id or os.environ.get("DATAMAKER_PROJECT_ID")
|
|
32
|
+
|
|
33
|
+
endpoint = "/keymaps"
|
|
34
|
+
if project_id:
|
|
35
|
+
endpoint += f"?projectId={project_id}"
|
|
36
|
+
|
|
37
|
+
response = self._make_request("GET", endpoint)
|
|
38
|
+
return response.json()
|
|
39
|
+
|
|
40
|
+
def keymap_put(
|
|
41
|
+
self,
|
|
42
|
+
map_name: str,
|
|
43
|
+
object: str,
|
|
44
|
+
entries: Dict[str, str],
|
|
45
|
+
run_id: Optional[str] = None,
|
|
46
|
+
project_id: Optional[str] = None,
|
|
47
|
+
) -> Dict:
|
|
48
|
+
"""Record old-to-new key mappings in a named key map (batch upsert).
|
|
49
|
+
|
|
50
|
+
Use after creating records in a target system to remember which source
|
|
51
|
+
key became which target key, so dependent data can reference the right
|
|
52
|
+
keys later. Writing the same old key again overwrites its new key.
|
|
53
|
+
Batches are capped at 5000 entries per call - page larger writes.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
map_name: Logical map name grouping the entries, e.g.
|
|
57
|
+
"sap-material-migration".
|
|
58
|
+
object: The domain object type the keys belong to, e.g. "Material"
|
|
59
|
+
or "BusinessPartner".
|
|
60
|
+
entries: Mapping of old key to new key.
|
|
61
|
+
run_id: Optional run/job id that minted these keys.
|
|
62
|
+
project_id: Optional project ID. Falls back to DATAMAKER_PROJECT_ID.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
A dictionary with ``mapName``, ``object`` and ``upserted`` (count).
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
DataMakerError: If ``map_name``, ``object`` or ``entries`` is empty.
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
>>> dm = DataMaker()
|
|
72
|
+
>>> dm.keymap_put(
|
|
73
|
+
... "sap-material-migration",
|
|
74
|
+
... "Material",
|
|
75
|
+
... {"MAT-001": "700001", "MAT-002": "700002"},
|
|
76
|
+
... )
|
|
77
|
+
"""
|
|
78
|
+
if not map_name:
|
|
79
|
+
raise DataMakerError("map_name is required to put key map entries.")
|
|
80
|
+
if not object:
|
|
81
|
+
raise DataMakerError("object is required to put key map entries.")
|
|
82
|
+
if not entries:
|
|
83
|
+
raise DataMakerError("entries must not be empty.")
|
|
84
|
+
|
|
85
|
+
project_id = project_id or os.environ.get("DATAMAKER_PROJECT_ID")
|
|
86
|
+
|
|
87
|
+
payload: Dict = {
|
|
88
|
+
"mapName": map_name,
|
|
89
|
+
"object": object,
|
|
90
|
+
"entries": [
|
|
91
|
+
{"oldKey": old_key, "newKey": new_key}
|
|
92
|
+
for old_key, new_key in entries.items()
|
|
93
|
+
],
|
|
94
|
+
}
|
|
95
|
+
if run_id is not None:
|
|
96
|
+
payload["runId"] = run_id
|
|
97
|
+
if project_id:
|
|
98
|
+
payload["projectId"] = project_id
|
|
99
|
+
|
|
100
|
+
response = self._make_request("POST", "/keymaps/entries", json=payload)
|
|
101
|
+
return response.json()
|
|
102
|
+
|
|
103
|
+
def keymap_lookup(
|
|
104
|
+
self,
|
|
105
|
+
map_name: str,
|
|
106
|
+
object: str,
|
|
107
|
+
old_keys: List[str],
|
|
108
|
+
project_id: Optional[str] = None,
|
|
109
|
+
) -> Dict:
|
|
110
|
+
"""Translate source-system keys to target-system keys (batch lookup).
|
|
111
|
+
|
|
112
|
+
Use when generating or migrating data that references records migrated
|
|
113
|
+
earlier (e.g. orders that need the NEW material numbers for OLD ones).
|
|
114
|
+
Lookups are capped at 5000 keys per call - page larger reads.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
map_name: The key map to look up in.
|
|
118
|
+
object: The domain object type, e.g. "Material".
|
|
119
|
+
old_keys: Source-system keys to translate.
|
|
120
|
+
project_id: Optional project ID. Falls back to DATAMAKER_PROJECT_ID.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
A dictionary with ``mappings`` (old key to new key for the keys
|
|
124
|
+
that were found) and ``missing`` (keys with no mapping yet).
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
DataMakerError: If ``map_name``, ``object`` or ``old_keys`` is empty.
|
|
128
|
+
|
|
129
|
+
Example:
|
|
130
|
+
>>> dm = DataMaker()
|
|
131
|
+
>>> result = dm.keymap_lookup(
|
|
132
|
+
... "sap-material-migration", "Material", ["MAT-001", "MAT-999"]
|
|
133
|
+
... )
|
|
134
|
+
>>> result["mappings"]
|
|
135
|
+
{'MAT-001': '700001'}
|
|
136
|
+
>>> result["missing"]
|
|
137
|
+
['MAT-999']
|
|
138
|
+
"""
|
|
139
|
+
if not map_name:
|
|
140
|
+
raise DataMakerError("map_name is required to look up key mappings.")
|
|
141
|
+
if not object:
|
|
142
|
+
raise DataMakerError("object is required to look up key mappings.")
|
|
143
|
+
if not old_keys:
|
|
144
|
+
raise DataMakerError("old_keys must not be empty.")
|
|
145
|
+
|
|
146
|
+
project_id = project_id or os.environ.get("DATAMAKER_PROJECT_ID")
|
|
147
|
+
|
|
148
|
+
payload: Dict = {
|
|
149
|
+
"mapName": map_name,
|
|
150
|
+
"object": object,
|
|
151
|
+
"oldKeys": old_keys,
|
|
152
|
+
}
|
|
153
|
+
if project_id:
|
|
154
|
+
payload["projectId"] = project_id
|
|
155
|
+
|
|
156
|
+
response = self._make_request("POST", "/keymaps/lookup", json=payload)
|
|
157
|
+
return response.json()
|
|
158
|
+
|
|
159
|
+
def get_keymap_entries(
|
|
160
|
+
self,
|
|
161
|
+
map_name: str,
|
|
162
|
+
object: Optional[str] = None,
|
|
163
|
+
page: int = 1,
|
|
164
|
+
page_size: int = 100,
|
|
165
|
+
project_id: Optional[str] = None,
|
|
166
|
+
) -> Dict:
|
|
167
|
+
"""Fetch a page of a key map's entries for inspection.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
map_name: The key map to read.
|
|
171
|
+
object: Optional domain object type filter.
|
|
172
|
+
page: 1-based page number.
|
|
173
|
+
page_size: Entries per page (server-capped at 500).
|
|
174
|
+
project_id: Optional project ID. Falls back to DATAMAKER_PROJECT_ID.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
A dictionary with ``entries``, ``total``, ``page`` and ``pageSize``.
|
|
178
|
+
"""
|
|
179
|
+
project_id = project_id or os.environ.get("DATAMAKER_PROJECT_ID")
|
|
180
|
+
|
|
181
|
+
params = [f"page={page}", f"pageSize={page_size}"]
|
|
182
|
+
if object:
|
|
183
|
+
params.append(f"object={object}")
|
|
184
|
+
if project_id:
|
|
185
|
+
params.append(f"projectId={project_id}")
|
|
186
|
+
|
|
187
|
+
endpoint = f"/keymaps/{map_name}/entries?" + "&".join(params)
|
|
188
|
+
response = self._make_request("GET", endpoint)
|
|
189
|
+
return response.json()
|
|
190
|
+
|
|
191
|
+
def delete_keymap(
|
|
192
|
+
self,
|
|
193
|
+
map_name: str,
|
|
194
|
+
object: Optional[str] = None,
|
|
195
|
+
project_id: Optional[str] = None,
|
|
196
|
+
) -> Dict:
|
|
197
|
+
"""Drop a key map (all its entries).
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
map_name: The key map to delete.
|
|
201
|
+
object: Optional domain object type - when given, only that
|
|
202
|
+
object's entries are dropped.
|
|
203
|
+
project_id: Optional project ID. Falls back to DATAMAKER_PROJECT_ID.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
Confirmation response with the deleted entry count.
|
|
207
|
+
"""
|
|
208
|
+
project_id = project_id or os.environ.get("DATAMAKER_PROJECT_ID")
|
|
209
|
+
|
|
210
|
+
params = []
|
|
211
|
+
if object:
|
|
212
|
+
params.append(f"object={object}")
|
|
213
|
+
if project_id:
|
|
214
|
+
params.append(f"projectId={project_id}")
|
|
215
|
+
|
|
216
|
+
endpoint = f"/keymaps/{map_name}"
|
|
217
|
+
if params:
|
|
218
|
+
endpoint += "?" + "&".join(params)
|
|
219
|
+
|
|
220
|
+
response = self._make_request("DELETE", endpoint)
|
|
221
|
+
return response.json()
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Client for masking policy operations.
|
|
2
|
+
|
|
3
|
+
A masking policy is the set of field rules a masked generation or export
|
|
4
|
+
applies. Two flags decide how it behaves, and they are easy to confuse:
|
|
5
|
+
|
|
6
|
+
``consistent``
|
|
7
|
+
The same input masks to the same output within the policy, so joins across
|
|
8
|
+
masked tables still line up.
|
|
9
|
+
|
|
10
|
+
``reversible``
|
|
11
|
+
The mapping is recorded so it can be reversed later. A reversible policy
|
|
12
|
+
mints its mappings into the KeyMap named by ``key_map_name``, which is why
|
|
13
|
+
that argument only means anything alongside it.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
from typing import Any, Dict, List, Optional
|
|
18
|
+
|
|
19
|
+
from .base import BaseClient
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MaskingPoliciesClient(BaseClient):
|
|
23
|
+
"""Client for masking policy operations."""
|
|
24
|
+
|
|
25
|
+
def get_masking_policies(
|
|
26
|
+
self, project_id: Optional[str] = None
|
|
27
|
+
) -> List[Dict[str, Any]]:
|
|
28
|
+
"""Fetch the masking policies in the caller's project/team scope.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
project_id: Optional project ID to scope the listing to. Falls back
|
|
32
|
+
to the DATAMAKER_PROJECT_ID env var.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
A list of masking policy dictionaries.
|
|
36
|
+
"""
|
|
37
|
+
project_id = project_id or os.environ.get("DATAMAKER_PROJECT_ID")
|
|
38
|
+
|
|
39
|
+
endpoint = "/masking-policies"
|
|
40
|
+
if project_id:
|
|
41
|
+
endpoint += f"?projectId={project_id}"
|
|
42
|
+
|
|
43
|
+
response = self._make_request("GET", endpoint)
|
|
44
|
+
return response.json()
|
|
45
|
+
|
|
46
|
+
def get_masking_policy(self, policy_id: str) -> Dict[str, Any]:
|
|
47
|
+
"""Get a single masking policy by ID.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
policy_id: The policy's ID.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
The masking policy dictionary.
|
|
54
|
+
"""
|
|
55
|
+
response = self._make_request("GET", f"/masking-policies/{policy_id}")
|
|
56
|
+
return response.json()
|
|
57
|
+
|
|
58
|
+
def create_masking_policy(
|
|
59
|
+
self,
|
|
60
|
+
name: str,
|
|
61
|
+
fields: Any,
|
|
62
|
+
description: Optional[str] = None,
|
|
63
|
+
consistent: Optional[bool] = None,
|
|
64
|
+
reversible: Optional[bool] = None,
|
|
65
|
+
key_map_name: Optional[str] = None,
|
|
66
|
+
project_id: Optional[str] = None,
|
|
67
|
+
) -> Dict[str, Any]:
|
|
68
|
+
"""Create a masking policy.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
name: The policy name.
|
|
72
|
+
fields: The field rule list. Shape varies by rule type.
|
|
73
|
+
description: Optional description.
|
|
74
|
+
consistent: Same input masks to the same output. Defaults to the
|
|
75
|
+
API's own default (True) when omitted.
|
|
76
|
+
reversible: Record the mapping so masking can be reversed. Requires
|
|
77
|
+
``key_map_name`` to be useful.
|
|
78
|
+
key_map_name: The KeyMap a reversible policy mints mappings into.
|
|
79
|
+
project_id: Project to create it in. Falls back to
|
|
80
|
+
DATAMAKER_PROJECT_ID.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
The created masking policy dictionary.
|
|
84
|
+
"""
|
|
85
|
+
project_id = project_id or os.environ.get("DATAMAKER_PROJECT_ID")
|
|
86
|
+
|
|
87
|
+
# Only send what the caller set: omitting a key lets the API apply its
|
|
88
|
+
# own default, whereas sending null would override it.
|
|
89
|
+
payload: Dict[str, Any] = {"name": name, "fields": fields}
|
|
90
|
+
if description is not None:
|
|
91
|
+
payload["description"] = description
|
|
92
|
+
if consistent is not None:
|
|
93
|
+
payload["consistent"] = consistent
|
|
94
|
+
if reversible is not None:
|
|
95
|
+
payload["reversible"] = reversible
|
|
96
|
+
if key_map_name is not None:
|
|
97
|
+
payload["keyMapName"] = key_map_name
|
|
98
|
+
if project_id:
|
|
99
|
+
payload["projectId"] = project_id
|
|
100
|
+
|
|
101
|
+
response = self._make_request("POST", "/masking-policies", json=payload)
|
|
102
|
+
return response.json()
|
|
103
|
+
|
|
104
|
+
def update_masking_policy(self, policy_id: str, **fields: Any) -> Dict[str, Any]:
|
|
105
|
+
"""Update a masking policy.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
policy_id: The policy's ID.
|
|
109
|
+
**fields: Fields to change. Use API spellings for multi-word keys,
|
|
110
|
+
e.g. ``keyMapName``.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
The updated masking policy dictionary.
|
|
114
|
+
"""
|
|
115
|
+
response = self._make_request(
|
|
116
|
+
"PATCH", f"/masking-policies/{policy_id}", json=fields
|
|
117
|
+
)
|
|
118
|
+
return response.json()
|
|
119
|
+
|
|
120
|
+
def delete_masking_policy(self, policy_id: str) -> Dict[str, Any]:
|
|
121
|
+
"""Delete a masking policy.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
policy_id: The policy's ID.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
``{"message": "Masking policy deleted"}``.
|
|
128
|
+
"""
|
|
129
|
+
response = self._make_request("DELETE", f"/masking-policies/{policy_id}")
|
|
130
|
+
return response.json()
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Client for Plan operations.
|
|
2
|
+
|
|
3
|
+
A Plan is a reviewable description of work the agent (or a person) intends to
|
|
4
|
+
carry out: which entities, which targets, how many rows, and against which
|
|
5
|
+
environment. Plans are the governed path - they are authored, reviewed and only
|
|
6
|
+
then run - which is why this client covers authoring and inspection rather than
|
|
7
|
+
execution.
|
|
8
|
+
|
|
9
|
+
The plan RUN endpoints (`/plans/{id}/run`, `/runs`, `/signoffs`) are not wrapped
|
|
10
|
+
yet: they carry artifacts, logs and multi-step state, and their response shapes
|
|
11
|
+
are not described in the API's OpenAPI document at the time of writing. Use
|
|
12
|
+
``client.plans._make_request`` or add them here once the API types them.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from typing import Any, Dict, List
|
|
16
|
+
|
|
17
|
+
from .base import BaseClient
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PlansClient(BaseClient):
|
|
21
|
+
"""Client for plan operations."""
|
|
22
|
+
|
|
23
|
+
def get_plans(self) -> List[Dict[str, Any]]:
|
|
24
|
+
"""List the plans in the caller's active project.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
A list of plan dictionaries, newest first.
|
|
28
|
+
"""
|
|
29
|
+
response = self._make_request("GET", "/plans")
|
|
30
|
+
return response.json()
|
|
31
|
+
|
|
32
|
+
def get_plan(self, plan_id: str) -> Dict[str, Any]:
|
|
33
|
+
"""Get a single plan by ID, including its full ``spec`` and ``history``.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
plan_id: The plan's ID.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
The plan dictionary.
|
|
40
|
+
"""
|
|
41
|
+
response = self._make_request("GET", f"/plans/{plan_id}")
|
|
42
|
+
return response.json()
|
|
43
|
+
|
|
44
|
+
def update_plan(self, plan_id: str, **fields: Any) -> Dict[str, Any]:
|
|
45
|
+
"""Update a plan.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
plan_id: The plan's ID.
|
|
49
|
+
**fields: Fields to change, e.g. ``title``, ``summary``, ``status``.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
The updated plan dictionary.
|
|
53
|
+
"""
|
|
54
|
+
response = self._make_request("PATCH", f"/plans/{plan_id}", json=fields)
|
|
55
|
+
return response.json()
|
|
56
|
+
|
|
57
|
+
def delete_plan(self, plan_id: str) -> Dict[str, Any]:
|
|
58
|
+
"""Delete a plan.
|
|
59
|
+
|
|
60
|
+
Note:
|
|
61
|
+
This endpoint answers ``{"success": true}``, unlike the other
|
|
62
|
+
resources, which answer ``{"message": ...}``. Returned as-is rather
|
|
63
|
+
than normalised, so the SDK reports what the API actually sent.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
plan_id: The plan's ID.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
``{"success": True}``.
|
|
70
|
+
"""
|
|
71
|
+
response = self._make_request("DELETE", f"/plans/{plan_id}")
|
|
72
|
+
return response.json()
|
|
@@ -14,6 +14,9 @@ src/datamaker/routes/custom_types.py
|
|
|
14
14
|
src/datamaker/routes/export_and_validation.py
|
|
15
15
|
src/datamaker/routes/folders_and_utils.py
|
|
16
16
|
src/datamaker/routes/generation.py
|
|
17
|
+
src/datamaker/routes/keymaps.py
|
|
18
|
+
src/datamaker/routes/masking_policies.py
|
|
19
|
+
src/datamaker/routes/plans.py
|
|
17
20
|
src/datamaker/routes/projects.py
|
|
18
21
|
src/datamaker/routes/scenario_files.py
|
|
19
22
|
src/datamaker/routes/sets.py
|
|
@@ -12,6 +12,7 @@ from src.datamaker.routes.projects import ProjectsClient
|
|
|
12
12
|
from src.datamaker.routes.users import UsersClient
|
|
13
13
|
from src.datamaker.routes.teams import TeamsClient, TeamMembersClient
|
|
14
14
|
from src.datamaker.routes.sets import SetsClient
|
|
15
|
+
from src.datamaker.routes.keymaps import KeyMapsClient
|
|
15
16
|
from src.datamaker.routes.custom_types import EndpointsClient
|
|
16
17
|
from src.datamaker.error import DataMakerError
|
|
17
18
|
|
|
@@ -690,3 +691,134 @@ class TestEndpointsClient:
|
|
|
690
691
|
)
|
|
691
692
|
assert result["authType"] == "Basic"
|
|
692
693
|
assert result["basic"]["username"] == "user"
|
|
694
|
+
|
|
695
|
+
class TestKeyMapsClient:
|
|
696
|
+
"""Test cases for the KeyMapsClient class."""
|
|
697
|
+
|
|
698
|
+
@patch("src.datamaker.routes.base.BaseClient._make_request")
|
|
699
|
+
def test_get_keymaps_with_project_id(self, mock_make_request, api_key):
|
|
700
|
+
"""Test listing key maps scoped to a project id."""
|
|
701
|
+
mock_response = Mock()
|
|
702
|
+
mock_response.json.return_value = [
|
|
703
|
+
{"mapName": "m1", "object": "Material", "entryCount": 2}
|
|
704
|
+
]
|
|
705
|
+
mock_make_request.return_value = mock_response
|
|
706
|
+
|
|
707
|
+
client = KeyMapsClient(api_key=api_key)
|
|
708
|
+
result = client.get_keymaps(project_id="proj-1")
|
|
709
|
+
|
|
710
|
+
mock_make_request.assert_called_once_with("GET", "/keymaps?projectId=proj-1")
|
|
711
|
+
assert result == [{"mapName": "m1", "object": "Material", "entryCount": 2}]
|
|
712
|
+
|
|
713
|
+
@patch("src.datamaker.routes.base.BaseClient._make_request")
|
|
714
|
+
def test_keymap_put(self, mock_make_request, api_key):
|
|
715
|
+
"""Test batch-upserting key map entries."""
|
|
716
|
+
mock_response = Mock()
|
|
717
|
+
mock_response.json.return_value = {
|
|
718
|
+
"mapName": "sap-material-migration",
|
|
719
|
+
"object": "Material",
|
|
720
|
+
"upserted": 2,
|
|
721
|
+
}
|
|
722
|
+
mock_make_request.return_value = mock_response
|
|
723
|
+
|
|
724
|
+
client = KeyMapsClient(api_key=api_key)
|
|
725
|
+
result = client.keymap_put(
|
|
726
|
+
"sap-material-migration",
|
|
727
|
+
"Material",
|
|
728
|
+
{"MAT-001": "700001", "MAT-002": "700002"},
|
|
729
|
+
run_id="run-1",
|
|
730
|
+
project_id="proj-1",
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
mock_make_request.assert_called_once_with(
|
|
734
|
+
"POST",
|
|
735
|
+
"/keymaps/entries",
|
|
736
|
+
json={
|
|
737
|
+
"mapName": "sap-material-migration",
|
|
738
|
+
"object": "Material",
|
|
739
|
+
"entries": [
|
|
740
|
+
{"oldKey": "MAT-001", "newKey": "700001"},
|
|
741
|
+
{"oldKey": "MAT-002", "newKey": "700002"},
|
|
742
|
+
],
|
|
743
|
+
"runId": "run-1",
|
|
744
|
+
"projectId": "proj-1",
|
|
745
|
+
},
|
|
746
|
+
)
|
|
747
|
+
assert result["upserted"] == 2
|
|
748
|
+
|
|
749
|
+
@patch("src.datamaker.routes.base.BaseClient._make_request")
|
|
750
|
+
def test_keymap_put_requires_entries(self, mock_make_request, api_key):
|
|
751
|
+
"""Test that an empty entries dict raises."""
|
|
752
|
+
client = KeyMapsClient(api_key=api_key)
|
|
753
|
+
with pytest.raises(DataMakerError):
|
|
754
|
+
client.keymap_put("m1", "Material", {})
|
|
755
|
+
mock_make_request.assert_not_called()
|
|
756
|
+
|
|
757
|
+
@patch("src.datamaker.routes.base.BaseClient._make_request")
|
|
758
|
+
def test_keymap_lookup(self, mock_make_request, api_key):
|
|
759
|
+
"""Test batch key lookup returns mappings and missing keys."""
|
|
760
|
+
mock_response = Mock()
|
|
761
|
+
mock_response.json.return_value = {
|
|
762
|
+
"mapName": "m1",
|
|
763
|
+
"object": "Material",
|
|
764
|
+
"mappings": {"MAT-001": "700001"},
|
|
765
|
+
"missing": ["MAT-999"],
|
|
766
|
+
}
|
|
767
|
+
mock_make_request.return_value = mock_response
|
|
768
|
+
|
|
769
|
+
client = KeyMapsClient(api_key=api_key)
|
|
770
|
+
result = client.keymap_lookup(
|
|
771
|
+
"m1", "Material", ["MAT-001", "MAT-999"], project_id="proj-1"
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
mock_make_request.assert_called_once_with(
|
|
775
|
+
"POST",
|
|
776
|
+
"/keymaps/lookup",
|
|
777
|
+
json={
|
|
778
|
+
"mapName": "m1",
|
|
779
|
+
"object": "Material",
|
|
780
|
+
"oldKeys": ["MAT-001", "MAT-999"],
|
|
781
|
+
"projectId": "proj-1",
|
|
782
|
+
},
|
|
783
|
+
)
|
|
784
|
+
assert result["mappings"] == {"MAT-001": "700001"}
|
|
785
|
+
assert result["missing"] == ["MAT-999"]
|
|
786
|
+
|
|
787
|
+
@patch("src.datamaker.routes.base.BaseClient._make_request")
|
|
788
|
+
def test_get_keymap_entries(self, mock_make_request, api_key):
|
|
789
|
+
"""Test paginated entry inspection with an object filter."""
|
|
790
|
+
mock_response = Mock()
|
|
791
|
+
mock_response.json.return_value = {
|
|
792
|
+
"mapName": "m1",
|
|
793
|
+
"page": 1,
|
|
794
|
+
"pageSize": 50,
|
|
795
|
+
"total": 1,
|
|
796
|
+
"entries": [{"object": "Material", "oldKey": "a", "newKey": "b"}],
|
|
797
|
+
}
|
|
798
|
+
mock_make_request.return_value = mock_response
|
|
799
|
+
|
|
800
|
+
client = KeyMapsClient(api_key=api_key)
|
|
801
|
+
result = client.get_keymap_entries(
|
|
802
|
+
"m1", object="Material", page=1, page_size=50, project_id="proj-1"
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
mock_make_request.assert_called_once_with(
|
|
806
|
+
"GET",
|
|
807
|
+
"/keymaps/m1/entries?page=1&pageSize=50&object=Material&projectId=proj-1",
|
|
808
|
+
)
|
|
809
|
+
assert result["total"] == 1
|
|
810
|
+
|
|
811
|
+
@patch("src.datamaker.routes.base.BaseClient._make_request")
|
|
812
|
+
def test_delete_keymap(self, mock_make_request, api_key):
|
|
813
|
+
"""Test dropping a key map scoped to one object type."""
|
|
814
|
+
mock_response = Mock()
|
|
815
|
+
mock_response.json.return_value = {"message": "Key map deleted", "deleted": 5}
|
|
816
|
+
mock_make_request.return_value = mock_response
|
|
817
|
+
|
|
818
|
+
client = KeyMapsClient(api_key=api_key)
|
|
819
|
+
result = client.delete_keymap("m1", object="Material", project_id="proj-1")
|
|
820
|
+
|
|
821
|
+
mock_make_request.assert_called_once_with(
|
|
822
|
+
"DELETE", "/keymaps/m1?object=Material&projectId=proj-1"
|
|
823
|
+
)
|
|
824
|
+
assert result["deleted"] == 5
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|