msdev-kit 0.1.8__tar.gz → 0.2.0__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 (27) hide show
  1. {msdev_kit-0.1.8 → msdev_kit-0.2.0}/PKG-INFO +8 -6
  2. {msdev_kit-0.1.8 → msdev_kit-0.2.0}/README.md +1 -1
  3. msdev_kit-0.2.0/msdev_kit/__init__.py +4 -0
  4. msdev_kit-0.2.0/msdev_kit/auth.py +52 -0
  5. {msdev_kit-0.1.8 → msdev_kit-0.2.0}/msdev_kit/fabric/__init__.py +14 -0
  6. {msdev_kit-0.1.8 → msdev_kit-0.2.0}/msdev_kit/fabric/capacity.py +63 -58
  7. {msdev_kit-0.1.8 → msdev_kit-0.2.0}/msdev_kit/fabric/dataflow.py +2 -3
  8. {msdev_kit-0.1.8 → msdev_kit-0.2.0}/msdev_kit/fabric/dataset.py +218 -182
  9. msdev_kit-0.2.0/msdev_kit/fabric/pipeline.py +601 -0
  10. {msdev_kit-0.1.8 → msdev_kit-0.2.0}/msdev_kit/fabric/report.py +487 -412
  11. {msdev_kit-0.1.8 → msdev_kit-0.2.0}/msdev_kit/fabric/workspace.py +202 -180
  12. {msdev_kit-0.1.8 → msdev_kit-0.2.0}/msdev_kit/graph/__init__.py +2 -0
  13. {msdev_kit-0.1.8 → msdev_kit-0.2.0}/msdev_kit/sharepoint/__init__.py +2 -0
  14. msdev_kit-0.2.0/msdev_kit/sharepoint/client.py +140 -0
  15. {msdev_kit-0.1.8 → msdev_kit-0.2.0}/pyproject.toml +13 -5
  16. msdev_kit-0.1.8/msdev_kit/__init__.py +0 -3
  17. msdev_kit-0.1.8/msdev_kit/auth.py +0 -35
  18. msdev_kit-0.1.8/msdev_kit/fabric/pipeline.py +0 -505
  19. msdev_kit-0.1.8/msdev_kit/sharepoint/client.py +0 -105
  20. {msdev_kit-0.1.8 → msdev_kit-0.2.0}/LICENSE +0 -0
  21. {msdev_kit-0.1.8 → msdev_kit-0.2.0}/msdev_kit/fabric/admin.py +0 -0
  22. {msdev_kit-0.1.8 → msdev_kit-0.2.0}/msdev_kit/fabric/database.py +0 -0
  23. {msdev_kit-0.1.8 → msdev_kit-0.2.0}/msdev_kit/fabric/kql.py +0 -0
  24. {msdev_kit-0.1.8 → msdev_kit-0.2.0}/msdev_kit/fabric/notebook.py +0 -0
  25. {msdev_kit-0.1.8 → msdev_kit-0.2.0}/msdev_kit/fabric/operations.py +0 -0
  26. {msdev_kit-0.1.8 → msdev_kit-0.2.0}/msdev_kit/fabric/utilities.py +0 -0
  27. {msdev_kit-0.1.8 → msdev_kit-0.2.0}/msdev_kit/graph/client.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: msdev-kit
3
- Version: 0.1.8
3
+ Version: 0.2.0
4
4
  Summary: Microsoft developer toolkit: Fabric, MS Graph, and SharePoint
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -14,17 +14,19 @@ Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Programming Language :: Python :: 3.12
15
15
  Classifier: Programming Language :: Python :: 3.13
16
16
  Classifier: Programming Language :: Python :: 3.14
17
- Requires-Dist: azure-identity (>=1.24.0,<2)
18
- Requires-Dist: azure-kusto-data (>=6.0.1,<7.0.0)
19
- Requires-Dist: cryptography (>=46.0.6,<47.0.0)
17
+ Requires-Dist: azure-identity (>=1.25.3,<2)
18
+ Requires-Dist: azure-kusto-data (>=6.0.3,<7.0.0)
19
+ Requires-Dist: cryptography (>=48.0.1,<49.0.0)
20
+ Requires-Dist: ipykernel (>=7.3.0,<8.0.0)
20
21
  Requires-Dist: numpy (==2.1.3)
