msdev-kit 0.1.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.
- msdev_kit/__init__.py +3 -0
- msdev_kit/auth.py +35 -0
- msdev_kit/fabric/__init__.py +11 -0
- msdev_kit/fabric/admin.py +42 -0
- msdev_kit/fabric/capacity.py +186 -0
- msdev_kit/fabric/database.py +94 -0
- msdev_kit/fabric/dataflow.py +1801 -0
- msdev_kit/fabric/dataset.py +617 -0
- msdev_kit/fabric/kql.py +66 -0
- msdev_kit/fabric/notebook.py +88 -0
- msdev_kit/fabric/operations.py +108 -0
- msdev_kit/fabric/pipeline.py +505 -0
- msdev_kit/fabric/report.py +1012 -0
- msdev_kit/fabric/utilities.py +12 -0
- msdev_kit/fabric/workspace.py +516 -0
- msdev_kit/graph/__init__.py +1 -0
- msdev_kit/graph/client.py +91 -0
- msdev_kit/sharepoint/__init__.py +1 -0
- msdev_kit/sharepoint/client.py +105 -0
- msdev_kit-0.1.0.dist-info/METADATA +269 -0
- msdev_kit-0.1.0.dist-info/RECORD +22 -0
- msdev_kit-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import time
|
|
3
|
+
import base64
|
|
4
|
+
import requests
|
|
5
|
+
from typing import Dict, List
|
|
6
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
7
|
+
from .dataflow import Dataflow
|
|
8
|
+
from .notebook import Notebook
|
|
9
|
+
from .dataset import Dataset
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Pipeline:
|
|
13
|
+
|
|
14
|
+
def __init__(self, token: str):
|
|
15
|
+
"""
|
|
16
|
+
Initialize variables.
|
|
17
|
+
"""
|
|
18
|
+
self.fabric_api_base_url = 'https://api.fabric.microsoft.com'
|
|
19
|
+
self.token = token
|
|
20
|
+
self.headers = {'Authorization': f'Bearer {self.token}'}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _request_with_retry(self, method: str, url: str, max_retries: int = 3, **kwargs) -> requests.Response:
|
|
24
|
+
"""
|
|
25
|
+
Makes an HTTP request with automatic retry on 429 (Too Many Requests).
|
|
26
|
+
Respects the Retry-After header when present.
|
|
27
|
+
"""
|
|
28
|
+
for attempt in range(max_retries + 1):
|
|
29
|
+
response = requests.request(method, url, **kwargs)
|
|
30
|
+
if response.status_code != 429:
|
|
31
|
+
return response
|
|
32
|
+
|
|
33
|
+
retry_after = int(response.headers.get('Retry-After', 5))
|
|
34
|
+
print(f" Rate limited (429). Retrying in {retry_after}s... (attempt {attempt + 1}/{max_retries})")
|
|
35
|
+
time.sleep(retry_after)
|
|
36
|
+
|
|
37
|
+
return response
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _resolve_pipeline(self, workspace_id: str, pipeline_id_or_name: str) -> tuple:
|
|
41
|
+
"""
|
|
42
|
+
Resolves a pipeline identifier that can be either an ID or a display name.
|
|
43
|
+
Tries as ID first via get_pipeline, falls back to listing all pipelines
|
|
44
|
+
and matching by display name (case-insensitive).
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
workspace_id (str): The workspace ID.
|
|
48
|
+
pipeline_id_or_name (str): The pipeline ID or display name.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
tuple: (pipeline_id, pipeline_name) or (None, None) if not found.
|
|
52
|
+
"""
|
|
53
|
+
# Try as ID first
|
|
54
|
+
result = self.get_pipeline(workspace_id, pipeline_id_or_name)
|
|
55
|
+
if result.get('message') == 'Success':
|
|
56
|
+
content = result['content']
|
|
57
|
+
return content.get('id', pipeline_id_or_name), content.get('displayName', '')
|
|
58
|
+
|
|
59
|
+
# Fall back to name search
|
|
60
|
+
result = self.list_pipelines(workspace_id)
|
|
61
|
+
if result.get('message') != 'Success':
|
|
62
|
+
return None, None
|
|
63
|
+
|
|
64
|
+
for p in result['content']:
|
|
65
|
+
if p.get('displayName', '').lower() == pipeline_id_or_name.lower():
|
|
66
|
+
return p['id'], p['displayName']
|
|
67
|
+
|
|
68
|
+
return None, None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _resolve_dataflow_id(self, workspace_id: str, dataflow_id_or_name: str) -> tuple:
|
|
72
|
+
"""
|
|
73
|
+
Resolves a dataflow identifier that can be either an ID or a display name.
|
|
74
|
+
Tries as ID first via Dataflow.get_dataflow_name, falls back to listing
|
|
75
|
+
all dataflows and matching by name (case-insensitive).
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
workspace_id (str): The workspace ID.
|
|
79
|
+
dataflow_id_or_name (str): The dataflow ID or display name.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
tuple: (dataflow_id, dataflow_name) or (None, None) if not found.
|
|
83
|
+
"""
|
|
84
|
+
dataflow = Dataflow(self.token)
|
|
85
|
+
|
|
86
|
+
# Try as ID first
|
|
87
|
+
name = dataflow.get_dataflow_name(workspace_id, dataflow_id_or_name)
|
|
88
|
+
if name:
|
|
89
|
+
return dataflow_id_or_name, name
|
|
90
|
+
|
|
91
|
+
# Fall back to name search
|
|
92
|
+
result = dataflow.list_dataflows(workspace_id)
|
|
93
|
+
if result.get('message') != 'Success':
|
|
94
|
+
return None, None
|
|
95
|
+
|
|
96
|
+
for df in result['content']:
|
|
97
|
+
if (df.get('name') or '').lower() == dataflow_id_or_name.lower():
|
|
98
|
+
return df.get('id', ''), df.get('name', '')
|
|
99
|
+
|
|
100
|
+
return None, None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def list_pipelines(self, workspace_id: str) -> Dict:
|
|
104
|
+
"""
|
|
105
|
+
Lists all Fabric Data Pipelines in a workspace.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
workspace_id (str): The ID of the workspace.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Dict: 'message' and 'content' (list of pipeline dicts with id, displayName, description).
|
|
112
|
+
"""
|
|
113
|
+
if workspace_id == '':
|
|
114
|
+
return {'message': 'Missing workspace id, please check.', 'content': ''}
|
|
115
|
+
|
|
116
|
+
api_url = f'{self.fabric_api_base_url}/v1/workspaces/{workspace_id}/dataPipelines'
|
|
117
|
+
pipelines = []
|
|
118
|
+
|
|
119
|
+
while api_url:
|
|
120
|
+
response = self._request_with_retry('GET', api_url, headers=self.headers)
|
|
121
|
+
if response.status_code != 200:
|
|
122
|
+
error_data = response.json() if response.headers.get('content-type', '').startswith('application/json') else {}
|
|
123
|
+
error_message = error_data.get('message', error_data.get('error', {}).get('message', response.text))
|
|
124
|
+
return {'message': {'error': error_message, 'status_code': response.status_code}}
|
|
125
|
+
|
|
126
|
+
data = response.json()
|
|
127
|
+
pipelines.extend(data.get('value', []))
|
|
128
|
+
api_url = data.get('continuationUri', None)
|
|
129
|
+
|
|
130
|
+
return {'message': 'Success', 'content': pipelines}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def find_pipelines_by_dataflow(self, workspace_id: str, dataflow_id_or_name: str, max_workers: int = 5) -> Dict:
|
|
134
|
+
"""
|
|
135
|
+
Finds all pipelines in a workspace that reference a specific dataflow.
|
|
136
|
+
|
|
137
|
+
Accepts either a dataflow ID or display name. Lists all pipelines, fetches
|
|
138
|
+
their activities concurrently, and checks which ones contain a RefreshDataflow
|
|
139
|
+
activity targeting the resolved dataflow ID.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
workspace_id (str): The workspace ID to search pipelines in.
|
|
143
|
+
dataflow_id_or_name (str): The dataflow ID or display name to search for.
|
|
144
|
+
max_workers (int): Maximum number of concurrent requests. Defaults to 5.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Dict: 'message' and 'content' (list of dicts with pipeline_id, pipeline_name,
|
|
148
|
+
and activities — the matching activity names within that pipeline).
|
|
149
|
+
"""
|
|
150
|
+
if workspace_id == '':
|
|
151
|
+
return {'message': 'Missing workspace id, please check.', 'content': ''}
|
|
152
|
+
if dataflow_id_or_name == '':
|
|
153
|
+
return {'message': 'Missing dataflow id or name, please check.', 'content': ''}
|
|
154
|
+
|
|
155
|
+
# Resolve dataflow ID
|
|
156
|
+
dataflow_id, dataflow_name = self._resolve_dataflow_id(workspace_id, dataflow_id_or_name)
|
|
157
|
+
if not dataflow_id:
|
|
158
|
+
return {'message': f'Dataflow not found: {dataflow_id_or_name}', 'content': ''}
|
|
159
|
+
|
|
160
|
+
print(f"Resolved dataflow: {dataflow_name} ({dataflow_id})")
|
|
161
|
+
|
|
162
|
+
# List all pipelines
|
|
163
|
+
pipelines_result = self.list_pipelines(workspace_id)
|
|
164
|
+
if pipelines_result.get('message') != 'Success':
|
|
165
|
+
return pipelines_result
|
|
166
|
+
|
|
167
|
+
pipelines = pipelines_result['content']
|
|
168
|
+
print(f"Found {len(pipelines)} pipelines. Scanning for dataflow {dataflow_id}...")
|
|
169
|
+
|
|
170
|
+
def _check_pipeline(p):
|
|
171
|
+
pipeline_id = p.get('id', '')
|
|
172
|
+
pipeline_name = p.get('displayName', '')
|
|
173
|
+
|
|
174
|
+
activities_result = self.get_pipeline_activities(workspace_id, pipeline_id)
|
|
175
|
+
if activities_result.get('message') != 'Success':
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
matching_activities = []
|
|
179
|
+
for activity in activities_result['content']:
|
|
180
|
+
if activity['activity_type'] != 'RefreshDataflow':
|
|
181
|
+
continue
|
|
182
|
+
props = activity.get('typeProperties', {})
|
|
183
|
+
if props.get('dataflowId') == dataflow_id:
|
|
184
|
+
matching_activities.append(activity['activity_name'])
|
|
185
|
+
|
|
186
|
+
if matching_activities:
|
|
187
|
+
return {
|
|
188
|
+
'pipeline_id': pipeline_id,
|
|
189
|
+
'pipeline_name': pipeline_name,
|
|
190
|
+
'activities': matching_activities
|
|
191
|
+
}
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
matches = []
|
|
195
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
196
|
+
futures = {executor.submit(_check_pipeline, p): p for p in pipelines}
|
|
197
|
+
for future in as_completed(futures):
|
|
198
|
+
result = future.result()
|
|
199
|
+
if result:
|
|
200
|
+
matches.append(result)
|
|
201
|
+
|
|
202
|
+
matches.sort(key=lambda m: m['pipeline_name'].lower())
|
|
203
|
+
print(f"Found {len(matches)} pipeline(s) referencing dataflow {dataflow_id}.")
|
|
204
|
+
return {'message': 'Success', 'content': matches}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def update_pipeline_definition(self, workspace_id: str, pipeline_id: str, definition: Dict) -> Dict:
|
|
208
|
+
"""
|
|
209
|
+
Updates an existing Fabric Data Pipeline definition.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
workspace_id (str): The ID of the workspace where the pipeline resides.
|
|
213
|
+
pipeline_id (str): The ID of the pipeline to update.
|
|
214
|
+
definition (Dict): The full pipeline definition (as returned by get_pipeline_definition).
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Dict: 'message' and 'content' with the update result.
|
|
218
|
+
"""
|
|
219
|
+
if workspace_id == '':
|
|
220
|
+
return {'message': 'Missing workspace id, please check.', 'content': ''}
|
|
221
|
+
if pipeline_id == '':
|
|
222
|
+
return {'message': 'Missing pipeline id, please check.', 'content': ''}
|
|
223
|
+
|
|
224
|
+
api_url = f'{self.fabric_api_base_url}/v1/workspaces/{workspace_id}/dataPipelines/{pipeline_id}/updateDefinition'
|
|
225
|
+
|
|
226
|
+
payload = {"definition": definition['definition']}
|
|
227
|
+
|
|
228
|
+
print(f"Updating pipeline {pipeline_id} in workspace {workspace_id}...")
|
|
229
|
+
response = self._request_with_retry('POST', api_url, headers=self.headers, json=payload)
|
|
230
|
+
|
|
231
|
+
if response.status_code in (200, 202):
|
|
232
|
+
content = response.json() if response.content else {'id': pipeline_id}
|
|
233
|
+
print(f"Successfully updated pipeline {pipeline_id}.")
|
|
234
|
+
return {'message': 'Success', 'content': content}
|
|
235
|
+
else:
|
|
236
|
+
error_data = response.json() if response.headers.get('content-type', '').startswith('application/json') else {}
|
|
237
|
+
error_message = error_data.get('message', error_data.get('error', {}).get('message', response.text))
|
|
238
|
+
print(f"Error updating pipeline: {response.status_code} - {error_message}")
|
|
239
|
+
return {'message': {'error': error_message, 'status_code': response.status_code}}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def replace_dataflow_id_in_pipeline(self, workspace_id: str, pipeline_id: str,
|
|
243
|
+
old_dataflow_id: str, new_dataflow_id: str) -> Dict:
|
|
244
|
+
"""
|
|
245
|
+
Replaces a dataflow ID in all RefreshDataflow activities of a pipeline.
|
|
246
|
+
|
|
247
|
+
Fetches the pipeline definition, finds all RefreshDataflow activities that reference
|
|
248
|
+
the old dataflow ID, updates them to point to the new dataflow ID, and saves the
|
|
249
|
+
modified definition back to Fabric.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
workspace_id (str): The workspace ID where the pipeline resides.
|
|
253
|
+
pipeline_id (str): The pipeline ID to update.
|
|
254
|
+
old_dataflow_id (str): The current dataflow ID to replace.
|
|
255
|
+
new_dataflow_id (str): The new dataflow ID to set.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Dict: 'message' and 'content' with the update result, including how many activities were updated.
|
|
259
|
+
"""
|
|
260
|
+
if workspace_id == '':
|
|
261
|
+
return {'message': 'Missing workspace id, please check.', 'content': ''}
|
|
262
|
+
if pipeline_id == '':
|
|
263
|
+
return {'message': 'Missing pipeline id, please check.', 'content': ''}
|
|
264
|
+
|
|
265
|
+
# Fetch definition
|
|
266
|
+
result = self.get_pipeline_definition(workspace_id, pipeline_id)
|
|
267
|
+
if result.get('message') != 'Success':
|
|
268
|
+
return result
|
|
269
|
+
|
|
270
|
+
definition = result['content']
|
|
271
|
+
|
|
272
|
+
# Find and decode pipeline-content.json
|
|
273
|
+
parts = definition.get('definition', {}).get('parts', [])
|
|
274
|
+
content_part = None
|
|
275
|
+
pipeline_content = None
|
|
276
|
+
for part in parts:
|
|
277
|
+
if part.get('path') == 'pipeline-content.json':
|
|
278
|
+
content_part = part
|
|
279
|
+
pipeline_content = json.loads(base64.b64decode(part['payload']).decode('utf-8'))
|
|
280
|
+
break
|
|
281
|
+
|
|
282
|
+
if not pipeline_content:
|
|
283
|
+
return {'message': 'No pipeline-content.json found in definition.', 'content': ''}
|
|
284
|
+
|
|
285
|
+
# Replace dataflow ID in matching activities
|
|
286
|
+
activities = pipeline_content.get('properties', {}).get('activities', [])
|
|
287
|
+
updated_count = 0
|
|
288
|
+
updated_names = []
|
|
289
|
+
|
|
290
|
+
for activity in activities:
|
|
291
|
+
if activity.get('type') != 'RefreshDataflow':
|
|
292
|
+
continue
|
|
293
|
+
props = activity.get('typeProperties', {})
|
|
294
|
+
if props.get('dataflowId') == old_dataflow_id:
|
|
295
|
+
props['dataflowId'] = new_dataflow_id
|
|
296
|
+
updated_count += 1
|
|
297
|
+
updated_names.append(activity.get('name', ''))
|
|
298
|
+
|
|
299
|
+
if updated_count == 0:
|
|
300
|
+
return {
|
|
301
|
+
'message': f'No RefreshDataflow activities found with dataflow ID {old_dataflow_id}.',
|
|
302
|
+
'content': ''
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
# Encode back and update
|
|
306
|
+
content_part['payload'] = base64.b64encode(
|
|
307
|
+
json.dumps(pipeline_content).encode('utf-8')
|
|
308
|
+
).decode('utf-8')
|
|
309
|
+
|
|
310
|
+
print(f"Replacing dataflow ID in {updated_count} activity(ies): {', '.join(updated_names)}")
|
|
311
|
+
update_result = self.update_pipeline_definition(workspace_id, pipeline_id, definition)
|
|
312
|
+
|
|
313
|
+
if update_result.get('message') == 'Success':
|
|
314
|
+
update_result['content'] = {
|
|
315
|
+
'pipeline_id': pipeline_id,
|
|
316
|
+
'activities_updated': updated_count,
|
|
317
|
+
'activity_names': updated_names
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return update_result
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def get_pipeline_definition(self, workspace_id: str, pipeline_id: str) -> Dict:
|
|
324
|
+
"""
|
|
325
|
+
Gets the definition of a Fabric Data Pipeline from a specified workspace.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
workspace_id (str): The ID of the workspace where the pipeline resides.
|
|
329
|
+
pipeline_id (str): The ID of the pipeline to retrieve the definition for.
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
Dict: A dictionary containing the status ('Success' or error) and the pipeline definition content.
|
|
333
|
+
"""
|
|
334
|
+
if workspace_id == '':
|
|
335
|
+
return {'message': 'Missing workspace id, please check.', 'content': ''}
|
|
336
|
+
if pipeline_id == '':
|
|
337
|
+
return {'message': 'Missing pipeline id, please check.', 'content': ''}
|
|
338
|
+
|
|
339
|
+
api_url = f'{self.fabric_api_base_url}/v1/workspaces/{workspace_id}/dataPipelines/{pipeline_id}/getDefinition'
|
|
340
|
+
|
|
341
|
+
# print(f"Extracting definition for pipeline {pipeline_id} from workspace {workspace_id}...")
|
|
342
|
+
response = self._request_with_retry('POST', api_url, headers=self.headers)
|
|
343
|
+
|
|
344
|
+
if response.status_code == 200:
|
|
345
|
+
definition = response.json()
|
|
346
|
+
# print("Successfully extracted pipeline definition.")
|
|
347
|
+
return {'message': 'Success', 'content': definition}
|
|
348
|
+
else:
|
|
349
|
+
error_data = response.json() if response.headers.get('content-type', '').startswith('application/json') else {}
|
|
350
|
+
error_message = error_data.get('message', error_data.get('error', {}).get('message', response.text))
|
|
351
|
+
print(f"Error getting pipeline definition: {response.status_code} - {error_message}")
|
|
352
|
+
return {'message': {'error': error_message, 'status_code': response.status_code}}
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def get_pipeline(self, workspace_id: str, pipeline_id: str) -> Dict:
|
|
356
|
+
"""
|
|
357
|
+
Gets the metadata of a specific pipeline (not its definition).
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
workspace_id (str): The ID of the workspace.
|
|
361
|
+
pipeline_id (str): The ID of the pipeline.
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
Dict: 'message' and 'content' (pipeline dict with id, displayName, description, etc.).
|
|
365
|
+
"""
|
|
366
|
+
if workspace_id == '':
|
|
367
|
+
return {'message': 'Missing workspace id, please check.', 'content': ''}
|
|
368
|
+
if pipeline_id == '':
|
|
369
|
+
return {'message': 'Missing pipeline id, please check.', 'content': ''}
|
|
370
|
+
|
|
371
|
+
api_url = f'{self.fabric_api_base_url}/v1/workspaces/{workspace_id}/dataPipelines/{pipeline_id}'
|
|
372
|
+
response = self._request_with_retry('GET', api_url, headers=self.headers)
|
|
373
|
+
|
|
374
|
+
if response.status_code == 200:
|
|
375
|
+
return {'message': 'Success', 'content': response.json()}
|
|
376
|
+
else:
|
|
377
|
+
error_data = response.json() if response.headers.get('content-type', '').startswith('application/json') else {}
|
|
378
|
+
error_message = error_data.get('message', error_data.get('error', {}).get('message', response.text))
|
|
379
|
+
return {'message': {'error': error_message, 'status_code': response.status_code}}
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def get_pipeline_activities(self, workspace_id: str, pipeline_id_or_name: str, max_workers: int = 5) -> Dict:
|
|
383
|
+
"""
|
|
384
|
+
Gets the list of activities from a Fabric Data Pipeline definition,
|
|
385
|
+
extracting name, type, typeProperties, and the resolved object name
|
|
386
|
+
for supported activity types.
|
|
387
|
+
|
|
388
|
+
Accepts either a pipeline ID or display name. For activities of type
|
|
389
|
+
RefreshDataflow, TridentNotebook, InvokePipeline, or DatasetRefresh,
|
|
390
|
+
the referenced object's display name is resolved using the appropriate class.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
workspace_id (str): The ID of the workspace where the pipeline resides.
|
|
394
|
+
pipeline_id_or_name (str): The pipeline ID or display name.
|
|
395
|
+
max_workers (int): Maximum number of concurrent requests for name resolution. Defaults to 5.
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
Dict: A dictionary with 'message' and 'content' (list of activity dicts with
|
|
399
|
+
pipeline_id, pipeline_name, activity_name, activity_type, typeProperties,
|
|
400
|
+
and object_name inside typeProperties for supported types).
|
|
401
|
+
"""
|
|
402
|
+
if workspace_id == '':
|
|
403
|
+
return {'message': 'Missing workspace id, please check.', 'content': ''}
|
|
404
|
+
if pipeline_id_or_name == '':
|
|
405
|
+
return {'message': 'Missing pipeline id or name, please check.', 'content': ''}
|
|
406
|
+
|
|
407
|
+
# Resolve pipeline ID and name
|
|
408
|
+
pipeline_id, pipeline_name = self._resolve_pipeline(workspace_id, pipeline_id_or_name)
|
|
409
|
+
if not pipeline_id:
|
|
410
|
+
return {'message': f'Pipeline not found: {pipeline_id_or_name}', 'content': ''}
|
|
411
|
+
|
|
412
|
+
result = self.get_pipeline_definition(workspace_id, pipeline_id)
|
|
413
|
+
|
|
414
|
+
if result.get('message') != 'Success':
|
|
415
|
+
return result
|
|
416
|
+
|
|
417
|
+
definition = result['content']
|
|
418
|
+
|
|
419
|
+
# Extract the pipeline-content.json part from the definition
|
|
420
|
+
pipeline_content = None
|
|
421
|
+
parts = definition.get('definition', {}).get('parts', [])
|
|
422
|
+
for part in parts:
|
|
423
|
+
if part.get('path') == 'pipeline-content.json':
|
|
424
|
+
payload = base64.b64decode(part['payload']).decode('utf-8')
|
|
425
|
+
pipeline_content = json.loads(payload)
|
|
426
|
+
break
|
|
427
|
+
|
|
428
|
+
if not pipeline_content:
|
|
429
|
+
return {'message': 'No pipeline-content.json found in definition.', 'content': ''}
|
|
430
|
+
|
|
431
|
+
# Map activity types to their object ID property key
|
|
432
|
+
activity_object_map = {
|
|
433
|
+
'RefreshDataflow': 'dataflowId',
|
|
434
|
+
'TridentNotebook': 'notebookId',
|
|
435
|
+
'InvokePipeline': 'pipelineId',
|
|
436
|
+
'DatasetRefresh': 'datasetId',
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
# Extract activities and collect unique items to resolve, grouped by type
|
|
440
|
+
raw_activities = pipeline_content.get('properties', {}).get('activities', [])
|
|
441
|
+
items_to_resolve = {} # {object_id: (target_workspace_id, activity_type)}
|
|
442
|
+
|
|
443
|
+
for activity in raw_activities:
|
|
444
|
+
activity_type = activity.get('type', '')
|
|
445
|
+
if activity_type in activity_object_map:
|
|
446
|
+
props = activity.get('typeProperties', {})
|
|
447
|
+
id_key = activity_object_map[activity_type]
|
|
448
|
+
object_id = props.get(id_key, '')
|
|
449
|
+
target_ws = props.get('workspaceId', workspace_id)
|
|
450
|
+
if object_id and object_id not in items_to_resolve:
|
|
451
|
+
items_to_resolve[object_id] = (target_ws, activity_type)
|
|
452
|
+
|
|
453
|
+
# Resolve object names concurrently using the appropriate class per type
|
|
454
|
+
resolved_names = {}
|
|
455
|
+
if items_to_resolve:
|
|
456
|
+
dataflow = Dataflow(self.token)
|
|
457
|
+
notebook = Notebook(self.token)
|
|
458
|
+
dataset = Dataset(self.token)
|
|
459
|
+
|
|
460
|
+
def _resolve_name(object_id, target_ws, activity_type):
|
|
461
|
+
if activity_type == 'RefreshDataflow':
|
|
462
|
+
return object_id, dataflow.get_dataflow_name(target_ws, object_id)
|
|
463
|
+
elif activity_type == 'TridentNotebook':
|
|
464
|
+
result = notebook.get_notebook(target_ws, object_id)
|
|
465
|
+
name = result['content'].get('displayName', '') if result.get('message') == 'Success' else ''
|
|
466
|
+
return object_id, name
|
|
467
|
+
elif activity_type == 'InvokePipeline':
|
|
468
|
+
result = self.get_pipeline(target_ws, object_id)
|
|
469
|
+
name = result['content'].get('displayName', '') if result.get('message') == 'Success' else ''
|
|
470
|
+
return object_id, name
|
|
471
|
+
elif activity_type == 'DatasetRefresh':
|
|
472
|
+
return object_id, dataset.get_dataset_name(target_ws, object_id)
|
|
473
|
+
return object_id, ''
|
|
474
|
+
|
|
475
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
476
|
+
futures = {
|
|
477
|
+
executor.submit(_resolve_name, obj_id, ws_id, a_type): obj_id
|
|
478
|
+
for obj_id, (ws_id, a_type) in items_to_resolve.items()
|
|
479
|
+
}
|
|
480
|
+
for future in as_completed(futures):
|
|
481
|
+
obj_id, name = future.result()
|
|
482
|
+
resolved_names[obj_id] = name
|
|
483
|
+
|
|
484
|
+
# Build activity list with object_name inside typeProperties as the first key
|
|
485
|
+
activities = []
|
|
486
|
+
for activity in raw_activities:
|
|
487
|
+
activity_type = activity.get('type', '')
|
|
488
|
+
type_props = activity.get('typeProperties', {})
|
|
489
|
+
|
|
490
|
+
if activity_type in activity_object_map:
|
|
491
|
+
object_id = type_props.get(activity_object_map[activity_type], '')
|
|
492
|
+
object_name = resolved_names.get(object_id, '')
|
|
493
|
+
type_props = {'object_name': object_name, **type_props}
|
|
494
|
+
|
|
495
|
+
entry = {
|
|
496
|
+
'pipeline_id': pipeline_id,
|
|
497
|
+
'pipeline_name': pipeline_name,
|
|
498
|
+
'activity_name': activity.get('name', ''),
|
|
499
|
+
'activity_type': activity_type,
|
|
500
|
+
'typeProperties': type_props,
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
activities.append(entry)
|
|
504
|
+
|
|
505
|
+
return {'message': 'Success', 'content': activities}
|