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.
Files changed (22) hide show
  1. {msdev_kit-0.1.2 → msdev_kit-0.1.4}/PKG-INFO +8 -14
  2. {msdev_kit-0.1.2 → msdev_kit-0.1.4}/README.md +7 -13
  3. {msdev_kit-0.1.2 → msdev_kit-0.1.4}/msdev_kit/__init__.py +1 -1
  4. {msdev_kit-0.1.2 → msdev_kit-0.1.4}/msdev_kit/fabric/report.py +232 -27
  5. {msdev_kit-0.1.2 → msdev_kit-0.1.4}/msdev_kit/fabric/workspace.py +37 -0
  6. {msdev_kit-0.1.2 → msdev_kit-0.1.4}/pyproject.toml +1 -1
  7. {msdev_kit-0.1.2 → msdev_kit-0.1.4}/msdev_kit/auth.py +0 -0
  8. {msdev_kit-0.1.2 → msdev_kit-0.1.4}/msdev_kit/fabric/__init__.py +0 -0
  9. {msdev_kit-0.1.2 → msdev_kit-0.1.4}/msdev_kit/fabric/admin.py +0 -0
  10. {msdev_kit-0.1.2 → msdev_kit-0.1.4}/msdev_kit/fabric/capacity.py +0 -0
  11. {msdev_kit-0.1.2 → msdev_kit-0.1.4}/msdev_kit/fabric/database.py +0 -0
  12. {msdev_kit-0.1.2 → msdev_kit-0.1.4}/msdev_kit/fabric/dataflow.py +0 -0
  13. {msdev_kit-0.1.2 → msdev_kit-0.1.4}/msdev_kit/fabric/dataset.py +0 -0
  14. {msdev_kit-0.1.2 → msdev_kit-0.1.4}/msdev_kit/fabric/kql.py +0 -0
  15. {msdev_kit-0.1.2 → msdev_kit-0.1.4}/msdev_kit/fabric/notebook.py +0 -0
  16. {msdev_kit-0.1.2 → msdev_kit-0.1.4}/msdev_kit/fabric/operations.py +0 -0
  17. {msdev_kit-0.1.2 → msdev_kit-0.1.4}/msdev_kit/fabric/pipeline.py +0 -0
  18. {msdev_kit-0.1.2 → msdev_kit-0.1.4}/msdev_kit/fabric/utilities.py +0 -0
  19. {msdev_kit-0.1.2 → msdev_kit-0.1.4}/msdev_kit/graph/__init__.py +0 -0
  20. {msdev_kit-0.1.2 → msdev_kit-0.1.4}/msdev_kit/graph/client.py +0 -0
  21. {msdev_kit-0.1.2 → msdev_kit-0.1.4}/msdev_kit/sharepoint/__init__.py +0 -0
  22. {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.2
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
  [![Python](https://img.shields.io/pypi/pyversions/msdev-kit)](https://pypi.org/project/msdev-kit/)
37
37
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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](https://github.com/Bernardo-Rufino/msdev-kit#workspace) — workspaces, users, permissions
149
- - [Dataset](https://github.com/Bernardo-Rufino/msdev-kit#dataset) — semantic models, DAX queries, permissions
150
- - [Report](https://github.com/Bernardo-Rufino/msdev-kit#report) — metadata, definitions, visuals, measures
151
- - [Dataflow](https://github.com/Bernardo-Rufino/msdev-kit#dataflow) — Gen1, Gen2, Gen2 CI/CD management
152
- - [Pipeline](https://github.com/Bernardo-Rufino/msdev-kit#pipeline) — Data Pipeline management
153
- - [Other modules](https://github.com/Bernardo-Rufino/msdev-kit#other-modules) — Capacity, Admin, KQL, Notebook, Database
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
- | `get_report_json_pages_and_visuals(json_data, workspace_id, report_id)` | Parse a PBIR-Legacy report JSON and extract pages/visuals into a DataFrame. |
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
  [![Python](https://img.shields.io/pypi/pyversions/msdev-kit)](https://pypi.org/project/msdev-kit/)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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](https://github.com/Bernardo-Rufino/msdev-kit#workspace) — workspaces, users, permissions
119
- - [Dataset](https://github.com/Bernardo-Rufino/msdev-kit#dataset) — semantic models, DAX queries, permissions
120
- - [Report](https://github.com/Bernardo-Rufino/msdev-kit#report) — metadata, definitions, visuals, measures
121
- - [Dataflow](https://github.com/Bernardo-Rufino/msdev-kit#dataflow) — Gen1, Gen2, Gen2 CI/CD management
122
- - [Pipeline](https://github.com/Bernardo-Rufino/msdev-kit#pipeline) — Data Pipeline management
123
- - [Other modules](https://github.com/Bernardo-Rufino/msdev-kit#other-modules) — Capacity, Admin, KQL, Notebook, Database
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
- | `get_report_json_pages_and_visuals(json_data, workspace_id, report_id)` | Parse a PBIR-Legacy report JSON and extract pages/visuals into a DataFrame. |
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. |
@@ -1,3 +1,3 @@
1
1
  from .auth import Auth
2
2
 
3
- __version__ = "0.1.2"
3
+ __version__ = "0.1.4"
@@ -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 get_report_json_pages_and_visuals(
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 = get_nested_value(data, ['config', '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 = get_nested_value(vc, ['config', 'name'], 'No 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 = get_nested_value(objects, path)
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 = get_nested_value(vc_objects, path)
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 = get_nested_value(objects, ['title', 0, 'properties', 'text', 'expr'])
312
- if title_obj and not get_nested_value(title_obj, ['Literal', 'Value']):
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:
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "msdev-kit"
3
3
  description = "Microsoft developer toolkit: Fabric, MS Graph, and SharePoint"
4
- version = "0.1.2"
4
+ version = "0.1.4"
5
5
  requires-python = ">=3.10"
6
6
  readme = "README.md"
7
7
  license = {text = "MIT"}
File without changes