21
22
  Requires-Dist: openpyxl (==3.1.5)
22
23
  Requires-Dist: pandas (==2.2.3)
23
24
  Requires-Dist: pydantic (>=2.13.4,<3.0.0)
25
+ Requires-Dist: pyjwt (>=2.13.0,<3.0.0)
24
26
  Requires-Dist: pyodbc (==5.2.0)
25
27
  Requires-Dist: python-dotenv (>=1.2.2,<2)
26
28
  Requires-Dist: pyyaml (>=6.0.3,<7.0.0)
27
- Requires-Dist: requests (>=2.33.0)
29
+ Requires-Dist: requests (>=2.34.2)
28
30
  Requires-Dist: sqlalchemy (==2.0.44)
29
31
  Project-URL: Homepage, https://github.com/Bernardo-Rufino/msdev-kit
30
32
  Project-URL: Repository, https://github.com/Bernardo-Rufino/msdev-kit
@@ -374,7 +376,7 @@ sp.create_folder('/Reports/2026/Q1')
374
376
  | Method | Description |
375
377
  |---|---|
376
378
  | `download_file(file_path, local_dir)` | Download a file from the default document library. Returns local file path. |
377
- | `upload_file(remote_path, source, content_type?)` | Upload/overwrite a file. `source` is a local file path (str) or raw bytes. |
379
+ | `upload_file(remote_path, source, content_type?)` | Upload/overwrite a file up to 250 MB. `source` is a local file path or raw bytes. |
378
380
  | `create_folder(folder_path)` | Create a folder and all intermediate folders. |
379
381
 
380
382
  Hostname and site path inputs are normalized automatically:
@@ -342,7 +342,7 @@ sp.create_folder('/Reports/2026/Q1')
342
342
  | Method | Description |
343
343
  |---|---|
344
344
  | `download_file(file_path, local_dir)` | Download a file from the default document library. Returns local file path. |
345
- | `upload_file(remote_path, source, content_type?)` | Upload/overwrite a file. `source` is a local file path (str) or raw bytes. |
345
+ | `upload_file(remote_path, source, content_type?)` | Upload/overwrite a file up to 250 MB. `source` is a local file path or raw bytes. |
346
346
  | `create_folder(folder_path)` | Create a folder and all intermediate folders. |
347
347
 
348
348
  Hostname and site path inputs are normalized automatically:
@@ -0,0 +1,4 @@
1
+ from .auth import Auth
2
+
3
+ __version__ = "0.2.0"
4
+ __all__ = ["Auth"]
@@ -0,0 +1,52 @@
1
+ from azure.identity import (
2
+ ClientSecretCredential,
3
+ InteractiveBrowserCredential,
4
+ TokenCachePersistenceOptions,
5
+ )
6
+
7
+ _SCOPES = {
8
+ "pbi": "https://analysis.windows.net/powerbi/api/.default",
9
+ "fabric": "https://api.fabric.microsoft.com/.default",
10
+ "azure": "https://management.azure.com/.default",
11
+ "graph": "https://graph.microsoft.com/.default",
12
+ }
13
+
14
+
15
+ class Auth:
16
+ def __init__(
17
+ self, tenant_id: str = "", client_id: str = "", client_secret: str = ""
18
+ ):
19
+ self.tenant_id = tenant_id
20
+ self.client_id = client_id
21
+ self.client_secret = client_secret
22
+ self._credential = (
23
+ ClientSecretCredential(
24
+ authority="https://login.microsoftonline.com/",
25
+ tenant_id=tenant_id,
26
+ client_id=client_id,
27
+ client_secret=client_secret,
28
+ )
29
+ if all((tenant_id, client_id, client_secret))
30
+ else InteractiveBrowserCredential(
31
+ cache_persistence_options=TokenCachePersistenceOptions()
32
+ )
33
+ )
34
+
35
+ def get_token(self, service: str = "pbi") -> str:
36
+ scope = _SCOPES.get(service)
37
+ if not scope:
38
+ raise ValueError(
39
+ f"Invalid service specified. Choose one of: {', '.join(_SCOPES)}"
40
+ )
41
+ return self._credential.get_token(scope).token
42
+
43
+ def get_token_for_user(self, service: str = "pbi") -> str:
44
+ scope = _SCOPES.get(service)
45
+ if not scope:
46
+ raise ValueError(
47
+ f"Invalid service specified. Choose one of: {', '.join(_SCOPES)}"
48
+ )
49
+ credential = InteractiveBrowserCredential(
50
+ cache_persistence_options=TokenCachePersistenceOptions()
51
+ )
52
+ return credential.get_token(scope).token
@@ -9,3 +9,17 @@ from .kql import KQLDatabase
9
9
  from .database import Database
