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.
@@ -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
+ )
@@ -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()