msdev-kit 0.1.3__tar.gz → 0.1.4__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.
- {msdev_kit-0.1.3 → msdev_kit-0.1.4}/PKG-INFO +2 -14
- {msdev_kit-0.1.3 → msdev_kit-0.1.4}/README.md +1 -13
- {msdev_kit-0.1.3 → msdev_kit-0.1.4}/msdev_kit/__init__.py +1 -1
- {msdev_kit-0.1.3 → msdev_kit-0.1.4}/msdev_kit/fabric/report.py +232 -27
- {msdev_kit-0.1.3 → msdev_kit-0.1.4}/msdev_kit/fabric/workspace.py +37 -0
- {msdev_kit-0.1.3 → msdev_kit-0.1.4}/pyproject.toml +1 -1
- {msdev_kit-0.1.3 → msdev_kit-0.1.4}/msdev_kit/auth.py +0 -0
- {msdev_kit-0.1.3 → msdev_kit-0.1.4}/msdev_kit/fabric/__init__.py +0 -0
- {msdev_kit-0.1.3 → msdev_kit-0.1.4}/msdev_kit/fabric/admin.py +0 -0
- {msdev_kit-0.1.3 → msdev_kit-0.1.4}/msdev_kit/fabric/capacity.py +0 -0
- {msdev_kit-0.1.3 → msdev_kit-0.1.4}/msdev_kit/fabric/database.py +0 -0
- {msdev_kit-0.1.3 → msdev_kit-0.1.4}/msdev_kit/fabric/dataflow.py +0 -0
- {msdev_kit-0.1.3 → msdev_kit-0.1.4}/msdev_kit/fabric/dataset.py +0 -0
- {msdev_kit-0.1.3 → msdev_kit-0.1.4}/msdev_kit/fabric/kql.py +0 -0
- {msdev_kit-0.1.3 → msdev_kit-0.1.4}/msdev_kit/fabric/notebook.py +0 -0
- {msdev_kit-0.1.3 → msdev_kit-0.1.4}/msdev_kit/fabric/operations.py +0 -0
- {msdev_kit-0.1.3 → msdev_kit-0.1.4}/msdev_kit/fabric/pipeline.py +0 -0
- {msdev_kit-0.1.3 → msdev_kit-0.1.4}/msdev_kit/fabric/utilities.py +0 -0
- {msdev_kit-0.1.3 → msdev_kit-0.1.4}/msdev_kit/graph/__init__.py +0 -0
- {msdev_kit-0.1.3 → msdev_kit-0.1.4}/msdev_kit/graph/client.py +0 -0
- {msdev_kit-0.1.3 → msdev_kit-0.1.4}/msdev_kit/sharepoint/__init__.py +0 -0
- {msdev_kit-0.1.3 → msdev_kit-0.1.4}/msdev_kit/sharepoint/client.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: msdev-kit
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Microsoft developer toolkit: Fabric, MS Graph, and SharePoint
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: Bernardo Rufino
|
|
@@ -36,18 +36,6 @@ Microsoft developer toolkit for Python: Fabric/Power BI, MS Graph (Entra), and S
|
|
|
36
36
|
[](https://pypi.org/project/msdev-kit/)
|
|
37
37
|
[](https://opensource.org/licenses/MIT)
|
|
38
38
|
|
|
39
|
-
## Table of Contents
|
|
40
|
-
|
|
41
|
-
- [Installation](#installation)
|
|
42
|
-
- [Quick Start](#quick-start)
|
|
43
|
-
- [Authentication](#authentication)
|
|
44
|
-
- [Fabric & Power BI](#fabric--power-bi)
|
|
45
|
-
- [MS Graph (Entra)](#ms-graph-entra)
|
|
46
|
-
- [SharePoint](#sharepoint)
|
|
47
|
-
- [Limitations](#limitations)
|
|
48
|
-
|
|
49
|
-
---
|
|
50
|
-
|
|
51
39
|
## Installation
|
|
52
40
|
|
|
53
41
|
```shell
|
|
@@ -216,7 +204,7 @@ pages = rpt.list_report_pages(workspace_id, report_id)
|
|
|
216
204
|
| `get_report_metadata(workspace_id, report_id)` | Get metadata for a specific report. |
|
|
217
205
|
| `get_report_name(workspace_id, report_id)` | Get a report's display name. |
|
|
218
206
|
| `list_report_pages(workspace_id, report_id)` | List all pages in a report. |
|
|
219
|
-
| `
|
|
207
|
+
| `get_legacy_report_pages_and_visuals(json_data, workspace_id, report_id)` | Parse a PBIR-Legacy report JSON and extract pages/visuals into a DataFrame. |
|
|
220
208
|
| `get_legacy_report_json(workspace_id, report_id, operations)` | Get and decode the full report definition for PBIR-Legacy reports. |
|
|
221
209
|
| `export_report(workspace_id, report_id, ...)` | Export a report as a `.pbix` file. |
|
|
222
210
|
| `get_report_measures(workspace_id, report_id, operations)` | Extract report-level measures and generate a DAX Query View script. |
|
|
@@ -6,18 +6,6 @@ Microsoft developer toolkit for Python: Fabric/Power BI, MS Graph (Entra), and S
|
|
|
6
6
|
[](https://pypi.org/project/msdev-kit/)
|
|
7
7
|
[](https://opensource.org/licenses/MIT)
|
|
8
8
|
|
|
9
|
-
## Table of Contents
|
|
10
|
-
|
|
11
|
-
- [Installation](#installation)
|
|
12
|
-
- [Quick Start](#quick-start)
|
|
13
|
-
- [Authentication](#authentication)
|
|
14
|
-
- [Fabric & Power BI](#fabric--power-bi)
|
|
15
|
-
- [MS Graph (Entra)](#ms-graph-entra)
|
|
16
|
-
- [SharePoint](#sharepoint)
|
|
17
|
-
- [Limitations](#limitations)
|
|
18
|
-
|
|
19
|
-
---
|
|
20
|
-
|
|
21
9
|
## Installation
|
|
22
10
|
|
|
23
11
|
```shell
|
|
@@ -186,7 +174,7 @@ pages = rpt.list_report_pages(workspace_id, report_id)
|
|
|
186
174
|
| `get_report_metadata(workspace_id, report_id)` | Get metadata for a specific report. |
|
|
187
175
|
| `get_report_name(workspace_id, report_id)` | Get a report's display name. |
|
|
188
176
|
| `list_report_pages(workspace_id, report_id)` | List all pages in a report. |
|
|
189
|
-
| `
|
|
177
|
+
| `get_legacy_report_pages_and_visuals(json_data, workspace_id, report_id)` | Parse a PBIR-Legacy report JSON and extract pages/visuals into a DataFrame. |
|
|
190
178
|
| `get_legacy_report_json(workspace_id, report_id, operations)` | Get and decode the full report definition for PBIR-Legacy reports. |
|
|
191
179
|
| `export_report(workspace_id, report_id, ...)` | Export a report as a `.pbix` file. |
|
|
192
180
|
| `get_report_measures(workspace_id, report_id, operations)` | Extract report-level measures and generate a DAX Query View script. |
|
|
@@ -144,6 +144,22 @@ class Report:
|
|
|
144
144
|
return report_name
|
|
145
145
|
|
|
146
146
|
|
|
147
|
+
@staticmethod
|
|
148
|
+
def _get_nested_value(data: dict, path: List[Any], default: Any = None) -> Any:
|
|
149
|
+
"""Safely traverses a nested dict/list structure."""
|
|
150
|
+
current = data
|
|
151
|
+
for key in path:
|
|
152
|
+
if isinstance(current, dict):
|
|
153
|
+
current = current.get(key)
|
|
154
|
+
elif isinstance(current, list) and isinstance(key, int) and len(current) > key:
|
|
155
|
+
current = current[key]
|
|
156
|
+
else:
|
|
157
|
+
return default
|
|
158
|
+
if current is None:
|
|
159
|
+
return default
|
|
160
|
+
return current
|
|
161
|
+
|
|
162
|
+
|
|
147
163
|
def list_report_pages(
|
|
148
164
|
self,
|
|
149
165
|
workspace_id: str = '',
|
|
@@ -198,7 +214,7 @@ class Report:
|
|
|
198
214
|
return {'message': {'error': error_message, 'content': response}}
|
|
199
215
|
|
|
200
216
|
|
|
201
|
-
def
|
|
217
|
+
def get_legacy_report_pages_and_visuals(
|
|
202
218
|
self,
|
|
203
219
|
json_data: str,
|
|
204
220
|
workspace_id: str,
|
|
@@ -214,21 +230,6 @@ class Report:
|
|
|
214
230
|
Returns:
|
|
215
231
|
Dict: status message and content.
|
|
216
232
|
"""
|
|
217
|
-
def get_nested_value(data: dict, path: List[Any], default: Any = None) -> Any:
|
|
218
|
-
"""Safely traverses a nested dictionary/list structure."""
|
|
219
|
-
current = data
|
|
220
|
-
for key in path:
|
|
221
|
-
if isinstance(current, dict):
|
|
222
|
-
current = current.get(key)
|
|
223
|
-
elif isinstance(current, list) and isinstance(key, int) and len(current) > key:
|
|
224
|
-
current = current[key]
|
|
225
|
-
else:
|
|
226
|
-
return default
|
|
227
|
-
|
|
228
|
-
if current is None:
|
|
229
|
-
return default
|
|
230
|
-
return current
|
|
231
|
-
|
|
232
233
|
# List to hold flat dictionary records for the final DataFrame
|
|
233
234
|
report_records: List[Dict[str, Any]] = []
|
|
234
235
|
|
|
@@ -241,7 +242,7 @@ class Report:
|
|
|
241
242
|
print("Error: Invalid JSON format.")
|
|
242
243
|
return pd.DataFrame()
|
|
243
244
|
|
|
244
|
-
sections =
|
|
245
|
+
sections = self._get_nested_value(data, ['config', 'sections'])
|
|
245
246
|
if not sections:
|
|
246
247
|
sections = data.get('sections', [])
|
|
247
248
|
|
|
@@ -252,7 +253,7 @@ class Report:
|
|
|
252
253
|
|
|
253
254
|
for vc in visual_containers:
|
|
254
255
|
# The 'name' property inside 'config' is the unique Visual ID
|
|
255
|
-
visual_id =
|
|
256
|
+
visual_id = self._get_nested_value(vc, ['config', 'name'], 'No ID')
|
|
256
257
|
visual_type = 'Unknown'
|
|
257
258
|
visual_title = 'No Title'
|
|
258
259
|
vc_config = vc.get('config', {})
|
|
@@ -281,14 +282,12 @@ class Report:
|
|
|
281
282
|
['text', 0, 'properties', 'text', 'expr', 'Literal', 'Value'],
|
|
282
283
|
# Path 4: Text Visual/Button Label (sometimes the second text object)
|
|
283
284
|
['text', 1, 'properties', 'text', 'expr', 'Literal', 'Value'],
|
|
284
|
-
# Path 5: Text Visual/Button Label (sometimes the second text object)
|
|
285
|
-
['text', 1, 'properties', 'text', 'expr', 'Literal', 'Value']
|
|
286
285
|
]
|
|
287
286
|
|
|
288
287
|
# Check on Objects
|
|
289
288
|
found_title = 'No Title'
|
|
290
289
|
for path in title_paths:
|
|
291
|
-
title_value =
|
|
290
|
+
title_value = self._get_nested_value(objects, path)
|
|
292
291
|
|
|
293
292
|
if isinstance(title_value, str) and title_value:
|
|
294
293
|
found_title = title_value.strip("'").replace('\'', "'")
|
|
@@ -296,7 +295,7 @@ class Report:
|
|
|
296
295
|
|
|
297
296
|
# Check on vcObjects
|
|
298
297
|
for path in title_paths:
|
|
299
|
-
title_value =
|
|
298
|
+
title_value = self._get_nested_value(vc_objects, path)
|
|
300
299
|
|
|
301
300
|
if isinstance(title_value, str) and title_value:
|
|
302
301
|
found_title = title_value.strip("'").replace('\'', "'")
|
|
@@ -308,8 +307,8 @@ class Report:
|
|
|
308
307
|
title_expression = None
|
|
309
308
|
if visual_title == visual_type:
|
|
310
309
|
# Check for dynamic title expression
|
|
311
|
-
title_obj =
|
|
312
|
-
if title_obj and not
|
|
310
|
+
title_obj = self._get_nested_value(objects, ['title', 0, 'properties', 'text', 'expr'])
|
|
311
|
+
if title_obj and not self._get_nested_value(title_obj, ['Literal', 'Value']):
|
|
313
312
|
# Capture the full expression structure (DAX)
|
|
314
313
|
title_expression = str(title_obj)
|
|
315
314
|
|
|
@@ -335,6 +334,214 @@ class Report:
|
|
|
335
334
|
return df
|
|
336
335
|
|
|
337
336
|
|
|
337
|
+
def get_pbir_report_pages_and_visuals(
|
|
338
|
+
self,
|
|
339
|
+
parts: List[Dict],
|
|
340
|
+
workspace_id: str,
|
|
341
|
+
report_id: str) -> pd.DataFrame:
|
|
342
|
+
"""
|
|
343
|
+
Parses a PBIR report definition's parts array to extract pages and visual
|
|
344
|
+
details, and returns the result as a Pandas DataFrame.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
parts (list): the 'parts' list from the Fabric API definition response
|
|
348
|
+
(report_content['definition']['parts']).
|
|
349
|
+
workspace_id (str): workspace id.
|
|
350
|
+
report_id (str): report id.
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
pd.DataFrame: one row per visual with columns report_id, pageIndex,
|
|
354
|
+
pageName, visual_id, type, title, title_expression.
|
|
355
|
+
"""
|
|
356
|
+
title_paths = [
|
|
357
|
+
['title', 0, 'properties', 'text', 'expr', 'Literal', 'Value'],
|
|
358
|
+
['general', 0, 'properties', 'title', 'expr', 'Literal', 'Value'],
|
|
359
|
+
['text', 0, 'properties', 'text', 'expr', 'Literal', 'Value'],
|
|
360
|
+
['text', 1, 'properties', 'text', 'expr', 'Literal', 'Value'],
|
|
361
|
+
]
|
|
362
|
+
|
|
363
|
+
# Index parts by path for O(1) lookup
|
|
364
|
+
parts_by_path = {p['path']: p for p in parts}
|
|
365
|
+
|
|
366
|
+
# Decode pages.json to get ordered page IDs
|
|
367
|
+
pages_meta_part = parts_by_path.get('definition/pages/pages.json')
|
|
368
|
+
if not pages_meta_part:
|
|
369
|
+
return pd.DataFrame()
|
|
370
|
+
|
|
371
|
+
pages_meta = json.loads(base64.b64decode(pages_meta_part['payload']).decode('utf-8'))
|
|
372
|
+
page_order = pages_meta.get('pageOrder', [])
|
|
373
|
+
|
|
374
|
+
report_records: List[Dict[str, Any]] = []
|
|
375
|
+
|
|
376
|
+
for page_index, page_id in enumerate(page_order, start=1):
|
|
377
|
+
page_part = parts_by_path.get(f'definition/pages/{page_id}/page.json')
|
|
378
|
+
if not page_part:
|
|
379
|
+
continue
|
|
380
|
+
page_data = json.loads(base64.b64decode(page_part['payload']).decode('utf-8'))
|
|
381
|
+
page_name = page_data.get('displayName', page_id)
|
|
382
|
+
|
|
383
|
+
prefix = f'definition/pages/{page_id}/visuals/'
|
|
384
|
+
visual_parts = [
|
|
385
|
+
p for p in parts
|
|
386
|
+
if p['path'].startswith(prefix) and p['path'].endswith('/visual.json')
|
|
387
|
+
]
|
|
388
|
+
|
|
389
|
+
for vp in visual_parts:
|
|
390
|
+
vc = json.loads(base64.b64decode(vp['payload']).decode('utf-8'))
|
|
391
|
+
visual_id = vc.get('name', 'No ID')
|
|
392
|
+
visual_type = 'Unknown'
|
|
393
|
+
visual_title = 'No Title'
|
|
394
|
+
title_expression = None
|
|
395
|
+
|
|
396
|
+
visual_group = vc.get('visualGroup')
|
|
397
|
+
if visual_group:
|
|
398
|
+
visual_type = 'Visual Group (Container)'
|
|
399
|
+
visual_title = visual_group.get('displayName', 'Visual Group')
|
|
400
|
+
else:
|
|
401
|
+
visual_data = vc.get('visual', {})
|
|
402
|
+
visual_type = visual_data.get('visualType', 'Generic Visual')
|
|
403
|
+
objects = visual_data.get('objects', {})
|
|
404
|
+
|
|
405
|
+
found_title = 'No Title'
|
|
406
|
+
for path in title_paths:
|
|
407
|
+
title_value = self._get_nested_value(objects, path)
|
|
408
|
+
if isinstance(title_value, str) and title_value:
|
|
409
|
+
found_title = title_value.strip("'").replace('\'', "'")
|
|
410
|
+
break
|
|
411
|
+
|
|
412
|
+
visual_title = found_title if found_title != 'No Title' else visual_type
|
|
413
|
+
|
|
414
|
+
if visual_title == visual_type:
|
|
415
|
+
title_obj = self._get_nested_value(
|
|
416
|
+
objects, ['title', 0, 'properties', 'text', 'expr']
|
|
417
|
+
)
|
|
418
|
+
if title_obj and not self._get_nested_value(title_obj, ['Literal', 'Value']):
|
|
419
|
+
title_expression = str(title_obj)
|
|
420
|
+
|
|
421
|
+
report_records.append({
|
|
422
|
+
'report_id': report_id,
|
|
423
|
+
'pageIndex': page_index,
|
|
424
|
+
'pageName': page_name,
|
|
425
|
+
'visual_id': visual_id,
|
|
426
|
+
'type': visual_type,
|
|
427
|
+
'title': visual_title,
|
|
428
|
+
'title_expression': title_expression,
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
report_name = (
|
|
432
|
+
self.get_report_name(workspace_id, report_id)
|
|
433
|
+
.replace(' ', '').replace('(', '').replace(')', '').strip()
|
|
434
|
+
)
|
|
435
|
+
df = pd.DataFrame(report_records)
|
|
436
|
+
df.sort_values(by=['pageIndex', 'title'], inplace=True)
|
|
437
|
+
df.to_excel(f'{self.data_dir}/pages_and_visuals/{report_name}.xlsx', index=False)
|
|
438
|
+
return df
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def get_report_pages_and_visuals(
|
|
442
|
+
self,
|
|
443
|
+
report_definition: Dict,
|
|
444
|
+
workspace_id: str,
|
|
445
|
+
report_id: str) -> pd.DataFrame:
|
|
446
|
+
"""
|
|
447
|
+
Dispatcher. Detects the report format and delegates to the appropriate
|
|
448
|
+
pages-and-visuals parser.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
report_definition (dict): dict with 'format' and 'parts' keys, as
|
|
452
|
+
returned by report_content['definition'] from the Fabric API.
|
|
453
|
+
workspace_id (str): workspace id.
|
|
454
|
+
report_id (str): report id.
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
pd.DataFrame: one row per visual. Empty DataFrame for unknown formats.
|
|
458
|
+
"""
|
|
459
|
+
fmt = report_definition.get('format', '').lower()
|
|
460
|
+
parts = report_definition.get('parts', [])
|
|
461
|
+
|
|
462
|
+
if fmt == 'pbir':
|
|
463
|
+
return self.get_pbir_report_pages_and_visuals(parts, workspace_id, report_id)
|
|
464
|
+
|
|
465
|
+
elif fmt == 'pbir-legacy':
|
|
466
|
+
report_payload = None
|
|
467
|
+
for part in parts:
|
|
468
|
+
if part.get('path') == 'report.json':
|
|
469
|
+
report_payload = part.get('payload')
|
|
470
|
+
break
|
|
471
|
+
|
|
472
|
+
if not report_payload:
|
|
473
|
+
print('get_report_pages_and_visuals: report.json not found in PBIR-Legacy parts.')
|
|
474
|
+
return pd.DataFrame()
|
|
475
|
+
|
|
476
|
+
decoded = base64.b64decode(report_payload).decode('utf-8')
|
|
477
|
+
json_data = json.loads(decoded)
|
|
478
|
+
|
|
479
|
+
if isinstance(json_data.get('config'), str):
|
|
480
|
+
json_data['config'] = json.loads(json_data['config'])
|
|
481
|
+
|
|
482
|
+
return self.get_legacy_report_pages_and_visuals(json_data, workspace_id, report_id)
|
|
483
|
+
|
|
484
|
+
else:
|
|
485
|
+
print(f'get_report_pages_and_visuals: unsupported format "{fmt}".')
|
|
486
|
+
return pd.DataFrame()
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def get_report_definition(
|
|
490
|
+
self,
|
|
491
|
+
workspace_id: str = '',
|
|
492
|
+
report_id: str = '',
|
|
493
|
+
operations: Operations = None) -> Dict:
|
|
494
|
+
"""
|
|
495
|
+
Fetch the raw report definition for any format (PBIR or PBIR-Legacy).
|
|
496
|
+
|
|
497
|
+
Returns the definition dict as-is from the Fabric API, without any
|
|
498
|
+
format-specific decoding. Pass the returned 'content' directly to
|
|
499
|
+
get_report_pages_and_visuals.
|
|
500
|
+
|
|
501
|
+
Args:
|
|
502
|
+
workspace_id (str): workspace id where the report is.
|
|
503
|
+
report_id (str): report id.
|
|
504
|
+
operations (Operations): Operations class instance.
|
|
505
|
+
|
|
506
|
+
Returns:
|
|
507
|
+
Dict: status message and content. On success, content is a dict
|
|
508
|
+
with 'format' and 'parts' keys.
|
|
509
|
+
"""
|
|
510
|
+
if not workspace_id:
|
|
511
|
+
return {'message': 'Missing workspace id, please check.', 'content': ''}
|
|
512
|
+
if not report_id:
|
|
513
|
+
return {'message': 'Missing report id, please check.', 'content': ''}
|
|
514
|
+
|
|
515
|
+
request_url = f'{self.main_fabric_url}/workspaces/{workspace_id}/reports/{report_id}/getDefinition'
|
|
516
|
+
r = requests.post(url=request_url, headers=self.headers)
|
|
517
|
+
status = r.status_code
|
|
518
|
+
response = json.loads(r.content)
|
|
519
|
+
print(f'status_code={status}')
|
|
520
|
+
|
|
521
|
+
if status != 202:
|
|
522
|
+
error_message = response.get('error', {}).get('message', 'Unknown error')
|
|
523
|
+
return {'message': {'error': error_message}, 'content': response}
|
|
524
|
+
|
|
525
|
+
operation_id = r.headers.get('x-ms-operation-id')
|
|
526
|
+
print(f'operation_id={operation_id}')
|
|
527
|
+
|
|
528
|
+
while True:
|
|
529
|
+
active_operation_state = operations.get_operation_state(operation_id)
|
|
530
|
+
print('Operation state:', active_operation_state)
|
|
531
|
+
if active_operation_state:
|
|
532
|
+
operation_state = active_operation_state.get('operation_state', '')
|
|
533
|
+
if operation_state in ('Succeeded', 'Failed'):
|
|
534
|
+
sleep(1)
|
|
535
|
+
break
|
|
536
|
+
sleep(1)
|
|
537
|
+
|
|
538
|
+
report_content = operations.get_operation_result(operation_id).get('content', '')
|
|
539
|
+
report_definition = report_content.get('definition')
|
|
540
|
+
print(f'Report format: {report_definition.get("format")}')
|
|
541
|
+
|
|
542
|
+
return {'message': 'Success', 'content': report_definition}
|
|
543
|
+
|
|
544
|
+
|
|
338
545
|
def get_legacy_report_json(
|
|
339
546
|
self,
|
|
340
547
|
workspace_id: str = '',
|
|
@@ -1007,6 +1214,4 @@ class Report:
|
|
|
1007
1214
|
'users_migrated': users_migrated,
|
|
1008
1215
|
'users_failed': users_failed
|
|
1009
1216
|
}
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
|
|
1217
|
+
}
|
|
@@ -135,6 +135,43 @@ class Workspace:
|
|
|
135
135
|
return {'message': {'error': error_message, 'content': response}}
|
|
136
136
|
|
|
137
137
|
|
|
138
|
+
def list_workspaces(self, admin: bool = False, top: int = 5000) -> Dict:
|
|
139
|
+
"""
|
|
140
|
+
List all workspaces the authenticated SPN has access to.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
admin (bool, optional): use the admin API to list all tenant workspaces.
|
|
144
|
+
Requires tenant-level admin or read-all permissions. Defaults to False.
|
|
145
|
+
top (int, optional): max results per page (admin only). Defaults to 5000.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Dict: status message and content.
|
|
149
|
+
"""
|
|
150
|
+
if admin:
|
|
151
|
+
all_workspaces = []
|
|
152
|
+
request_url = self.main_url + f'/admin/groups?$top={top}'
|
|
153
|
+
while request_url:
|
|
154
|
+
r = requests.get(url=request_url, headers=self.headers)
|
|
155
|
+
if r.status_code != 200:
|
|
156
|
+
body = json.loads(r.content)
|
|
157
|
+
return {'message': {'error': body['error']['message'], 'content': body}}
|
|
158
|
+
body = json.loads(r.content)
|
|
159
|
+
all_workspaces.extend(body.get('value', []))
|
|
160
|
+
request_url = body.get('odata.nextLink', None)
|
|
161
|
+
response = all_workspaces
|
|
162
|
+
else:
|
|
163
|
+
r = requests.get(url=self.main_url + '/groups', headers=self.headers)
|
|
164
|
+
if r.status_code != 200:
|
|
165
|
+
body = json.loads(r.content)
|
|
166
|
+
return {'message': {'error': body['error']['message'], 'content': body}}
|
|
167
|
+
response = json.loads(r.content).get('value', [])
|
|
168
|
+
|
|
169
|
+
df = pd.DataFrame(response)
|
|
170
|
+
df.to_excel(f'{self.workspace_dir}/workspaces_all.xlsx', index=False)
|
|
171
|
+
|
|
172
|
+
return {'message': 'Success', 'content': response}
|
|
173
|
+
|
|
174
|
+
|
|
138
175
|
def get_workspace_details(
|
|
139
176
|
self,
|
|
140
177
|
workspace_id: str = '') -> Dict:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|