10
10
  from .pipeline import Pipeline
11
11
  from .notebook import Notebook
12
+
13
+ __all__ = [
14
+ "Workspace",
15
+ "Dataset",
16
+ "Report",
17
+ "Dataflow",
18
+ "Capacity",
19
+ "Admin",
20
+ "Operations",
21
+ "KQLDatabase",
22
+ "Database",
23
+ "Pipeline",
24
+ "Notebook",
25
+ ]
@@ -1,4 +1,3 @@
1
- import os
2
1
  import json
3
2
  import requests
4
3
  import pandas as pd
@@ -9,31 +8,32 @@ from .workspace import Workspace
9
8
 
10
9
  class Capacity:
11
10
 
12
- def __init__(self, pbi_token: str, fabric_token: str = None, azure_token: str = None):
11
+ def __init__(
12
+ self, pbi_token: str, fabric_token: str = None, azure_token: str = None
13
+ ):
13
14
  """
14
15
  Initialize variables.
15
16
  """
16
17
  # Power BI Capacity parameters
17
- self.main_url = 'https://api.powerbi.com/v1.0/myorg'
18
+ self.main_url = "https://api.powerbi.com/v1.0/myorg"
18
19
 
19
20
  # Fabric Capacity parameters
20
- self.fabric_api_base_url = 'https://management.azure.com'
21
+ self.fabric_api_base_url = "https://management.azure.com"
21
22
  self.azure_subscription_id = None
22
23
  self.azure_resource_group = None
23
24
 
24
25
  # General parameters
25
26
  self.token = pbi_token
26
- self.headers = {'Authorization': f'Bearer {self.token}'}
27
+ self.headers = {"Authorization": f"Bearer {self.token}"}
27
28
  self.workspace = Workspace(self.token)
28
29
 
29
30
  # Directories
30
- self.capacities_dir = './data/capacities'
31
+ self.capacities_dir = "./data/capacities"
31
32
  self.directories = [self.capacities_dir]
32
33
 
33
34
  for dir in self.directories:
34
35
  create_directory(dir)
35
36
 
36
-
37
37
  def list_powerbi_capacities(self) -> Dict:
38
38
  """
39
39
  List all Power BI capacities that the user has access to.
@@ -44,44 +44,45 @@ class Capacity:
44
44
  Returns:
45
45
  Dict: status message and content.
46
46
  """
47
-
47
+
48
48
  # Main URL
49
- request_url = f'{self.main_url}/capacities'
49
+ request_url = f"{self.main_url}/capacities"
50
50
 
51
- filename = f'capacities_powerbi.xlsx'
51
+ filename = "capacities_powerbi.xlsx"
52
52
 
53
53
  # Make the request
54
54
  r = requests.get(url=request_url, headers=self.headers)
55
55
 
56
56
  # Get HTTP status and content
57
57
  status = r.status_code
58
- response = json.loads(r.content).get('value', '')
58
+ response = json.loads(r.content).get("value", "")
59
59
 
60
60
  # If success...
61
61
  if status == 200:
62
- if type == 'fabric':
62
+ if type == "fabric": # noqa: E721
63
63
  df = pd.json_normalize(response)
64
- df['name'] = df['displayName']
65
- df.drop(columns=['displayName'], inplace=True)
64
+ df["name"] = df["displayName"]
65
+ df.drop(columns=["displayName"], inplace=True)
66
66
  else:
