msdev-kit 0.1.2__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.2 → msdev_kit-0.1.4}/PKG-INFO +8 -14
- {msdev_kit-0.1.2 → msdev_kit-0.1.4}/README.md +7 -13
- {msdev_kit-0.1.2 → msdev_kit-0.1.4}/msdev_kit/__init__.py +1 -1
- {msdev_kit-0.1.2 → msdev_kit-0.1.4}/msdev_kit/fabric/report.py +232 -27
- {msdev_kit-0.1.2 → msdev_kit-0.1.4}/msdev_kit/fabric/workspace.py +37 -0
- {msdev_kit-0.1.2 → msdev_kit-0.1.4}/pyproject.toml +1 -1
- {msdev_kit-0.1.2 → msdev_kit-0.1.4}/msdev_kit/auth.py +0 -0
- {msdev_kit-0.1.2 → msdev_kit-0.1.4}/msdev_kit/fabric/__init__.py +0 -0
- {msdev_kit-0.1.2 → msdev_kit-0.1.4}/msdev_kit/fabric/admin.py +0 -0
- {msdev_kit-0.1.2 → msdev_kit-0.1.4}/msdev_kit/fabric/capacity.py +0 -0
- {msdev_kit-0.1.2 → msdev_kit-0.1.4}/msdev_kit/fabric/database.py +0 -0
- {msdev_kit-0.1.2 → msdev_kit-0.1.4}/msdev_kit/fabric/dataflow.py +0 -0
- {msdev_kit-0.1.2 → msdev_kit-0.1.4}/msdev_kit/fabric/dataset.py +0 -0
- {msdev_kit-0.1.2 → msdev_kit-0.1.4}/msdev_kit/fabric/kql.py +0 -0
- {msdev_kit-0.1.2 → msdev_kit-0.1.4}/msdev_kit/fabric/notebook.py +0 -0
- {msdev_kit-0.1.2 → msdev_kit-0.1.4}/msdev_kit/fabric/operations.py +0 -0
- {msdev_kit-0.1.2 → msdev_kit-0.1.4}/msdev_kit/fabric/pipeline.py +0 -0
- {msdev_kit-0.1.2 → msdev_kit-0.1.4}/msdev_kit/fabric/utilities.py +0 -0
- {msdev_kit-0.1.2 → msdev_kit-0.1.4}/msdev_kit/graph/__init__.py +0 -0
- {msdev_kit-0.1.2 → msdev_kit-0.1.4}/msdev_kit/graph/client.py +0 -0
- {msdev_kit-0.1.2 → msdev_kit-0.1.4}/msdev_kit/sharepoint/__init__.py +0 -0
- {msdev_kit-0.1.2 → 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,12 +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](https://github.com/Bernardo-Rufino/msdev-kit#installation) · [Quick Start](https://github.com/Bernardo-Rufino/msdev-kit#quick-start) · [Authentication](https://github.com/Bernardo-Rufino/msdev-kit#authentication) · [Fabric & Power BI](https://github.com/Bernardo-Rufino/msdev-kit#fabric--power-bi) · [MS Graph (Entra)](https://github.com/Bernardo-Rufino/msdev-kit#ms-graph-entra) · [SharePoint](https://github.com/Bernardo-Rufino/msdev-kit#sharepoint) · [Limitations](https://github.com/Bernardo-Rufino/msdev-kit#limitations)
|
|
42
|
-
|
|
43
|
-
---
|
|
44
|
-
|
|
45
39
|
## Installation
|
|
46
40
|
|
|
47
41
|
```shell
|
|
@@ -145,12 +139,12 @@ auth = Auth(tenant_id, client_id, client_secret)
|
|
|
145
139
|
|
|
146
140
|
Fabric classes take a token string — call `auth.get_token('fabric')` or `auth.get_token('pbi')` depending on the API.
|
|
147
141
|
|
|
148
|
-
- [Workspace](
|
|
149
|
-
- [Dataset](
|
|
150
|
-
- [Report](
|
|
151
|
-
- [Dataflow](
|
|
152
|
-
- [Pipeline](
|
|
153
|
-
- [Other modules](
|
|
142
|
+
- [Workspace](#workspace) — workspaces, users, permissions
|
|
143
|
+
- [Dataset](#dataset) — semantic models, DAX queries, permissions
|
|
144
|
+
- [Report](#report) — metadata, definitions, visuals, measures
|
|
145
|
+
- [Dataflow](#dataflow) — Gen1, Gen2, Gen2 CI/CD management
|
|
146
|
+
- [Pipeline](#pipeline) — Data Pipeline management
|
|
147
|
+
- [Other modules](#other-modules) — Capacity, Admin, KQL, Notebook, Database
|
|
154
148
|
|
|
155
149
|
### Workspace
|
|
156
150
|
|
|
@@ -210,7 +204,7 @@ pages = rpt.list_report_pages(workspace_id, report_id)
|
|
|
210
204
|
| `get_report_metadata(workspace_id, report_id)` | Get metadata for a specific report. |
|
|
211
205
|
| `get_report_name(workspace_id, report_id)` | Get a report's display name. |
|
|
212
206
|
| `list_report_pages(workspace_id, report_id)` | List all pages in a report. |
|
|
213
|
-
| `
|
|
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. |
|
|
214
208
|
| `get_legacy_report_json(workspace_id, report_id, operations)` | Get and decode the full report definition for PBIR-Legacy reports. |
|
|
215
209
|
| `export_report(workspace_id, report_id, ...)` | Export a report as a `.pbix` file. |
|
|
216
210
|
| `get_report_measures(workspace_id, report_id, operations)` | Extract report-level measures and generate a DAX Query View script. |
|
|
@@ -6,12 +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](https://github.com/Bernardo-Rufino/msdev-kit#installation) · [Quick Start](https://github.com/Bernardo-Rufino/msdev-kit#quick-start) · [Authentication](https://github.com/Bernardo-Rufino/msdev-kit#authentication) · [Fabric & Power BI](https://github.com/Bernardo-Rufino/msdev-kit#fabric--power-bi) · [MS Graph (Entra)](https://github.com/Bernardo-Rufino/msdev-kit#ms-graph-entra) · [SharePoint](https://github.com/Bernardo-Rufino/msdev-kit#sharepoint) · [Limitations](https://github.com/Bernardo-Rufino/msdev-kit#limitations)
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
9
|
## Installation
|
|
16
10
|
|
|
17
11
|
```shell
|
|
@@ -115,12 +109,12 @@ auth = Auth(tenant_id, client_id, client_secret)
|
|
|
115
109
|
|
|
116
110
|
Fabric classes take a token string — call `auth.get_token('fabric')` or `auth.get_token('pbi')` depending on the API.
|
|
117
111
|
|
|
118
|
-
- [Workspace](
|
|
119
|
-
- [Dataset](
|
|
120
|
-
- [Report](
|
|
121
|
-
- [Dataflow](
|
|
122
|
-
- [Pipeline](
|
|
123
|
-
- [Other modules](
|
|
112
|
+
- [Workspace](#workspace) — workspaces, users, permissions
|
|
113
|
+
- [Dataset](#dataset) — semantic models, DAX queries, permissions
|
|
114
|
+
- [Report](#report) — metadata, definitions, visuals, measures
|
|
115
|
+
- [Dataflow](#dataflow) — Gen1, Gen2, Gen2 CI/CD management
|
|
116
|
+
- [Pipeline](#pipeline) — Data Pipeline management
|
|
117
|
+
- [Other modules](#other-modules) — Capacity, Admin, KQL, Notebook, Database
|
|
124
118
|
|
|
125
119
|
### Workspace
|
|
126
120
|
|
|
@@ -180,7 +174,7 @@ pages = rpt.list_report_pages(workspace_id, report_id)
|
|
|
180
174
|
| `get_report_metadata(workspace_id, report_id)` | Get metadata for a specific report. |
|
|
181
175
|
| `get_report_name(workspace_id, report_id)` | Get a report's display name. |
|
|
182
176
|
| `list_report_pages(workspace_id, report_id)` | List all pages in a report. |
|
|
183
|
-
| `
|
|
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. |
|
|
184
178
|
| `get_legacy_report_json(workspace_id, report_id, operations)` | Get and decode the full report definition for PBIR-Legacy reports. |
|
|
185
179
|
| `export_report(workspace_id, report_id, ...)` | Export a report as a `.pbix` file. |
|
|
186
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
|