datamaker-py 0.10.0__tar.gz → 0.11.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.
Files changed (42) hide show
  1. {datamaker_py-0.10.0 → datamaker_py-0.11.0}/PKG-INFO +30 -1
  2. {datamaker_py-0.10.0 → datamaker_py-0.11.0}/README.md +29 -0
  3. {datamaker_py-0.10.0 → datamaker_py-0.11.0}/pyproject.toml +1 -1
  4. datamaker_py-0.11.0/src/datamaker/__init__.py +12 -0
  5. datamaker_py-0.11.0/src/datamaker/error.py +94 -0
  6. {datamaker_py-0.10.0 → datamaker_py-0.11.0}/src/datamaker/main.py +105 -1
  7. {datamaker_py-0.10.0 → datamaker_py-0.11.0}/src/datamaker/routes/__init__.py +2 -0
  8. {datamaker_py-0.10.0 → datamaker_py-0.11.0}/src/datamaker/routes/base.py +10 -2
  9. datamaker_py-0.11.0/src/datamaker/routes/tosca.py +270 -0
  10. {datamaker_py-0.10.0 → datamaker_py-0.11.0}/src/datamaker_py.egg-info/PKG-INFO +30 -1
  11. {datamaker_py-0.10.0 → datamaker_py-0.11.0}/src/datamaker_py.egg-info/SOURCES.txt +3 -1
  12. datamaker_py-0.11.0/tests/test_tosca.py +425 -0
  13. datamaker_py-0.10.0/src/datamaker/__init__.py +0 -2
  14. datamaker_py-0.10.0/src/datamaker/error.py +0 -2
  15. {datamaker_py-0.10.0 → datamaker_py-0.11.0}/LICENSE +0 -0
  16. {datamaker_py-0.10.0 → datamaker_py-0.11.0}/setup.cfg +0 -0
  17. {datamaker_py-0.10.0 → datamaker_py-0.11.0}/src/datamaker/core.py +0 -0
  18. {datamaker_py-0.10.0 → datamaker_py-0.11.0}/src/datamaker/generated/__init__.py +0 -0
  19. {datamaker_py-0.10.0 → datamaker_py-0.11.0}/src/datamaker/generated/schema.py +0 -0
  20. {datamaker_py-0.10.0 → datamaker_py-0.11.0}/src/datamaker/routes/api_keys.py +0 -0
  21. {datamaker_py-0.10.0 → datamaker_py-0.11.0}/src/datamaker/routes/connections.py +0 -0
  22. {datamaker_py-0.10.0 → datamaker_py-0.11.0}/src/datamaker/routes/custom_types.py +0 -0
  23. {datamaker_py-0.10.0 → datamaker_py-0.11.0}/src/datamaker/routes/export_and_validation.py +0 -0
  24. {datamaker_py-0.10.0 → datamaker_py-0.11.0}/src/datamaker/routes/folders_and_utils.py +0 -0
  25. {datamaker_py-0.10.0 → datamaker_py-0.11.0}/src/datamaker/routes/generation.py +0 -0
  26. {datamaker_py-0.10.0 → datamaker_py-0.11.0}/src/datamaker/routes/keymaps.py +0 -0
  27. {datamaker_py-0.10.0 → datamaker_py-0.11.0}/src/datamaker/routes/masking_policies.py +0 -0
  28. {datamaker_py-0.10.0 → datamaker_py-0.11.0}/src/datamaker/routes/plans.py +0 -0
  29. {datamaker_py-0.10.0 → datamaker_py-0.11.0}/src/datamaker/routes/projects.py +0 -0
  30. {datamaker_py-0.10.0 → datamaker_py-0.11.0}/src/datamaker/routes/scenario_files.py +0 -0
  31. {datamaker_py-0.10.0 → datamaker_py-0.11.0}/src/datamaker/routes/sets.py +0 -0
  32. {datamaker_py-0.10.0 → datamaker_py-0.11.0}/src/datamaker/routes/teams.py +0 -0
  33. {datamaker_py-0.10.0 → datamaker_py-0.11.0}/src/datamaker/routes/templates.py +0 -0
  34. {datamaker_py-0.10.0 → datamaker_py-0.11.0}/src/datamaker/routes/users.py +0 -0
  35. {datamaker_py-0.10.0 → datamaker_py-0.11.0}/src/datamaker/template.py +0 -0
  36. {datamaker_py-0.10.0 → datamaker_py-0.11.0}/src/datamaker/types.py +0 -0
  37. {datamaker_py-0.10.0 → datamaker_py-0.11.0}/src/datamaker_py.egg-info/dependency_links.txt +0 -0
  38. {datamaker_py-0.10.0 → datamaker_py-0.11.0}/src/datamaker_py.egg-info/requires.txt +0 -0
  39. {datamaker_py-0.10.0 → datamaker_py-0.11.0}/src/datamaker_py.egg-info/top_level.txt +0 -0
  40. {datamaker_py-0.10.0 → datamaker_py-0.11.0}/tests/test_integration.py +0 -0
  41. {datamaker_py-0.10.0 → datamaker_py-0.11.0}/tests/test_main.py +0 -0
  42. {datamaker_py-0.10.0 → datamaker_py-0.11.0}/tests/test_routes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datamaker-py
3
- Version: 0.10.0
3
+ Version: 0.11.0
4
4
  Summary: The official Python library for the Automators DataMaker API.
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -62,6 +62,35 @@ if __name__ == "__main__":
62
62
 
63
63
  ```
64
64
 
65
+ ## Push a set into Tosca Cloud
66
+
67
+ `push_tosca_dataset` creates or replaces a native Tosca Cloud data set from a saved
68
+ set, an input set version, a dataset or inline rows. A Tosca test step reads it with
69
+ `{TDM[<data set>][<subset>][<column>]}`.
70
+
71
+ ```python
72
+ from datamaker import DataMaker, ToscaDatasetSchemaChangedError
73
+
74
+ dm = DataMaker()
75
+
76
+ try:
77
+ result = dm.push_tosca_dataset(
78
+ integration_id,
79
+ name="Orders",
80
+ input_set_id="is_123", # or set_id=..., dataset_id=..., rows=[...]
81
+ on_existing="overwrite", # "fail" (default) | "overwrite" | "new"
82
+ )
83
+ print(result.dataset_id, result.rows_count, result.outcome, result.example)
84
+ except ToscaDatasetSchemaChangedError as e:
85
+ # The rows dropped a column the data set has. Pass on_schema_change="recreate"
86
+ # to delete and recreate it instead.
87
+ print("removed:", e.removed_columns, "added:", e.added_columns)
88
+ ```
89
+
90
+ An input set gets one Tosca subset per test case by default; `subsets_by` picks another
91
+ column or turns it off with `False`. `get_tosca_datasets(integration_id)` lists what a
92
+ space already holds.
93
+
65
94
  ## Autocomplete Types
66
95
 
67
96
  This SDK automatically generates TypeScript autocomplete definitions for use in the [datamaker application](https://github.com/automators/datamaker).
@@ -51,6 +51,35 @@ if __name__ == "__main__":
51
51
 
52
52
  ```
53
53
 
54
+ ## Push a set into Tosca Cloud
55
+
56
+ `push_tosca_dataset` creates or replaces a native Tosca Cloud data set from a saved
57
+ set, an input set version, a dataset or inline rows. A Tosca test step reads it with
58
+ `{TDM[<data set>][<subset>][<column>]}`.
59
+
60
+ ```python
61
+ from datamaker import DataMaker, ToscaDatasetSchemaChangedError
62
+
63
+ dm = DataMaker()
64
+
65
+ try:
66
+ result = dm.push_tosca_dataset(
67
+ integration_id,
68
+ name="Orders",
69
+ input_set_id="is_123", # or set_id=..., dataset_id=..., rows=[...]
70
+ on_existing="overwrite", # "fail" (default) | "overwrite" | "new"
71
+ )
72
+ print(result.dataset_id, result.rows_count, result.outcome, result.example)
73
+ except ToscaDatasetSchemaChangedError as e:
74
+ # The rows dropped a column the data set has. Pass on_schema_change="recreate"
75
+ # to delete and recreate it instead.
76
+ print("removed:", e.removed_columns, "added:", e.added_columns)
77
+ ```
78
+
79
+ An input set gets one Tosca subset per test case by default; `subsets_by` picks another
80
+ column or turns it off with `False`. `get_tosca_datasets(integration_id)` lists what a
81
+ space already holds.
82
+
54
83
  ## Autocomplete Types
55
84
 
56
85
  This SDK automatically generates TypeScript autocomplete definitions for use in the [datamaker application](https://github.com/automators/datamaker).
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "datamaker-py"
7
- version = "0.10.0"
7
+ version = "0.11.0"
8
8
  description = "The official Python library for the Automators DataMaker API."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -0,0 +1,12 @@
1
+ from .main import DataMaker # noqa
2
+ from .template import Template # noqa
3
+ from .error import ( # noqa
4
+ DataMakerError,
5
+ DataMakerAPIError,
6
+ ReadOnlyConnectionError,
7
+ ToscaDatasetExistsError,
8
+ ToscaDatasetLockedError,
9
+ ToscaDatasetSchemaChangedError,
10
+ ToscaDatasetTooLargeError,
11
+ )
12
+ from .routes.tosca import ToscaDatasetPushResult # noqa
@@ -0,0 +1,94 @@
1
+ from typing import Any, List, Optional
2
+
3
+
4
+ class DataMakerError(Exception):
5
+ pass
6
+
7
+
8
+ class DataMakerAPIError(DataMakerError):
9
+ """The API answered with a non-2xx status.
10
+
11
+ Subclasses ``DataMakerError`` and keeps its message, so existing
12
+ ``except DataMakerError`` handlers are unaffected. Adds the status code and
13
+ the decoded JSON body (``None`` when the body was not JSON).
14
+ """
15
+
16
+ def __init__(self, message: str, status_code: int, body: Any = None):
17
+ super().__init__(message)
18
+ self.status_code = status_code
19
+ self.body = body
20
+
21
+ def _detail(self, key: str) -> Any:
22
+ return self.body.get(key) if isinstance(self.body, dict) else None
23
+
24
+
25
+ class ReadOnlyConnectionError(DataMakerAPIError):
26
+ """403: the connection is read-only, so DataMaker refused the write."""
27
+
28
+
29
+ class ToscaDatasetExistsError(DataMakerAPIError):
30
+ """409: a Tosca data set of that name exists and ``on_existing`` is "fail"."""
31
+
32
+ @property
33
+ def dataset_id(self) -> Optional[str]:
34
+ """The id of the data set that is already there."""
35
+ return self._detail("datasetId")
36
+
37
+
38
+ class ToscaDatasetLockedError(DataMakerAPIError):
39
+ """409: the data set is locked in Tosca Cloud, so it cannot be overwritten.
40
+
41
+ A test run or a "Test data - Expert" step holds it. Retry once it is
42
+ unlocked.
43
+ """
44
+
45
+ @property
46
+ def dataset_id(self) -> Optional[str]:
47
+ return self._detail("datasetId")
48
+
49
+
50
+ class ToscaDatasetSchemaChangedError(DataMakerAPIError):
51
+ """409: an overwrite would drop columns the Tosca data set has.
52
+
53
+ Tosca only overwrites in place when the columns match. Added columns are
54
+ handled by the API; a removed column is refused unless the push asks for
55
+ ``on_schema_change="recreate"``.
56
+ """
57
+
58
+ @property
59
+ def dataset_id(self) -> Optional[str]:
60
+ return self._detail("datasetId")
61
+
62
+ @property
63
+ def added_columns(self) -> List[str]:
64
+ return self._detail("added") or []
65
+
66
+ @property
67
+ def removed_columns(self) -> List[str]:
68
+ return self._detail("removed") or []
69
+
70
+
71
+ class ToscaDatasetTooLargeError(DataMakerAPIError):
72
+ """413: more rows or bytes than one push may write.
73
+
74
+ The row pair (``max_rows``, ``rows``) or the byte pair (``max_bytes``,
75
+ ``bytes``) is set, depending on which cap was hit. A set, input set or
76
+ dataset refused before its rows are loaded carries neither; the message
77
+ names the cap.
78
+ """
79
+
80
+ @property
81
+ def max_rows(self) -> Optional[int]:
82
+ return self._detail("maxRows")
83
+
84
+ @property
85
+ def rows(self) -> Optional[int]:
86
+ return self._detail("rows")
87
+
88
+ @property
89
+ def max_bytes(self) -> Optional[int]:
90
+ return self._detail("maxBytes")
91
+
92
+ @property
93
+ def bytes(self) -> Optional[int]:
94
+ return self._detail("bytes")
@@ -1,6 +1,6 @@
1
1
  import os
2
2
  from dotenv import load_dotenv
3
- from typing import Any, Optional, Dict, List
3
+ from typing import Any, Optional, Dict, List, Union
4
4
  from .types import WorkspaceUploadResult
5
5
  from .routes.base import BaseClient
6
6
  from .routes.generation import GenerationClient
@@ -26,6 +26,7 @@ from .routes.sets import SetsClient
26
26
  from .routes.plans import PlansClient
27
27
  from .routes.masking_policies import MaskingPoliciesClient
28
28
  from .routes.keymaps import KeyMapsClient
29
+ from .routes.tosca import ToscaClient, ToscaDatasetPushResult
29
30
 
30
31
  load_dotenv()
31
32
 
@@ -79,6 +80,7 @@ class DataMaker:
79
80
  self._masking_policies = MaskingPoliciesClient(
80
81
  api_key, default_headers, base_url, verify
81
82
  )
83
+ self._tosca = ToscaClient(api_key, default_headers, base_url, verify)
82
84
 
83
85
  # Maintain backward compatibility
84
86
  self.api_key = self._generation.api_key
@@ -939,6 +941,103 @@ class DataMaker:
939
941
  project_id=project_id,
940
942
  )
941
943
 
944
+ # =================== TOSCA METHODS ===================
945
+ def get_tosca_datasets(self, integration_id: str, space: Optional[str] = None):
946
+ """List the test data sets in a Tosca Cloud space.
947
+
948
+ Args:
949
+ integration_id: The Tosca Cloud connection to read through.
950
+ space: The Tosca Cloud space (id or name). Defaults to "default".
951
+
952
+ Returns:
953
+ A dictionary with space and items, each item carrying id, name,
954
+ subsetCount and isLocked.
955
+ """
956
+ return self._tosca.get_tosca_datasets(integration_id, space)
957
+
958
+ def push_tosca_dataset(
959
+ self,
960
+ integration_id: str,
961
+ name: str,
962
+ *,
963
+ set_id: Optional[str] = None,
964
+ input_set_id: Optional[str] = None,
965
+ version: Optional[Union[int, str]] = None,
966
+ dataset_id: Optional[str] = None,
967
+ rows: Optional[List[Dict[str, Any]]] = None,
968
+ allow_unlocked: bool = False,
969
+ on_existing: str = "fail",
970
+ on_schema_change: str = "refuse",
971
+ subsets_by: Optional[Union[str, bool]] = None,
972
+ space: str = "default",
973
+ ) -> ToscaDatasetPushResult:
974
+ """Create or replace a Tosca Cloud data set from DataMaker rows.
975
+
976
+ A Tosca test step reads the data set with
977
+ {TDM[<data set>][<subset>][<column>]}. Give exactly one source: set_id,
978
+ input_set_id (with an optional version), dataset_id or rows.
979
+
980
+ Args:
981
+ integration_id: The Tosca Cloud connection to push through.
982
+ name: The Tosca data set name. Unique per space.
983
+ set_id: Push the rows of this DataMaker set. It must be locked
984
+ unless allow_unlocked is True.
985
+ input_set_id: Push a frozen version of this input set.
986
+ version: The input set version number, or "latest" (the default).
987
+ dataset_id: Push the rows of this DataMaker dataset.
988
+ rows: Inline rows, each a dictionary of column name to value.
989
+ allow_unlocked: Push a set that is not locked.
990
+ on_existing: "fail" (default), "overwrite" (replace the rows,
991
+ keeping the id and subsets) or "new" (a copy beside it).
992
+ on_schema_change: On overwrite, when the rows drop a column:
993
+ "refuse" (default) or "recreate" (the id and subsets are lost).
994
+ subsets_by: Create one subset per distinct value of this column.
995
+ An input set is split by "TestCase" by default; False turns
996
+ it off.
997
+ space: The Tosca Cloud space (id or name). Defaults to "default".
998
+
999
+ Returns:
1000
+ A result with dataset_id, name, space, rows_count, outcome
1001
+ ("created", "overwritten", "recreated" or "copied"), columns,
1002
+ added_columns, subsets and example.
1003
+
1004
+ Raises:
1005
+ ReadOnlyConnectionError: The connection is read-only (403).
1006
+ ToscaDatasetExistsError: The name is taken and on_existing is
1007
+ "fail" (409). Carries dataset_id.
1008
+ ToscaDatasetLockedError: An overwrite of a data set that is
1009
+ locked in Tosca Cloud (409). Carries dataset_id.
1010
+ ToscaDatasetSchemaChangedError: An overwrite would drop columns
1011
+ (409). Carries added_columns and removed_columns.
1012
+ ToscaDatasetTooLargeError: Too many rows or bytes for one push
1013
+ (413). Carries max_rows or max_bytes when the API sends them.
1014
+
1015
+ Example:
1016
+ >>> dm = DataMaker()
1017
+ >>> result = dm.push_tosca_dataset(
1018
+ ... integration_id,
1019
+ ... name="Orders",
1020
+ ... input_set_id="is_123",
1021
+ ... on_existing="overwrite",
1022
+ ... )
1023
+ >>> result.example
1024
+ '{TDM[Orders][Customer]}'
1025
+ """
1026
+ return self._tosca.push_tosca_dataset(
1027
+ integration_id,
1028
+ name,
1029
+ set_id=set_id,
1030
+ input_set_id=input_set_id,
1031
+ version=version,
1032
+ dataset_id=dataset_id,
1033
+ rows=rows,
1034
+ allow_unlocked=allow_unlocked,
1035
+ on_existing=on_existing,
1036
+ on_schema_change=on_schema_change,
1037
+ subsets_by=subsets_by,
1038
+ space=space,
1039
+ )
1040
+
942
1041
  # =================== PROPERTY ACCESS TO CLIENTS ===================
943
1042
  # For advanced users who want direct access to specific clients
944
1043
 
@@ -1174,3 +1273,8 @@ class DataMaker:
1174
1273
  def masking_policies(self):
1175
1274
  """Access to masking policies client."""
1176
1275
  return self._masking_policies
1276
+
1277
+ @property
1278
+ def tosca(self):
1279
+ """Access to Tosca client."""
1280
+ return self._tosca
@@ -14,6 +14,7 @@ from .export_and_validation import ExportClient, ValidationClient
14
14
  from .scenario_files import ScenarioFilesClient
15
15
  from .sets import SetsClient
16
16
  from .keymaps import KeyMapsClient
17
+ from .tosca import ToscaClient
17
18
 