67
67
  df = pd.DataFrame(response)
68
- df.to_excel(f'{self.capacities_dir}/{filename}', index=False)
69
- result = json.loads(df.to_json(orient='records'))
68
+ df.to_excel(f"{self.capacities_dir}/{filename}", index=False)
69
+ result = json.loads(df.to_json(orient="records"))
70
70
 
71
- return {'message': 'Success', 'content': result}
71
+ return {"message": "Success", "content": result}
72
72
 
73
- else:
73
+ else:
74
74
  # If any error happens, return message.
75
75
  response = json.loads(r.content)
76
- error_message = response['error']['message']
76
+ error_message = response["error"]["message"]
77
77
 
78
- return {'message': {'error': error_message, 'content': response}}
78
+ return {"message": {"error": error_message, "content": response}}
79
79
 
80
-
81
- def list_fabric_capacities(self, azure_subscription_id: str, azure_resource_group: str = None) -> Dict:
80
+ def list_fabric_capacities(
81
+ self, azure_subscription_id: str, azure_resource_group: str = None
82
+ ) -> Dict:
82
83
  """
83
84
  List all Fabric capacities that the user has access to, to a given subscription.
84
-
85
+
85
86
  If a resource group is provided, only capacities within that resource group will be listed.
86
87
 
87
88
  Args:
@@ -91,47 +92,45 @@ class Capacity:
91
92
  Returns:
92
93
  Dict: status message and content.
93
94
  """
94
-
95
+
95
96
  # Main URL
96
97
  if azure_resource_group:
97
- request_url = f'{self.fabric_api_base_url}/subscriptions/{azure_subscription_id}/resourceGroups/{azure_resource_group}/providers/Microsoft.Fabric/capacities?api-version=2023-11-01'
98
+ request_url = f"{self.fabric_api_base_url}/subscriptions/{azure_subscription_id}/resourceGroups/{azure_resource_group}/providers/Microsoft.Fabric/capacities?api-version=2023-11-01"
98
99
  else:
99
- request_url = f'{self.fabric_api_base_url}/subscriptions/{azure_subscription_id}/providers/Microsoft.Fabric/capacities?api-version=2023-11-01'
100
+ request_url = f"{self.fabric_api_base_url}/subscriptions/{azure_subscription_id}/providers/Microsoft.Fabric/capacities?api-version=2023-11-01"
100
101
 
101
- filename = f'capacities_powerbi.xlsx'
102
+ filename = "capacities_powerbi.xlsx"
102
103
 
103
104
  # Make the request
104
105
  r = requests.get(url=request_url, headers=self.headers)
105
106
 
106
107
  # Get HTTP status and content
107
108
  status = r.status_code
108
- response = json.loads(r.content).get('value', '')
109
+ response = json.loads(r.content).get("value", "")
109
110
 
110
111
  # If success...
111
112
  if status == 200:
112
- if type == 'fabric':
113
+ if type == "fabric": # noqa: E721
113
114
  df = pd.json_normalize(response)
114
- df['name'] = df['displayName']
115
- df.drop(columns=['displayName'], inplace=True)
115
+ df["name"] = df["displayName"]
116
+ df.drop(columns=["displayName"], inplace=True)
116
117
  else:
117
118
  df = pd.DataFrame(response)
118
- df.to_excel(f'{self.capacities_dir}/{filename}', index=False)
119
- result = json.loads(df.to_json(orient='records'))
119
+ df.to_excel(f"{self.capacities_dir}/{filename}", index=False)
120
+ result = json.loads(df.to_json(orient="records"))
120
121
 
121
- return {'message': 'Success', 'content': result}
122
+ return {"message": "Success", "content": result}
122
123
 
123
- else:
124
+ else:
124
125
  # If any error happens, return message.
125
126
  response = json.loads(r.content)
126
- error_message = response['error']['message']
127
-
128
- return {'message': {'error': error_message, 'content': response}}
127
+ error_message = response["error"]["message"]
129
128
 
129
+ return {"message": {"error": error_message, "content": response}}
130
130
 
