datamaker-py 0.7.0__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.
- datamaker/__init__.py +2 -0
- datamaker/core.py +11 -0
- datamaker/error.py +2 -0
- datamaker/main.py +886 -0
- datamaker/routes/__init__.py +37 -0
- datamaker/routes/api_keys.py +50 -0
- datamaker/routes/base.py +40 -0
- datamaker/routes/connections.py +87 -0
- datamaker/routes/custom_types.py +100 -0
- datamaker/routes/export_and_validation.py +25 -0
- datamaker/routes/folders_and_utils.py +80 -0
- datamaker/routes/generation.py +14 -0
- datamaker/routes/projects.py +35 -0
- datamaker/routes/scenario_files.py +453 -0
- datamaker/routes/sets.py +177 -0
- datamaker/routes/teams.py +62 -0
- datamaker/routes/templates.py +69 -0
- datamaker/routes/users.py +43 -0
- datamaker/template.py +162 -0
- datamaker_py-0.7.0.dist-info/METADATA +79 -0
- datamaker_py-0.7.0.dist-info/RECORD +24 -0
- datamaker_py-0.7.0.dist-info/WHEEL +5 -0
- datamaker_py-0.7.0.dist-info/licenses/LICENSE +21 -0
- datamaker_py-0.7.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
"""Client for scenario file operations - CRUD functionality for file persistence in scenarios."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import base64
|
|
5
|
+
import mimetypes
|
|
6
|
+
import requests
|
|
7
|
+
from typing import Dict, List, Optional, Union, BinaryIO
|
|
8
|
+
from .base import BaseClient
|
|
9
|
+
from ..error import DataMakerError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ScenarioFilesClient(BaseClient):
|
|
13
|
+
"""Client for scenario file operations.
|
|
14
|
+
|
|
15
|
+
Provides CRUD functionality for persisting and managing files within scenarios.
|
|
16
|
+
Files stored through this client will appear in the 'Recently Added Files' folder
|
|
17
|
+
on the DataMaker scenarios page.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def get_scenario_files(
|
|
21
|
+
self, scenario_id: Optional[str] = None, folder: Optional[str] = None
|
|
22
|
+
) -> List[Dict]:
|
|
23
|
+
"""Fetch all files for a scenario.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
scenario_id: Optional scenario ID. Falls back to DATAMAKER_SCENARIO_ID env var.
|
|
27
|
+
folder: Optional folder filter ("uploads" or "outputs").
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Dictionary containing files list, storage usage, and storage limit.
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
DataMakerError: If scenario_id is not provided and not available in environment.
|
|
34
|
+
"""
|
|
35
|
+
# Get scenario_id from environment if not provided
|
|
36
|
+
scenario_id = scenario_id or os.environ.get("DATAMAKER_SCENARIO_ID")
|
|
37
|
+
|
|
38
|
+
if not scenario_id:
|
|
39
|
+
raise DataMakerError(
|
|
40
|
+
"scenario_id is required. Either pass it as a parameter or set "
|
|
41
|
+
"DATAMAKER_SCENARIO_ID environment variable."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
endpoint = f"/scenarios/{scenario_id}/files"
|
|
45
|
+
if folder:
|
|
46
|
+
endpoint += f"?folder={folder}"
|
|
47
|
+
response = self._make_request("GET", endpoint)
|
|
48
|
+
return response.json()
|
|
49
|
+
|
|
50
|
+
def get_scenario_file(
|
|
51
|
+
self, file_id: str, scenario_id: Optional[str] = None
|
|
52
|
+
) -> Dict:
|
|
53
|
+
"""Get metadata for a specific file by ID.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
file_id: The unique identifier of the file.
|
|
57
|
+
scenario_id: Optional scenario ID. Falls back to DATAMAKER_SCENARIO_ID env var.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
File metadata dictionary with refreshed presigned URL.
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
DataMakerError: If scenario_id is not provided and not available in environment.
|
|
64
|
+
"""
|
|
65
|
+
# Get scenario_id from environment if not provided
|
|
66
|
+
scenario_id = scenario_id or os.environ.get("DATAMAKER_SCENARIO_ID")
|
|
67
|
+
|
|
68
|
+
if not scenario_id:
|
|
69
|
+
raise DataMakerError(
|
|
70
|
+
"scenario_id is required. Either pass it as a parameter or set "
|
|
71
|
+
"DATAMAKER_SCENARIO_ID environment variable."
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
response = self._make_request(
|
|
75
|
+
"GET", f"/scenarios/{scenario_id}/files/{file_id}"
|
|
76
|
+
)
|
|
77
|
+
return response.json()
|
|
78
|
+
|
|
79
|
+
def download_scenario_file(
|
|
80
|
+
self, file_id: str, scenario_id: Optional[str] = None
|
|
81
|
+
) -> bytes:
|
|
82
|
+
"""Download a file's content by ID.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
file_id: The unique identifier of the file.
|
|
86
|
+
scenario_id: Optional scenario ID. Falls back to DATAMAKER_SCENARIO_ID env var.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
The file content as bytes.
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
DataMakerError: If scenario_id is not provided and not available in environment.
|
|
93
|
+
"""
|
|
94
|
+
# Get file metadata with presigned URL first
|
|
95
|
+
file_metadata = self.get_scenario_file(file_id, scenario_id)
|
|
96
|
+
|
|
97
|
+
if not file_metadata.get("presignedUrl"):
|
|
98
|
+
raise DataMakerError(f"No presigned URL available for file: {file_id}")
|
|
99
|
+
|
|
100
|
+
# Download file using presigned URL
|
|
101
|
+
presigned_url = file_metadata["presignedUrl"]
|
|
102
|
+
download_response = requests.get(presigned_url, timeout=30)
|
|
103
|
+
|
|
104
|
+
if download_response.status_code == 404:
|
|
105
|
+
raise DataMakerError(f"File not found: {file_id}")
|
|
106
|
+
elif download_response.status_code != 200:
|
|
107
|
+
raise DataMakerError(
|
|
108
|
+
f"Failed to download file: HTTP {download_response.status_code}"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return download_response.content
|
|
112
|
+
|
|
113
|
+
def download_scenario_file_to_path(
|
|
114
|
+
self, file_id: str, destination_path: str, scenario_id: Optional[str] = None
|
|
115
|
+
) -> str:
|
|
116
|
+
"""Download a file and save it to a local path.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
file_id: The unique identifier of the file.
|
|
120
|
+
destination_path: Local file path to save the downloaded content.
|
|
121
|
+
scenario_id: Optional scenario ID. Falls back to DATAMAKER_SCENARIO_ID env var.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
The destination path where the file was saved.
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
DataMakerError: If scenario_id is not provided and not available in environment.
|
|
128
|
+
"""
|
|
129
|
+
content = self.download_scenario_file(file_id, scenario_id)
|
|
130
|
+
with open(destination_path, "wb") as f:
|
|
131
|
+
f.write(content)
|
|
132
|
+
return destination_path
|
|
133
|
+
|
|
134
|
+
def create_scenario_file(
|
|
135
|
+
self,
|
|
136
|
+
name: str,
|
|
137
|
+
content: Union[str, bytes, BinaryIO],
|
|
138
|
+
scenario_id: Optional[str] = None,
|
|
139
|
+
team_id: Optional[str] = None,
|
|
140
|
+
project_id: Optional[str] = None,
|
|
141
|
+
description: Optional[str] = None,
|
|
142
|
+
mime_type: Optional[str] = None,
|
|
143
|
+
folder_id: Optional[str] = None,
|
|
144
|
+
) -> Dict:
|
|
145
|
+
"""Create/upload a new file in a scenario.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
name: The filename (e.g., 'data.json', 'config.yaml').
|
|
149
|
+
content: The file content - can be a string, bytes, or file-like object.
|
|
150
|
+
scenario_id: Optional scenario ID. Falls back to DATAMAKER_SCENARIO_ID env var.
|
|
151
|
+
team_id: Optional team ID. Falls back to DATAMAKER_TEAM_ID env var.
|
|
152
|
+
project_id: Optional project ID to associate with. Falls back to DATAMAKER_PROJECT_ID env var.
|
|
153
|
+
description: Optional description of the file.
|
|
154
|
+
mime_type: Optional MIME type. If not provided, will be guessed from filename.
|
|
155
|
+
folder_id: Optional folder ID to place the file in.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
The created file metadata dictionary.
|
|
159
|
+
|
|
160
|
+
Raises:
|
|
161
|
+
DataMakerError: If required IDs are not provided and not available in environment.
|
|
162
|
+
"""
|
|
163
|
+
# Get required IDs from environment if not provided
|
|
164
|
+
scenario_id = scenario_id or os.environ.get("DATAMAKER_SCENARIO_ID")
|
|
165
|
+
team_id = team_id or os.environ.get("DATAMAKER_TEAM_ID")
|
|
166
|
+
project_id = project_id or os.environ.get("DATAMAKER_PROJECT_ID")
|
|
167
|
+
|
|
168
|
+
if not scenario_id:
|
|
169
|
+
raise DataMakerError(
|
|
170
|
+
"scenario_id is required. Either pass it as a parameter or set "
|
|
171
|
+
"DATAMAKER_SCENARIO_ID environment variable."
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
if not team_id:
|
|
175
|
+
raise DataMakerError(
|
|
176
|
+
"team_id is required. Either pass it as a parameter or set "
|
|
177
|
+
"DATAMAKER_TEAM_ID environment variable."
|
|
178
|
+
)
|
|
179
|
+
# Handle different content types
|
|
180
|
+
if isinstance(content, str):
|
|
181
|
+
content_bytes = content.encode("utf-8")
|
|
182
|
+
elif hasattr(content, "read"):
|
|
183
|
+
content_bytes = content.read()
|
|
184
|
+
else:
|
|
185
|
+
content_bytes = content
|
|
186
|
+
|
|
187
|
+
# Encode content as base64 for API transmission
|
|
188
|
+
content_base64 = base64.b64encode(content_bytes).decode("utf-8")
|
|
189
|
+
|
|
190
|
+
# Guess MIME type if not provided
|
|
191
|
+
if not mime_type:
|
|
192
|
+
mime_type, _ = mimetypes.guess_type(name)
|
|
193
|
+
if not mime_type:
|
|
194
|
+
mime_type = "application/octet-stream"
|
|
195
|
+
|
|
196
|
+
file_data = {
|
|
197
|
+
"name": name,
|
|
198
|
+
"content": content_base64,
|
|
199
|
+
"scenarioId": scenario_id,
|
|
200
|
+
"teamId": team_id,
|
|
201
|
+
"mimeType": mime_type,
|
|
202
|
+
"size": len(content_bytes),
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if project_id:
|
|
206
|
+
file_data["projectId"] = project_id
|
|
207
|
+
if description:
|
|
208
|
+
file_data["description"] = description
|
|
209
|
+
if folder_id:
|
|
210
|
+
file_data["folderId"] = folder_id
|
|
211
|
+
|
|
212
|
+
response = self._make_request("POST", "/scenario-files", json=file_data)
|
|
213
|
+
return response.json()
|
|
214
|
+
|
|
215
|
+
def upload_scenario_file_from_path(
|
|
216
|
+
self,
|
|
217
|
+
file_path: str,
|
|
218
|
+
scenario_id: Optional[str] = None,
|
|
219
|
+
team_id: Optional[str] = None,
|
|
220
|
+
project_id: Optional[str] = None,
|
|
221
|
+
name: Optional[str] = None,
|
|
222
|
+
description: Optional[str] = None,
|
|
223
|
+
folder_id: Optional[str] = None,
|
|
224
|
+
folder: str = "uploads",
|
|
225
|
+
) -> Dict:
|
|
226
|
+
"""Upload a file from a local path to a scenario using multipart upload.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
file_path: Path to the local file to upload.
|
|
230
|
+
scenario_id: Optional scenario ID. Falls back to DATAMAKER_SCENARIO_ID env var.
|
|
231
|
+
team_id: Optional team ID. Falls back to DATAMAKER_TEAM_ID env var (not used for upload).
|
|
232
|
+
project_id: Optional project ID to associate with. Falls back to DATAMAKER_PROJECT_ID env var (not used for upload).
|
|
233
|
+
name: Optional filename. If not provided, uses the original filename.
|
|
234
|
+
description: Optional description of the file (not used for multipart upload).
|
|
235
|
+
folder_id: Optional folder ID to place the file in (not used for multipart upload).
|
|
236
|
+
folder: Folder to upload to ("uploads" or "outputs", default: "uploads").
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
The created file metadata dictionary.
|
|
240
|
+
|
|
241
|
+
Raises:
|
|
242
|
+
DataMakerError: If file not found or required IDs are not provided and not available in environment.
|
|
243
|
+
"""
|
|
244
|
+
# Get required IDs from environment if not provided
|
|
245
|
+
scenario_id = scenario_id or os.environ.get("DATAMAKER_SCENARIO_ID")
|
|
246
|
+
|
|
247
|
+
if not scenario_id:
|
|
248
|
+
raise DataMakerError(
|
|
249
|
+
"scenario_id is required. Either pass it as a parameter or set "
|
|
250
|
+
"DATAMAKER_SCENARIO_ID environment variable."
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
if not os.path.exists(file_path):
|
|
254
|
+
raise DataMakerError(f"File not found: {file_path}")
|
|
255
|
+
|
|
256
|
+
filename = name or os.path.basename(file_path)
|
|
257
|
+
|
|
258
|
+
# Use multipart upload endpoint for better performance
|
|
259
|
+
with open(file_path, "rb") as f:
|
|
260
|
+
files = {"file": (filename, f)}
|
|
261
|
+
data = {"folder": folder}
|
|
262
|
+
|
|
263
|
+
response = self._make_request(
|
|
264
|
+
"POST",
|
|
265
|
+
f"/scenarios/{scenario_id}/files/upload",
|
|
266
|
+
files=files,
|
|
267
|
+
data=data,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
result = response.json()
|
|
271
|
+
return result.get("file", result)
|
|
272
|
+
|
|
273
|
+
def delete_scenario_file(
|
|
274
|
+
self, file_id: str, scenario_id: Optional[str] = None
|
|
275
|
+
) -> Dict:
|
|
276
|
+
"""Delete a file by ID.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
file_id: The unique identifier of the file to delete.
|
|
280
|
+
scenario_id: Optional scenario ID. Falls back to DATAMAKER_SCENARIO_ID env var.
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
Confirmation response.
|
|
284
|
+
|
|
285
|
+
Raises:
|
|
286
|
+
DataMakerError: If scenario_id is not provided and not available in environment.
|
|
287
|
+
"""
|
|
288
|
+
# Get scenario_id from environment if not provided
|
|
289
|
+
scenario_id = scenario_id or os.environ.get("DATAMAKER_SCENARIO_ID")
|
|
290
|
+
|
|
291
|
+
if not scenario_id:
|
|
292
|
+
raise DataMakerError(
|
|
293
|
+
"scenario_id is required. Either pass it as a parameter or set "
|
|
294
|
+
"DATAMAKER_SCENARIO_ID environment variable."
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
response = self._make_request(
|
|
298
|
+
"DELETE", f"/scenarios/{scenario_id}/files/{file_id}"
|
|
299
|
+
)
|
|
300
|
+
return response.json()
|
|
301
|
+
|
|
302
|
+
def read_file_by_path(
|
|
303
|
+
self,
|
|
304
|
+
file_path: str,
|
|
305
|
+
storage_base_url: Optional[str] = None,
|
|
306
|
+
) -> bytes:
|
|
307
|
+
"""Read a file directly from storage by its path.
|
|
308
|
+
|
|
309
|
+
This method allows you to read files from the storage bucket using the
|
|
310
|
+
file path copied from the workspace files modal. It's useful for accessing
|
|
311
|
+
files uploaded to scenarios from within Python scripts.
|
|
312
|
+
|
|
313
|
+
The method first queries the API to get a presigned URL for the file,
|
|
314
|
+
then downloads the file content using that URL.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
file_path: The file path (key) in storage. This is typically copied from
|
|
318
|
+
the workspace files modal using the "Copy path" button.
|
|
319
|
+
Example: "scenarios/team-id/project-id/scenario-id/workspace/uploads/filename.txt"
|
|
320
|
+
storage_base_url: Deprecated. This parameter is ignored as the method now uses
|
|
321
|
+
the API to get presigned URLs instead of direct storage access.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
The file content as bytes.
|
|
325
|
+
|
|
326
|
+
Raises:
|
|
327
|
+
DataMakerError: If the file cannot be read or doesn't exist.
|
|
328
|
+
|
|
329
|
+
Example:
|
|
330
|
+
>>> client = DataMaker(api_key="your-key")
|
|
331
|
+
>>> # Copy path from UI: "scenarios/.../workspace/uploads/data.json"
|
|
332
|
+
>>> content = client.read_file_by_path("scenarios/.../workspace/uploads/data.json")
|
|
333
|
+
>>> data = json.loads(content.decode('utf-8'))
|
|
334
|
+
"""
|
|
335
|
+
try:
|
|
336
|
+
# Get file metadata with presigned URL from API
|
|
337
|
+
response = self._make_request(
|
|
338
|
+
"GET", f"/workspace-files/by-key?key={file_path}"
|
|
339
|
+
)
|
|
340
|
+
file_data = response.json()
|
|
341
|
+
|
|
342
|
+
if not file_data.get("presignedUrl"):
|
|
343
|
+
raise DataMakerError(
|
|
344
|
+
f"No presigned URL available for file: {file_path}"
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
# Download file using presigned URL
|
|
348
|
+
presigned_url = file_data["presignedUrl"]
|
|
349
|
+
download_response = requests.get(presigned_url, timeout=30)
|
|
350
|
+
|
|
351
|
+
if download_response.status_code == 404:
|
|
352
|
+
raise DataMakerError(f"File not found at path: {file_path}")
|
|
353
|
+
elif download_response.status_code != 200:
|
|
354
|
+
raise DataMakerError(
|
|
355
|
+
f"Failed to download file: HTTP {download_response.status_code}"
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
return download_response.content
|
|
359
|
+
|
|
360
|
+
except DataMakerError:
|
|
361
|
+
raise
|
|
362
|
+
except requests.RequestException as e:
|
|
363
|
+
raise DataMakerError(f"Failed to read file from storage: {str(e)}")
|
|
364
|
+
except Exception as e:
|
|
365
|
+
raise DataMakerError(f"Failed to read file: {str(e)}")
|
|
366
|
+
|
|
367
|
+
def read_file_by_path_as_text(
|
|
368
|
+
self,
|
|
369
|
+
file_path: str,
|
|
370
|
+
storage_base_url: Optional[str] = None,
|
|
371
|
+
encoding: str = "utf-8",
|
|
372
|
+
) -> str:
|
|
373
|
+
"""Read a file directly from storage by its path and return as text.
|
|
374
|
+
|
|
375
|
+
Convenience method that reads a file and decodes it as text.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
file_path: The file path (key) in storage.
|
|
379
|
+
storage_base_url: Optional base URL for the storage bucket.
|
|
380
|
+
encoding: Text encoding to use (default: 'utf-8').
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
The file content as a string.
|
|
384
|
+
|
|
385
|
+
Example:
|
|
386
|
+
>>> client = DataMaker(api_key="your-key")
|
|
387
|
+
>>> content = client.read_file_by_path_as_text("scenarios/abc123/uploads/data.txt")
|
|
388
|
+
>>> print(content)
|
|
389
|
+
"""
|
|
390
|
+
content = self.read_file_by_path(file_path, storage_base_url)
|
|
391
|
+
return content.decode(encoding)
|
|
392
|
+
|
|
393
|
+
def save_file(
|
|
394
|
+
self,
|
|
395
|
+
file_path: str,
|
|
396
|
+
scenario_id: Optional[str] = None,
|
|
397
|
+
team_id: Optional[str] = None,
|
|
398
|
+
project_id: Optional[str] = None,
|
|
399
|
+
name: Optional[str] = None,
|
|
400
|
+
description: Optional[str] = None,
|
|
401
|
+
folder_id: Optional[str] = None,
|
|
402
|
+
folder: str = "uploads",
|
|
403
|
+
) -> Dict:
|
|
404
|
+
"""Convenience method to save a local file to workspace storage.
|
|
405
|
+
|
|
406
|
+
This method simplifies uploading files by automatically pulling required
|
|
407
|
+
context (scenario_id) from environment variables if not provided.
|
|
408
|
+
This is especially useful in DataMaker sandbox environments where these
|
|
409
|
+
variables are pre-configured.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
file_path: Path to the local file to save to workspace.
|
|
413
|
+
scenario_id: Optional scenario ID. Falls back to DATAMAKER_SCENARIO_ID env var.
|
|
414
|
+
team_id: Optional team ID (deprecated, not used for multipart upload).
|
|
415
|
+
project_id: Optional project ID (deprecated, not used for multipart upload).
|
|
416
|
+
name: Optional filename. If not provided, uses the original filename.
|
|
417
|
+
description: Optional description of the file (deprecated, not used for multipart upload).
|
|
418
|
+
folder_id: Optional folder ID (deprecated, not used for multipart upload).
|
|
419
|
+
folder: Folder to upload to ("uploads" or "outputs", default: "uploads").
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
The created file metadata dictionary.
|
|
423
|
+
|
|
424
|
+
Raises:
|
|
425
|
+
DataMakerError: If scenario_id is not provided and not available in environment.
|
|
426
|
+
|
|
427
|
+
Example:
|
|
428
|
+
>>> # In a DataMaker sandbox environment:
|
|
429
|
+
>>> dm = DataMaker()
|
|
430
|
+
>>> result = dm.save_file("test_file.txt")
|
|
431
|
+
>>> print(f"File saved: {result['name']}")
|
|
432
|
+
|
|
433
|
+
>>> # Or specify IDs explicitly:
|
|
434
|
+
>>> result = dm.save_file(
|
|
435
|
+
... "test_file.txt",
|
|
436
|
+
... scenario_id="scenario-123"
|
|
437
|
+
... )
|
|
438
|
+
"""
|
|
439
|
+
# Get required IDs from environment if not provided
|
|
440
|
+
scenario_id = scenario_id or os.environ.get("DATAMAKER_SCENARIO_ID")
|
|
441
|
+
|
|
442
|
+
if not scenario_id:
|
|
443
|
+
raise DataMakerError(
|
|
444
|
+
"scenario_id is required. Either pass it as a parameter or set "
|
|
445
|
+
"DATAMAKER_SCENARIO_ID environment variable."
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
return self.upload_scenario_file_from_path(
|
|
449
|
+
file_path=file_path,
|
|
450
|
+
scenario_id=scenario_id,
|
|
451
|
+
name=name,
|
|
452
|
+
folder=folder,
|
|
453
|
+
)
|
datamaker/routes/sets.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Client for set operations - persistent named snapshots of rows.
|
|
2
|
+
|
|
3
|
+
A "set" is a saved snapshot of generated or fetched rows (e.g. a pinned
|
|
4
|
+
regression dataset, a shared fixture, or an audit copy of what was masked and
|
|
5
|
+
exported). Sets are scoped to a project/team and show up on the DataMaker Sets
|
|
6
|
+
page.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from typing import Dict, List, Optional, Any
|
|
11
|
+
from .base import BaseClient
|
|
12
|
+
from ..error import DataMakerError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SetsClient(BaseClient):
|
|
16
|
+
"""Client for set operations (persistent saved row snapshots)."""
|
|
17
|
+
|
|
18
|
+
def get_sets(self, project_id: Optional[str] = None) -> List[Dict]:
|
|
19
|
+
"""Fetch all saved sets for the caller's project/team scope.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
project_id: Optional project ID to scope the listing to. Falls back
|
|
23
|
+
to the DATAMAKER_PROJECT_ID env var. When omitted, the API
|
|
24
|
+
returns every set the API key can access.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
A list of set metadata dictionaries.
|
|
28
|
+
"""
|
|
29
|
+
project_id = project_id or os.environ.get("DATAMAKER_PROJECT_ID")
|
|
30
|
+
|
|
31
|
+
endpoint = "/sets"
|
|
32
|
+
if project_id:
|
|
33
|
+
endpoint += f"?projectId={project_id}"
|
|
34
|
+
|
|
35
|
+
response = self._make_request("GET", endpoint)
|
|
36
|
+
return response.json()
|
|
37
|
+
|
|
38
|
+
def get_set(self, set_id: str) -> Dict:
|
|
39
|
+
"""Get a single saved set by ID, including its full ``data`` payload.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
set_id: The unique identifier of the set.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
The set dictionary (including its saved rows in ``data``).
|
|
46
|
+
"""
|
|
47
|
+
response = self._make_request("GET", f"/sets/{set_id}")
|
|
48
|
+
return response.json()
|
|
49
|
+
|
|
50
|
+
def create_set(
|
|
51
|
+
self,
|
|
52
|
+
name: str,
|
|
53
|
+
data: Optional[Any] = None,
|
|
54
|
+
description: Optional[str] = None,
|
|
55
|
+
row_count: Optional[int] = None,
|
|
56
|
+
project_id: Optional[str] = None,
|
|
57
|
+
) -> Dict:
|
|
58
|
+
"""Create (save) a new set.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
name: A name for the saved set.
|
|
62
|
+
data: The rows payload to save - typically a list of row dicts.
|
|
63
|
+
When omitted, an empty set is created.
|
|
64
|
+
description: Optional short description of what the set captures.
|
|
65
|
+
row_count: Optional explicit row count. Derived from ``data`` by the
|
|
66
|
+
API when omitted.
|
|
67
|
+
project_id: Optional project ID the set belongs to. Falls back to the
|
|
68
|
+
DATAMAKER_PROJECT_ID env var. Required when the API key is not
|
|
69
|
+
already scoped to a single project.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
The created set dictionary.
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
DataMakerError: If ``name`` is not provided.
|
|
76
|
+
"""
|
|
77
|
+
if not name:
|
|
78
|
+
raise DataMakerError("name is required to create a set.")
|
|
79
|
+
|
|
80
|
+
project_id = project_id or os.environ.get("DATAMAKER_PROJECT_ID")
|
|
81
|
+
|
|
82
|
+
set_data: Dict[str, Any] = {"name": name}
|
|
83
|
+
if data is not None:
|
|
84
|
+
set_data["data"] = data
|
|
85
|
+
if description is not None:
|
|
86
|
+
set_data["description"] = description
|
|
87
|
+
if row_count is not None:
|
|
88
|
+
set_data["rowCount"] = row_count
|
|
89
|
+
if project_id:
|
|
90
|
+
set_data["projectId"] = project_id
|
|
91
|
+
|
|
92
|
+
response = self._make_request("POST", "/sets", json=set_data)
|
|
93
|
+
return response.json()
|
|
94
|
+
|
|
95
|
+
def update_set(
|
|
96
|
+
self,
|
|
97
|
+
set_id: str,
|
|
98
|
+
name: Optional[str] = None,
|
|
99
|
+
description: Optional[str] = None,
|
|
100
|
+
data: Optional[Any] = None,
|
|
101
|
+
row_count: Optional[int] = None,
|
|
102
|
+
) -> Dict:
|
|
103
|
+
"""Update a saved set.
|
|
104
|
+
|
|
105
|
+
Only the fields that are provided are sent; the rest are left unchanged.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
set_id: The unique identifier of the set to update.
|
|
109
|
+
name: New name for the set.
|
|
110
|
+
description: New description (pass ``None`` to leave unchanged).
|
|
111
|
+
data: Replacement rows payload. The API keeps ``rowCount`` in sync
|
|
112
|
+
whenever ``data`` changes.
|
|
113
|
+
row_count: Explicit row count override.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
The updated set dictionary.
|
|
117
|
+
"""
|
|
118
|
+
update_data: Dict[str, Any] = {}
|
|
119
|
+
if name is not None:
|
|
120
|
+
update_data["name"] = name
|
|
121
|
+
if description is not None:
|
|
122
|
+
update_data["description"] = description
|
|
123
|
+
if data is not None:
|
|
124
|
+
update_data["data"] = data
|
|
125
|
+
if row_count is not None:
|
|
126
|
+
update_data["rowCount"] = row_count
|
|
127
|
+
|
|
128
|
+
response = self._make_request("PATCH", f"/sets/{set_id}", json=update_data)
|
|
129
|
+
return response.json()
|
|
130
|
+
|
|
131
|
+
def delete_set(self, set_id: str) -> Dict:
|
|
132
|
+
"""Delete a saved set by ID.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
set_id: The unique identifier of the set to delete.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Confirmation response.
|
|
139
|
+
"""
|
|
140
|
+
response = self._make_request("DELETE", f"/sets/{set_id}")
|
|
141
|
+
return response.json()
|
|
142
|
+
|
|
143
|
+
def save_set(
|
|
144
|
+
self,
|
|
145
|
+
name: str,
|
|
146
|
+
data: Any,
|
|
147
|
+
description: Optional[str] = None,
|
|
148
|
+
project_id: Optional[str] = None,
|
|
149
|
+
) -> Dict:
|
|
150
|
+
"""Convenience method to save rows as a named set.
|
|
151
|
+
|
|
152
|
+
This is the most common entry point: hand it a name and the rows you
|
|
153
|
+
want to keep. Project context is pulled from the DATAMAKER_PROJECT_ID
|
|
154
|
+
env var when not provided, which is convenient inside DataMaker sandbox
|
|
155
|
+
environments where it is pre-configured.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
name: A name for the saved set.
|
|
159
|
+
data: The rows to save - typically a list of row dicts.
|
|
160
|
+
description: Optional short description of what the set captures.
|
|
161
|
+
project_id: Optional project ID. Falls back to DATAMAKER_PROJECT_ID.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
The created set dictionary.
|
|
165
|
+
|
|
166
|
+
Example:
|
|
167
|
+
>>> dm = DataMaker()
|
|
168
|
+
>>> rows = dm.generate(template)
|
|
169
|
+
>>> result = dm.save_set("nightly-regression", rows)
|
|
170
|
+
>>> print(f"Saved set: {result['name']} ({result['rowCount']} rows)")
|
|
171
|
+
"""
|
|
172
|
+
return self.create_set(
|
|
173
|
+
name=name,
|
|
174
|
+
data=data,
|
|
175
|
+
description=description,
|
|
176
|
+
project_id=project_id,
|
|
177
|
+
)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from .base import BaseClient
|
|
2
|
+
from typing import Optional, Dict, List
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TeamsClient(BaseClient):
|
|
6
|
+
"""Client for team management operations."""
|
|
7
|
+
|
|
8
|
+
def get_teams(self) -> List[Dict]:
|
|
9
|
+
"""Get all teams."""
|
|
10
|
+
response = self._make_request("GET", "/teams")
|
|
11
|
+
return response.json()
|
|
12
|
+
|
|
13
|
+
def create_team(self, team_data: Dict) -> Dict:
|
|
14
|
+
"""Create a new team."""
|
|
15
|
+
response = self._make_request("POST", "/teams", json=team_data)
|
|
16
|
+
return response.json()
|
|
17
|
+
|
|
18
|
+
def update_team(self, team_id: str, team_data: Dict) -> Dict:
|
|
19
|
+
"""Update a team."""
|
|
20
|
+
response = self._make_request("PUT", f"/teams/{team_id}", json=team_data)
|
|
21
|
+
return response.json()
|
|
22
|
+
|
|
23
|
+
def delete_team(self, team_id: str) -> Dict:
|
|
24
|
+
"""Delete a team."""
|
|
25
|
+
response = self._make_request("DELETE", f"/teams/{team_id}")
|
|
26
|
+
return response.json()
|
|
27
|
+
|
|
28
|
+
def setup_team(self, team_data: Dict) -> Dict:
|
|
29
|
+
"""Setup a new team."""
|
|
30
|
+
response = self._make_request("POST", "/setup/teams", json=team_data)
|
|
31
|
+
return response.json()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TeamMembersClient(BaseClient):
|
|
35
|
+
"""Client for team member management operations."""
|
|
36
|
+
|
|
37
|
+
def get_team_members(self) -> List[Dict]:
|
|
38
|
+
"""Get all team members."""
|
|
39
|
+
response = self._make_request("GET", "/teamMembers")
|
|
40
|
+
return response.json()
|
|
41
|
+
|
|
42
|
+
def add_team_member(self, member_data: Dict) -> Dict:
|
|
43
|
+
"""Add a new team member."""
|
|
44
|
+
response = self._make_request("POST", "/teamMembers", json=member_data)
|
|
45
|
+
return response.json()
|
|
46
|
+
|
|
47
|
+
def invite_team_member(self, invite_data: Dict) -> Dict:
|
|
48
|
+
"""Invite a new team member."""
|
|
49
|
+
response = self._make_request("POST", "/teamMembers/invite", json=invite_data)
|
|
50
|
+
return response.json()
|
|
51
|
+
|
|
52
|
+
def update_team_member(self, member_id: str, member_data: Dict) -> Dict:
|
|
53
|
+
"""Update a team member."""
|
|
54
|
+
response = self._make_request(
|
|
55
|
+
"PUT", f"/teamMembers/{member_id}", json=member_data
|
|
56
|
+
)
|
|
57
|
+
return response.json()
|
|
58
|
+
|
|
59
|
+
def remove_team_member(self, member_id: str) -> Dict:
|
|
60
|
+
"""Remove a team member."""
|
|
61
|
+
response = self._make_request("DELETE", f"/teamMembers/{member_id}")
|
|
62
|
+
return response.json()
|