18
19
  __all__ = [
19
20
  "BaseClient",
@@ -36,4 +37,5 @@ __all__ = [
36
37
  "ScenarioFilesClient",
37
38
  "SetsClient",
38
39
  "KeyMapsClient",
40
+ "ToscaClient",
39
41
  ]
@@ -1,7 +1,7 @@
1
1
  import os
2
2
  import requests
3
3
  from typing import Optional, Dict
4
- from ..error import DataMakerError
4
+ from ..error import DataMakerAPIError
5
5
 
6
6
 
7
7
  class BaseClient:
@@ -35,6 +35,14 @@ class BaseClient:
35
35
  response = requests.request(method, url, headers=self.headers, **kwargs)
36
36
 
37
37
  if response.status_code not in [200, 201]:
38
- raise DataMakerError(f"API request failed: {response.text}")
38
+ try:
39
+ body = response.json()
40
+ except ValueError:
41
+ body = None
42
+ raise DataMakerAPIError(
43
+ f"API request failed: {response.text}",
44
+ status_code=response.status_code,
45
+ body=body,
46
+ )
39
47
 
40
48
  return response
@@ -0,0 +1,270 @@
1
+ """Client for Tosca Cloud test data sets.
2
+
3
+ A Tosca Cloud data set (Prepare > Test data) is what a test step reads with
4
+ ``{TDM[<data set>][<subset>][<column>]}``. The push goes through DataMaker:
5
+ the API reads the rows with the caller's own access, uploads them to Tosca
6
+ with the connection's credentials and writes the audit event, so a scenario
7
+ never holds Tosca credentials or calls Tosca itself.
8
+ """
9
+
10
+ from dataclasses import dataclass, field
11
+ from typing import Any, Dict, List, Optional, Union
12
+ from urllib.parse import quote
13
+
14
+ from .base import BaseClient
15
+ from ..error import (
16
+ DataMakerAPIError,
17
+ DataMakerError,
18
+ ReadOnlyConnectionError,
19
+ ToscaDatasetExistsError,
20
+ ToscaDatasetLockedError,
21
+ ToscaDatasetSchemaChangedError,
22
+ ToscaDatasetTooLargeError,
23
+ )
24
+
25
+ ON_EXISTING = ("fail", "overwrite", "new")
26
+ ON_SCHEMA_CHANGE = ("refuse", "recreate")
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class ToscaDatasetPushResult:
31
+ """The data set as Tosca holds it after a push."""
32
+
33
+ dataset_id: str
34
+ name: str
35
+ space: str
36
+ rows_count: int
37
+ # "created", "overwritten", "recreated" or "copied"
38
+ outcome: str
39
+ # A ready-to-paste reference, e.g. "{TDM[Orders][Customer]}"
40
+ example: str
41
+ # [{"name", "type"}] as Tosca typed them: String, Double, Boolean, DateTime
42
+ columns: List[Dict[str, str]] = field(default_factory=list)
43
+ # Columns an overwrite added to the existing data set
44
+ added_columns: List[str] = field(default_factory=list)
45
+ # {"created": [names], "existing": [names], "skipped": count}
46
+ subsets: Dict[str, Any] = field(default_factory=dict)
47
+ # The API's response as sent, for fields this class does not name yet.
48
+ raw: Dict[str, Any] = field(default_factory=dict, repr=False, compare=False)
49
+
50
+
51
+ class ToscaClient(BaseClient):
52
+ """Client for Tosca Cloud test data operations."""
53
+
54
+ def get_tosca_datasets(
55
+ self, integration_id: str, space: Optional[str] = None
56
+ ) -> Dict[str, Any]:
57
+ """List the test data sets in a Tosca Cloud space.
58
+
59
+ Args:
60
+ integration_id: The Tosca Cloud connection to read through.
61
+ space: The Tosca Cloud space (id or name). Defaults to "default".
62
+
63
+ Returns:
64
+ A dictionary with ``space`` and ``items``, each item carrying
65
+ ``id``, ``name``, ``subsetCount`` and ``isLocked``.
66
+ """
67
+ if not integration_id:
68
+ raise DataMakerError("integration_id is required to list Tosca data sets.")
69
+
70
+ endpoint = f"/integrations/{integration_id}/tosca/datasets"
71
+ if space:
72
+ endpoint += f"?space={quote(space, safe='')}"
73
+
74
+ response = self._make_request("GET", endpoint)
75
+ return response.json()
76
+
77
+ def push_tosca_dataset(
78
+ self,
79
+ integration_id: str,
80
+ name: str,
81
+ *,
82
+ set_id: Optional[str] = None,
83
+ input_set_id: Optional[str] = None,
84
+ version: Optional[Union[int, str]] = None,
85
+ dataset_id: Optional[str] = None,
86
+ rows: Optional[List[Dict[str, Any]]] = None,
87
+ allow_unlocked: bool = False,
88
+ on_existing: str = "fail",
89
+ on_schema_change: str = "refuse",
90
+ subsets_by: Optional[Union[str, bool]] = None,
91
+ space: str = "default",
92
+ ) -> ToscaDatasetPushResult:
93
+ """Create or replace a Tosca Cloud data set from DataMaker rows.
94
+
95
+ Give exactly one source: ``set_id``, ``input_set_id`` (with an optional
96
+ ``version``), ``dataset_id`` or ``rows``.
97
+
98
+ Args:
99
+ integration_id: The Tosca Cloud connection to push through.
100
+ name: The Tosca data set name. Unique per space; test steps
101
+ reference the data set by this name.
102
+ set_id: Push the rows of this DataMaker set. The set must be
103
+ locked unless ``allow_unlocked`` is True.
104
+ input_set_id: Push a frozen version of this input set.
105
+ version: The input set version number, or "latest" (the default).
106
+ Only valid with ``input_set_id``.
107
+ dataset_id: Push the rows of this DataMaker dataset.
108
+ rows: Inline rows, each a dictionary of column name to value.
109
+ Column types are inferred.
110
+ allow_unlocked: Push a set that is not locked. Only valid with
111
+ ``set_id``.
112
+ on_existing: What to do when a data set called ``name`` exists:
113
+ "fail" (default), "overwrite" (replace its rows, keeping its
114
+ id and subsets) or "new" (Tosca creates a copy beside it).
115
+ on_schema_change: On overwrite, when the rows drop a column the
116
+ data set has: "refuse" (default) or "recreate" (delete and
117
+ recreate it; test steps keep working, its id and subsets are
118
+ lost). Added columns are always handled.
119
+ subsets_by: Create one subset per distinct value of this column,
120
+ so each Tosca test case reads only its rows. Left unset, an
121
+ input set is split by "TestCase" and other sources are not
122
+ split. False turns it off.
123
+ space: The Tosca Cloud space (id or name). Defaults to "default".
124
+
125
+ Returns:
126
+ A ``ToscaDatasetPushResult`` with ``dataset_id``, ``name``,
127
+ ``space``, ``rows_count``, ``outcome``, ``columns``,
128
+ ``added_columns``, ``subsets`` and ``example``.
129
+
130
+ Raises:
131
+ DataMakerError: If the arguments do not describe exactly one source.
132
+ ReadOnlyConnectionError: 403, the connection is read-only.
133
+ ToscaDatasetExistsError: 409, the name is taken and ``on_existing``
134
+ is "fail". Carries ``dataset_id``.
135
+ ToscaDatasetLockedError: 409, an overwrite of a data set that is
136
+ locked in Tosca Cloud. Carries ``dataset_id``.
137
+ ToscaDatasetSchemaChangedError: 409, an overwrite would drop
138
+ columns. Carries ``added_columns`` and ``removed_columns``.
139
+ ToscaDatasetTooLargeError: 413, more rows or bytes than one push
140
+ allows. Carries ``max_rows`` or ``max_bytes`` when the API
141
+ sends them.
142
+ DataMakerAPIError: Any other non-2xx answer, with ``status_code``
143
+ and ``body``.
144
+
145
+ Example:
146
+ >>> dm = DataMaker()
147
+ >>> result = dm.push_tosca_dataset(
148
+ ... integration_id,
149
+ ... name="Orders",
150
+ ... input_set_id="is_123",
151
+ ... on_existing="overwrite",
152
+ ... )
153
+ >>> result.example
154
+ '{TDM[Orders][Customer]}'
155
+ """
156
+ if not integration_id:
157
+ raise DataMakerError("integration_id is required to push a Tosca data set.")
158
+ if not name:
159
+ raise DataMakerError("name is required to push a Tosca data set.")
160
+ if on_existing not in ON_EXISTING:
161
+ raise DataMakerError(
162
+ f"on_existing must be one of {', '.join(ON_EXISTING)}; got {on_existing!r}."
163
+ )
164
+ if on_schema_change not in ON_SCHEMA_CHANGE:
165
+ raise DataMakerError(
166
+ f"on_schema_change must be one of {', '.join(ON_SCHEMA_CHANGE)}; "
167
+ f"got {on_schema_change!r}."
168
+ )
169
+ if subsets_by is True or subsets_by == "":
170
+ raise DataMakerError("subsets_by must be a column name or False.")
171
+
172
+ given = [
173
+ label
174
+ for label, value in (
175
+ ("set_id", set_id),
176
+ ("input_set_id", input_set_id),
177
+ ("dataset_id", dataset_id),
178
+ ("rows", rows),
179
+ )
180
+ if value is not None
181
+ ]
182
+ if len(given) != 1:
183
+ raise DataMakerError(
184
+ "Give exactly one source: set_id, input_set_id, dataset_id or rows"
185
+ + (f"; got {', '.join(given)}." if given else "; got none.")
186
+ )
187
+ if version is not None and input_set_id is None:
188
+ raise DataMakerError("version only applies to input_set_id.")
189
+ if allow_unlocked and set_id is None:
190
+ raise DataMakerError("allow_unlocked only applies to set_id.")
191
+
192
+ source: Dict[str, Any]
193
+ if set_id is not None:
194
+ source = {"kind": "set", "setId": set_id}
195
+ if allow_unlocked:
196
+ source["allowUnlocked"] = True
197
+ elif input_set_id is not None:
198
+ source = {
199
+ "kind": "inputSet",
200
+ "inputSetId": input_set_id,
201
+ "version": "latest" if version is None else version,
202
+ }
203
+ elif dataset_id is not None:
204
+ source = {"kind": "dataset", "datasetId": dataset_id}
205
+ else:
206
+ if not rows:
207
+ raise DataMakerError("rows must not be empty.")
208
+ source = {"kind": "rows", "rows": rows}
209
+
210
+ payload: Dict[str, Any] = {
211
+ "space": space,
212
+ "name": name,
213
+ "source": source,
214
+ "onExisting": on_existing,
215
+ "onSchemaChange": on_schema_change,
216
+ }
217
+ if subsets_by is not None:
218
+ payload["subsetsBy"] = subsets_by
219
+
220
+ try:
221
+ response = self._make_request(
222
+ "POST", f"/integrations/{integration_id}/tosca/datasets", json=payload
223
+ )
224
+ except DataMakerAPIError as error:
225
+ raise self._typed_error(error, on_existing) from error
226
+
227
+ data = response.json()
228
+ return ToscaDatasetPushResult(
229
+ dataset_id=data["datasetId"],
230
+ name=data["name"],
231
+ space=data["space"],
232
+ rows_count=data["rowsCount"],
233
+ outcome=data["outcome"],
234
+ example=data["example"],
235
+ columns=data.get("columns") or [],
236
+ added_columns=data.get("addedColumns") or [],
237
+ subsets=data.get("subsets") or {},
238
+ raw=data,
239
+ )
240
+
241
+ @staticmethod
242
+ def _typed_error(error: DataMakerAPIError, on_existing: str) -> DataMakerAPIError:
243
+ """Narrow a failed push to the error a scenario can act on."""
244
+ body = error.body if isinstance(error.body, dict) else {}
245
+ args = (str(error), error.status_code, error.body)
246
+
247
+ # 403 is also what a caller without read access to the source gets,
248
+ # so the status alone does not mean the connection is read-only.
249
+ if error.status_code == 403 and "read-only" in str(body.get("error", "")):
250
+ return ReadOnlyConnectionError(*args)
251
+ if error.status_code == 409:
252
+ by_code = {
253
+ "exists": ToscaDatasetExistsError,
254
+ "locked": ToscaDatasetLockedError,
255
+ "schemaChanged": ToscaDatasetSchemaChangedError,
256
+ }
257
+ if body.get("code") in by_code:
258
+ return by_code[body["code"]](*args)
259
+ # No code: an API from before the 409s were named. "Name taken"
260
+ # and "locked" then share a body shape, and which one it is
261
+ # follows from what the push asked for.
262
+ if "added" in body or "removed" in body:
263
+ return ToscaDatasetSchemaChangedError(*args)
264
+ if "datasetId" in body and on_existing == "fail":
265
+ return ToscaDatasetExistsError(*args)
266
+ if "datasetId" in body and on_existing == "overwrite":
267
+ return ToscaDatasetLockedError(*args)
268
+ if error.status_code == 413:
269
+ return ToscaDatasetTooLargeError(*args)
270
+ return error
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datamaker-py
3
- Version: 0.10.0
3
+ Version: 0.11.0
4
4
  Summary: The official Python library for the Automators DataMaker API.
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -62,6 +62,35 @@ if __name__ == "__main__":
62
62
 
63
63
  ```
64
64
 
65
+ ## Push a set into Tosca Cloud
66
+
67
+ `push_tosca_dataset` creates or replaces a native Tosca Cloud data set from a saved
68
+ set, an input set version, a dataset or inline rows. A Tosca test step reads it with
69
+ `{TDM[<data set>][<subset>][<column>]}`.
70
+
71
+ ```python
72
+ from datamaker import DataMaker, ToscaDatasetSchemaChangedError
73
+
74
+ dm = DataMaker()
75
+
76
+ try:
77
+ result = dm.push_tosca_dataset(
78
+ integration_id,
79
+ name="Orders",
80
+ input_set_id="is_123", # or set_id=..., dataset_id=..., rows=[...]
81
+ on_existing="overwrite", # "fail" (default) | "overwrite" | "new"
82
+ )
83
+ print(result.dataset_id, result.rows_count, result.outcome, result.example)
84
+ except ToscaDatasetSchemaChangedError as e:
85
+ # The rows dropped a column the data set has. Pass on_schema_change="recreate"
86
+ # to delete and recreate it instead.
87
+ print("removed:", e.removed_columns, "added:", e.added_columns)
88
+ ```
89
+
90
+ An input set gets one Tosca subset per test case by default; `subsets_by` picks another
91
+ column or turns it off with `False`. `get_tosca_datasets(integration_id)` lists what a
92
+ space already holds.
93
+
65
94
  ## Autocomplete Types
66
95
 
67
96
  This SDK automatically generates TypeScript autocomplete definitions for use in the [datamaker application](https://github.com/automators/datamaker).
@@ -25,6 +25,7 @@ src/datamaker/routes/scenario_files.py
25
25
  src/datamaker/routes/sets.py
26
26
  src/datamaker/routes/teams.py
27
27
  src/datamaker/routes/templates.py
28
+ src/datamaker/routes/tosca.py
28
29
  src/datamaker/routes/users.py
29
30
  src/datamaker_py.egg-info/PKG-INFO
30
31
  src/datamaker_py.egg-info/SOURCES.txt
@@ -33,4 +34,5 @@ src/datamaker_py.egg-info/requires.txt
33
34
  src/datamaker_py.egg-info/top_level.txt
34
35
  tests/test_integration.py
35
36
  tests/test_main.py
36
- tests/test_routes.py
37
+ tests/test_routes.py
38
+ tests/test_tosca.py
@@ -0,0 +1,425 @@
1
+ """Tests for pushing rows into Tosca Cloud data sets."""
2
+
3
+ import json
4
+
5
+ import pytest
6
+ from unittest.mock import Mock, patch
7
+
8
+ from src.datamaker.main import DataMaker
9
+ from src.datamaker.error import (
10
+ DataMakerAPIError,
11
+ DataMakerError,
12
+ ReadOnlyConnectionError,
13
+ ToscaDatasetExistsError,
14
+ ToscaDatasetLockedError,
15
+ ToscaDatasetSchemaChangedError,
16
+ ToscaDatasetTooLargeError,
17
+ )
18
+ from src.datamaker.routes.tosca import ToscaDatasetPushResult
19
+
20
+ PUSHED = {
21
+ "datasetId": "TDS-1",
22
+ "name": "Orders",
23
+ "space": "default",
24
+ "rowsCount": 2,
25
+ "columns": [{"name": "Customer", "type": "String"}],
26
+ "outcome": "created",
27
+ "addedColumns": [],
28
+ "subsets": {"created": ["TC-1"], "existing": [], "skipped": 0},
29
+ "example": "{TDM[Orders][Customer]}",
30
+ }
31
+
32
+
33
+ def ok(body=None, status_code=201):
34
+ response = Mock()
35
+ response.status_code = status_code
36
+ response.json.return_value = body or PUSHED
37
+ return response
38
+
39
+
40
+ def failed(status_code, body):
41
+ response = Mock()
42
+ response.status_code = status_code
43
+ response.text = json.dumps(body)
44
+ response.json.return_value = body
45
+ return response
46
+
47
+
48
+ def push(api_key, **kwargs):
49
+ return DataMaker(api_key=api_key).push_tosca_dataset(
50
+ "int-1", name="Orders", **kwargs
51
+ )
52
+
53
+
54
+ class TestPushToscaDataset:
55
+ """The request each source kind sends, and the result it returns."""
56
+
57
+ @patch("requests.request")
58
+ def test_set_source(self, mock_request, api_key):
59
+ mock_request.return_value = ok()
60
+
61
+ result = push(api_key, set_id="set-1")
62
+
63
+ method, url = mock_request.call_args.args
64
+ assert method == "POST"
65
+ assert url.endswith("/integrations/int-1/tosca/datasets")
66
+ assert mock_request.call_args.kwargs["json"] == {
67
+ "space": "default",
68
+ "name": "Orders",
69
+ "source": {"kind": "set", "setId": "set-1"},
70
+ "onExisting": "fail",
71
+ "onSchemaChange": "refuse",
72
+ }
73
+ assert isinstance(result, ToscaDatasetPushResult)
74
+ assert result.dataset_id == "TDS-1"
75
+ assert result.name == "Orders"
76
+ assert result.space == "default"
77
+ assert result.rows_count == 2
78
+ assert result.outcome == "created"
79
+ assert result.columns == [{"name": "Customer", "type": "String"}]
80
+ assert result.added_columns == []
81
+ assert result.subsets == {"created": ["TC-1"], "existing": [], "skipped": 0}
82
+ assert result.example == "{TDM[Orders][Customer]}"
83
+ assert result.raw == PUSHED
84
+
85
+ @patch("requests.request")
86
+ def test_unlocked_set(self, mock_request, api_key):
87
+ mock_request.return_value = ok()
88
+
89
+ push(api_key, set_id="set-1", allow_unlocked=True)
90
+
91
+ assert mock_request.call_args.kwargs["json"]["source"] == {
92
+ "kind": "set",
93
+ "setId": "set-1",
94
+ "allowUnlocked": True,
95
+ }
96
+
97
+ @patch("requests.request")
98
+ def test_input_set_source_defaults_to_latest(self, mock_request, api_key):
99
+ mock_request.return_value = ok()
100
+
101
+ push(api_key, input_set_id="is-1")
102
+
103
+ payload = mock_request.call_args.kwargs["json"]
104
+ assert payload["source"] == {
105
+ "kind": "inputSet",
106
+ "inputSetId": "is-1",
107
+ "version": "latest",
108
+ }
109
+ # Left out so the API applies its own default (TestCase for an input set).
110
+ assert "subsetsBy" not in payload
111
+
112
+ @patch("requests.request")
113
+ def test_input_set_source_with_options(self, mock_request, api_key):
114
+ mock_request.return_value = ok(
115
+ {**PUSHED, "outcome": "overwritten", "addedColumns": ["Region"]},
116
+ status_code=200,
117
+ )
118
+
119
+ result = push(
120
+ api_key,
121
+ input_set_id="is-1",
122
+ version=3,
123
+ on_existing="overwrite",
124
+ on_schema_change="recreate",
125
+ subsets_by="Region",
126
+ space="qa",
127
+ )
128
+
129
+ payload = mock_request.call_args.kwargs["json"]
130
+ assert payload["source"]["version"] == 3
131
+ assert payload["onExisting"] == "overwrite"
132
+ assert payload["onSchemaChange"] == "recreate"
133
+ assert payload["subsetsBy"] == "Region"
134
+ assert payload["space"] == "qa"
135
+ assert result.outcome == "overwritten"
136
+ assert result.added_columns == ["Region"]
137
+
138
+ @patch("requests.request")
139
+ def test_subsets_can_be_turned_off(self, mock_request, api_key):
140
+ mock_request.return_value = ok()
141
+
142
+ push(api_key, input_set_id="is-1", subsets_by=False)
143
+
144
+ assert mock_request.call_args.kwargs["json"]["subsetsBy"] is False
145
+
146
+ @patch("requests.request")
147
+ def test_dataset_source(self, mock_request, api_key):
148
+ mock_request.return_value = ok()
149
+
150
+ push(api_key, dataset_id="ds-1")
151
+
152
+ assert mock_request.call_args.kwargs["json"]["source"] == {
153
+ "kind": "dataset",
154
+ "datasetId": "ds-1",
155
+ }
156
+
157
+ @patch("requests.request")
158
+ def test_rows_source(self, mock_request, api_key):
159
+ mock_request.return_value = ok({**PUSHED, "outcome": "copied"})
160
+ rows = [{"Customer": "0042"}, {"Customer": "0043"}]
161
+
162
+ result = push(api_key, rows=rows, on_existing="new")
163
+
164
+ payload = mock_request.call_args.kwargs["json"]
165
+ assert payload["source"] == {"kind": "rows", "rows": rows}
166
+ assert payload["onExisting"] == "new"
167
+ assert result.outcome == "copied"
168
+
169
+
170
+ class TestPushToscaDatasetValidation:
171
+ """Bad arguments are refused before any request is made."""
172
+
173
+ @pytest.mark.parametrize(
174
+ "kwargs, match",
175
+ [
176
+ ({}, "exactly one source"),
177
+ ({"set_id": "s", "input_set_id": "i"}, "exactly one source"),
178
+ ({"set_id": "s", "rows": [{"a": 1}]}, "exactly one source"),
179
+ ({"input_set_id": "i", "dataset_id": "d"}, "exactly one source"),
180
+ ({"rows": []}, "rows must not be empty"),
181
+ ({"set_id": "s", "version": 2}, "version only applies"),
182
+ ({"dataset_id": "d", "allow_unlocked": True}, "allow_unlocked only applies"),
183
+ ({"set_id": "s", "on_existing": "replace"}, "on_existing must be one of"),
184
+ ({"set_id": "s", "on_schema_change": "drop"}, "on_schema_change must be"),
185
+ ({"set_id": "s", "subsets_by": True}, "subsets_by must be"),
186
+ ],
187
+ )
188
+ @patch("requests.request")
189
+ def test_rejected_locally(self, mock_request, api_key, kwargs, match):
190
+ with pytest.raises(DataMakerError, match=match):
191
+ push(api_key, **kwargs)
192
+ mock_request.assert_not_called()
193
+
194
+ @patch("requests.request")
195
+ def test_requires_integration_id_and_name(self, mock_request, api_key):
196
+ dm = DataMaker(api_key=api_key)
197
+ with pytest.raises(DataMakerError, match="integration_id is required"):
198
+ dm.push_tosca_dataset("", name="Orders", set_id="s")
199
+ with pytest.raises(DataMakerError, match="name is required"):
200
+ dm.push_tosca_dataset("int-1", name="", set_id="s")
201
+ mock_request.assert_not_called()
202
+
203
+
204
+ class TestPushToscaDatasetErrors:
205
+ """Each failure a scenario can act on arrives as its own type."""
206
+
207
+ @patch("requests.request")
208
+ def test_read_only_connection(self, mock_request, api_key):
209
+ body = {
210
+ "error": "This Tosca connection is read-only, so creating a test "
211
+ "data set is blocked."
212
+ }
213
+ mock_request.return_value = failed(403, body)
214
+
215
+ with pytest.raises(ReadOnlyConnectionError, match="read-only") as raised:
216
+ push(api_key, set_id="s")
217
+
218
+ assert raised.value.status_code == 403
219
+ assert raised.value.body == body
220
+
221
+ @patch("requests.request")
222
+ def test_403_for_an_unreadable_source_is_not_read_only(
223
+ self, mock_request, api_key
224
+ ):
225
+ mock_request.return_value = failed(403, {"error": "Missing set:read"})
226
+
227
+ with pytest.raises(DataMakerAPIError) as raised:
228
+ push(api_key, set_id="s")
229
+
230
+ assert type(raised.value) is DataMakerAPIError
231
+ assert raised.value.status_code == 403
232
+
233
+ @patch("requests.request")
234
+ def test_schema_change(self, mock_request, api_key):
235
+ mock_request.return_value = failed(
236
+ 409,
237
+ {
238
+ "error": "The rows no longer have column Country",
239
+ "datasetId": "TDS-1",
240
+ "added": ["Region"],
241
+ "removed": ["Country"],
242
+ },
243
+ )
244
+
245
+ with pytest.raises(ToscaDatasetSchemaChangedError) as raised:
246
+ push(api_key, set_id="s", on_existing="overwrite")
247
+
248
+ assert raised.value.status_code == 409
249
+ assert raised.value.dataset_id == "TDS-1"
250
+ assert raised.value.added_columns == ["Region"]
251
+ assert raised.value.removed_columns == ["Country"]
252
+
253
+ @patch("requests.request")
254
+ def test_name_exists(self, mock_request, api_key):
255
+ mock_request.return_value = failed(
256
+ 409, {"error": "A data set named Orders exists", "datasetId": "TDS-1"}
257
+ )
258
+
259
+ with pytest.raises(ToscaDatasetExistsError) as raised:
260
+ push(api_key, set_id="s")
261
+
262
+ assert raised.value.status_code == 409
263
+ assert raised.value.dataset_id == "TDS-1"
264
+
265
+ @pytest.mark.parametrize(
266
+ "body, on_existing, expected",
267
+ [
268
+ ({"code": "exists", "datasetId": "TDS-1"}, "fail", ToscaDatasetExistsError),
269
+ ({"code": "locked", "datasetId": "TDS-1"}, "overwrite", ToscaDatasetLockedError),
270
+ (
271
+ {"code": "schemaChanged", "datasetId": "TDS-1", "added": [], "removed": ["A"]},
272
+ "overwrite",
273
+ ToscaDatasetSchemaChangedError,
274
+ ),
275
+ # The code wins over what the request's on_existing would suggest.
276
+ ({"code": "locked", "datasetId": "TDS-1"}, "fail", ToscaDatasetLockedError),
277
+ ],
278
+ )
279
+ @patch("requests.request")
280
+ def test_409_code_names_the_reason(
281
+ self, mock_request, api_key, body, on_existing, expected
282
+ ):
283
+ mock_request.return_value = failed(409, {"error": "refused", **body})
284
+
285
+ with pytest.raises(expected) as raised:
286
+ push(api_key, set_id="s", on_existing=on_existing)
287
+
288
+ assert type(raised.value) is expected
289
+ assert raised.value.dataset_id == "TDS-1"
290
+
291
+ @patch("requests.request")
292
+ def test_409_with_an_unknown_code_stays_generic(self, mock_request, api_key):
293
+ mock_request.return_value = failed(409, {"error": "refused", "code": "other"})
294
+
295
+ with pytest.raises(DataMakerAPIError) as raised:
296
+ push(api_key, set_id="s", on_existing="new")
297
+
298
+ assert type(raised.value) is DataMakerAPIError
299
+
300
+ @patch("requests.request")
301
+ def test_locked_data_set(self, mock_request, api_key):
302
+ # No code: same body shape as "name exists", told apart by the request.
303
+ mock_request.return_value = failed(
304
+ 409, {"error": 'Data set "Orders" is locked', "datasetId": "TDS-1"}
305
+ )
306
+
307
+ with pytest.raises(ToscaDatasetLockedError) as raised:
308
+ push(api_key, set_id="s", on_existing="overwrite")
309
+
310
+ assert raised.value.dataset_id == "TDS-1"
311
+
312
+ @patch("requests.request")
313
+ def test_too_large_before_loading_names_no_numbers(self, mock_request, api_key):
314
+ mock_request.return_value = failed(
315
+ 413, {"error": "A push may write at most 50000 rows; dataset has 60000."}
316
+ )
317
+
318
+ with pytest.raises(ToscaDatasetTooLargeError, match="50000") as raised:
319
+ push(api_key, dataset_id="d")
320
+
321
+ assert raised.value.max_rows is None
322
+ assert raised.value.max_bytes is None
323
+
324
+ @patch("requests.request")
325
+ def test_too_many_rows(self, mock_request, api_key):
326
+ mock_request.return_value = failed(
327
+ 413, {"error": "Too many rows", "maxRows": 50000, "rows": 60000}
328
+ )
329
+
330
+ with pytest.raises(ToscaDatasetTooLargeError) as raised:
331
+ push(api_key, set_id="s")
332
+
333
+ assert raised.value.status_code == 413
334
+ assert raised.value.max_rows == 50000
335
+ assert raised.value.rows == 60000
336
+ assert raised.value.max_bytes is None
337
+
338
+ @patch("requests.request")
339
+ def test_too_many_bytes(self, mock_request, api_key):
340
+ mock_request.return_value = failed(
341
+ 413, {"error": "Too large", "maxBytes": 1000, "bytes": 2000}
342
+ )
343
+
344
+ with pytest.raises(ToscaDatasetTooLargeError) as raised:
345
+ push(api_key, set_id="s")
346
+
347
+ assert raised.value.max_bytes == 1000
348
+ assert raised.value.bytes == 2000
349
+ assert raised.value.max_rows is None
350
+
351
+ @pytest.mark.parametrize(
352
+ "status_code, body",
353
+ [
354
+ (400, {"error": "The set is not locked"}),
355
+ (404, {"error": "Set not found"}),
356
+ (429, {"message": "Too many Tosca data set pushes. Try again in 60s."}),
357
+ (502, {"error": "Tosca refused the data set", "toscaError": "boom"}),
358
+ ],
359
+ )
360
+ @patch("requests.request")
361
+ def test_other_failures_carry_status_and_body(
362
+ self, mock_request, api_key, status_code, body
363
+ ):
364
+ mock_request.return_value = failed(status_code, body)
365
+
366
+ with pytest.raises(DataMakerAPIError) as raised:
367
+ push(api_key, set_id="s")
368
+
369
+ assert type(raised.value) is DataMakerAPIError
370
+ assert raised.value.status_code == status_code
371
+ assert raised.value.body == body
372
+
373
+ @patch("requests.request")
374
+ def test_typed_errors_are_still_datamaker_errors(self, mock_request, api_key):
375
+ mock_request.return_value = failed(413, {"error": "too big", "maxRows": 10})
376
+
377
+ with pytest.raises(DataMakerError):
378
+ push(api_key, set_id="s")
379
+
380
+
381
+ class TestGetToscaDatasets:
382
+ @patch("requests.request")
383
+ def test_lists_a_space(self, mock_request, api_key):
384
+ listing = {
385
+ "space": "qa",
386
+ "items": [
387
+ {"id": "TDS-1", "name": "Orders", "subsetCount": 2, "isLocked": False}
388
+ ],
389
+ }
390
+ mock_request.return_value = ok(listing, status_code=200)
391
+
392
+ result = DataMaker(api_key=api_key).get_tosca_datasets("int-1", space="my qa")
393
+
394
+ method, url = mock_request.call_args.args
395
+ assert method == "GET"
396
+ assert url.endswith("/integrations/int-1/tosca/datasets?space=my%20qa")
397
+ assert result == listing
398
+
399
+ @patch("requests.request")
400
+ def test_default_space(self, mock_request, api_key):
401
+ mock_request.return_value = ok({"space": "default", "items": []}, 200)
402
+
403
+ DataMaker(api_key=api_key).get_tosca_datasets("int-1")
404
+
405
+ assert mock_request.call_args.args[1].endswith(
406
+ "/integrations/int-1/tosca/datasets"
407
+ )
408
+
409
+
410
+ class TestApiErrorDetails:
411
+ """Every failed request now carries its status and body."""
412
+
413
+ @patch("requests.request")
414
+ def test_non_json_body(self, mock_request, api_key):
415
+ response = Mock()
416
+ response.status_code = 502
417
+ response.text = "Bad Gateway"
418
+ response.json.side_effect = ValueError("not json")
419
+ mock_request.return_value = response
420
+
421
+ with pytest.raises(DataMakerAPIError, match="Bad Gateway") as raised:
422
+ DataMaker(api_key=api_key).get_projects()
423
+
424
+ assert raised.value.status_code == 502
425
+ assert raised.value.body is None
@@ -1,2 +0,0 @@
1
- from .main import DataMaker # noqa
2
- from .template import Template # noqa
@@ -1,2 +0,0 @@
1
- class DataMakerError(Exception):
2
- pass
File without changes
File without changes