131
131
  def assign_workspace_to_capacity(
132
- self,
133
- workspace_id: str = '',
134
- capacity_id: str = '') -> Dict:
132
+ self, workspace_id: str = "", capacity_id: str = ""
133
+ ) -> Dict:
135
134
  """
136
135
  Assign a workspace to a specific capacity.
137
136
 
@@ -144,23 +143,22 @@ class Capacity:
144
143
  """
145
144
 
146
145
  # If both workspace and capacity were provided...
147
- if (workspace_id != '') & (capacity_id != ''):
146
+ if (workspace_id != "") & (capacity_id != ""):
148
147
 
149
148
  ws = self.workspace.get_worspace_details(workspace_id)
150
- workspace_name = ws.get('content', {}).get('name', None)
151
- current_capacity_id = ws.get('content', {}).get('capacityId', None)
149
+ current_capacity_id = ws.get("content", {}).get("capacityId", None)
152
150
 
153
151
  if current_capacity_id.lower() == capacity_id.lower():
154
- return {'message': 'Workspace is already assigned to the specified capacity.'}
152
+ return {
153
+ "message": "Workspace is already assigned to the specified capacity."
154
+ }
155
155
 
156
- request_url = self.main_url + f'/groups/{workspace_id}/AssignToCapacity'
156
+ request_url = self.main_url + f"/groups/{workspace_id}/AssignToCapacity"
157
157
 
158
- headers = {'Authorization': f'Bearer {self.token}'}
158
+ headers = {"Authorization": f"Bearer {self.token}"}
159
159
 
160
160
  # https://learn.microsoft.com/en-us/rest/api/power-bi/capacities/groups-assign-to-capacity
161
- data = {
162
- "capacityId": capacity_id
163
- }
161
+ data = {"capacityId": capacity_id}
164
162
 
165
163
  # Make the request
166
164
  r = requests.post(url=request_url, headers=headers, json=data)
@@ -170,17 +168,24 @@ class Capacity:
170
168
 
171
169
  # If success...
172
170
  if status == 200:
173
- return {'message': 'Success'}
174
-
171
+ return {"message": "Success"}
172
+
175
173
  elif status == 401:
176
- return {'message': 'Unauthorized. Please check your access level. Workspace administration rights are required to perform this action.'}
177
-
178
- else:
174
+ return {
175
+ "message": "Unauthorized. Please check your access level. Workspace administration rights are required to perform this action."
176
+ }
177
+
178
+ else:
179
179
  # If any error happens, return message.
180
180
  response = json.loads(r.content)
181
- error_message = response['error']['code']
181
+ error_code = response.get("error", {}).get("code", "Unknown error")
182
182
 
183
- return {'message': {'error': {'status': status, 'description': ''}, 'content': response.content}}
183
+ return {
184
+ "message": {
185
+ "error": {"status": status, "description": error_code},
186
+ "content": response,
187
+ }
188
+ }
184
189
 
185
190
  else:
186
- return {'message': 'Missing parameters, please check.', 'content': ''}
191
+ return {"message": "Missing parameters, please check.", "content": ""}
@@ -274,7 +274,7 @@ class Dataflow:
274
274
  response = json.loads(r.content)
275
275
  error_message = response['error']
276
276
 
277
- except:
277
+ except Exception:
278
278
  return {'message': 'Error reading JSON response'}
279
279
 
280
280
  return {'message': {'error': error_message, 'content': response}}
@@ -337,7 +337,7 @@ class Dataflow:
337
337
  response = json.loads(r.content)
338
338
  error_message = response['error']
339
339
 
340
- except:
340
+ except Exception:
341
341
  return {'message': 'Error reading JSON response'}
342
342
 
343
343
  return {'message': {'error': error_message, 'content': response}}
@@ -926,7 +926,6 @@ class Dataflow:
926
926
  return {'message': 'Could not parse mashup.pq.', 'content': ''}
927
927
 
928
928
  data_queries = parsed['data_queries']
929
- source_queries = parsed['source_queries']
930
929
  dest_queries = parsed['dest_queries']
931
930
 
932
931
  if not data_queries: