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.
- {msdev_kit-0.1.8 → msdev_kit-0.2.0}/PKG-INFO +8 -6
- {msdev_kit-0.1.8 → msdev_kit-0.2.0}/README.md +1 -1
- msdev_kit-0.2.0/msdev_kit/__init__.py +4 -0
- msdev_kit-0.2.0/msdev_kit/auth.py +52 -0
- {msdev_kit-0.1.8 → msdev_kit-0.2.0}/msdev_kit/fabric/__init__.py +14 -0
- {msdev_kit-0.1.8 → msdev_kit-0.2.0}/msdev_kit/fabric/capacity.py +63 -58
- {msdev_kit-0.1.8 → msdev_kit-0.2.0}/msdev_kit/fabric/dataflow.py +2 -3
- {msdev_kit-0.1.8 → msdev_kit-0.2.0}/msdev_kit/fabric/dataset.py +218 -182
- msdev_kit-0.2.0/msdev_kit/fabric/pipeline.py +601 -0
- {msdev_kit-0.1.8 → msdev_kit-0.2.0}/msdev_kit/fabric/report.py +487 -412
- {msdev_kit-0.1.8 → msdev_kit-0.2.0}/msdev_kit/fabric/workspace.py +202 -180
- {msdev_kit-0.1.8 → msdev_kit-0.2.0}/msdev_kit/graph/__init__.py +2 -0
- {msdev_kit-0.1.8 → msdev_kit-0.2.0}/msdev_kit/sharepoint/__init__.py +2 -0
- msdev_kit-0.2.0/msdev_kit/sharepoint/client.py +140 -0
- {msdev_kit-0.1.8 → msdev_kit-0.2.0}/pyproject.toml +13 -5
- msdev_kit-0.1.8/msdev_kit/__init__.py +0 -3
- msdev_kit-0.1.8/msdev_kit/auth.py +0 -35
- msdev_kit-0.1.8/msdev_kit/fabric/pipeline.py +0 -505
- msdev_kit-0.1.8/msdev_kit/sharepoint/client.py +0 -105
- {msdev_kit-0.1.8 → msdev_kit-0.2.0}/LICENSE +0 -0
- {msdev_kit-0.1.8 → msdev_kit-0.2.0}/msdev_kit/fabric/admin.py +0 -0
- {msdev_kit-0.1.8 → msdev_kit-0.2.0}/msdev_kit/fabric/database.py +0 -0
- {msdev_kit-0.1.8 → msdev_kit-0.2.0}/msdev_kit/fabric/kql.py +0 -0
- {msdev_kit-0.1.8 → msdev_kit-0.2.0}/msdev_kit/fabric/notebook.py +0 -0
- {msdev_kit-0.1.8 → msdev_kit-0.2.0}/msdev_kit/fabric/operations.py +0 -0
- {msdev_kit-0.1.8 → msdev_kit-0.2.0}/msdev_kit/fabric/utilities.py +0 -0
- {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.
|
|
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.
|
|
18
|
-
Requires-Dist: azure-kusto-data (>=6.0.
|
|
19
|
-
Requires-Dist: cryptography (>=
|
|
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.
|
|
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
|
|
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
|
|
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,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__(
|
|
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 =
|
|
18
|
+
self.main_url = "https://api.powerbi.com/v1.0/myorg"
|
|
18
19
|
|
|
19
20
|
# Fabric Capacity parameters
|
|
20
|
-
self.fabric_api_base_url =
|
|
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 = {
|
|
27
|
+
self.headers = {"Authorization": f"Bearer {self.token}"}
|
|
27
28
|
self.workspace = Workspace(self.token)
|
|
28
29
|
|
|
29
30
|
# Directories
|
|
30
|
-
self.capacities_dir =
|
|
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
|
|
49
|
+
request_url = f"{self.main_url}/capacities"
|
|
50
50
|
|
|
51
|
-
filename =
|
|
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(
|
|
58
|
+
response = json.loads(r.content).get("value", "")
|
|
59
59
|
|
|
60
60
|
# If success...
|
|
61
61
|
if status == 200:
|
|
62
|
-
if type ==
|
|
62
|
+
if type == "fabric": # noqa: E721
|
|
63
63
|
df = pd.json_normalize(response)
|
|
64
|
-
df[
|
|
65
|
-
df.drop(columns=[
|
|
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
|
|
69
|
-
result = json.loads(df.to_json(orient=
|
|
68
|
+
df.to_excel(f"{self.capacities_dir}/{filename}", index=False)
|
|
69
|
+
result = json.loads(df.to_json(orient="records"))
|
|
70
70
|
|
|
71
|
-
return {
|
|
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[
|
|
76
|
+
error_message = response["error"]["message"]
|
|
77
77
|
|
|
78
|
-
return {
|
|
78
|
+
return {"message": {"error": error_message, "content": response}}
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
109
|
+
response = json.loads(r.content).get("value", "")
|
|
109
110
|
|
|
110
111
|
# If success...
|
|
111
112
|
if status == 200:
|
|
112
|
-
if type ==
|
|
113
|
+
if type == "fabric": # noqa: E721
|
|
113
114
|
df = pd.json_normalize(response)
|
|
114
|
-
df[
|
|
115
|
-
df.drop(columns=[
|
|
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
|
|
119
|
-
result = json.loads(df.to_json(orient=
|
|
119
|
+
df.to_excel(f"{self.capacities_dir}/{filename}", index=False)
|
|
120
|
+
result = json.loads(df.to_json(orient="records"))
|
|
120
121
|
|
|
121
|
-
return {
|
|
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[
|
|
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
|
-
|
|
133
|
-
|
|
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 !=
|
|
146
|
+
if (workspace_id != "") & (capacity_id != ""):
|
|
148
147
|
|
|
149
148
|
ws = self.workspace.get_worspace_details(workspace_id)
|
|
150
|
-
|
|
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 {
|
|
152
|
+
return {
|
|
153
|
+
"message": "Workspace is already assigned to the specified capacity."
|
|
154
|
+
}
|
|
155
155
|
|
|
156
|
-
request_url = self.main_url + f
|
|
156
|
+
request_url = self.main_url + f"/groups/{workspace_id}/AssignToCapacity"
|
|
157
157
|
|
|
158
|
-
headers = {
|
|
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 {
|
|
174
|
-
|
|
171
|
+
return {"message": "Success"}
|
|
172
|
+
|
|
175
173
|
elif status == 401:
|
|
176
|
-
return {
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
181
|
+
error_code = response.get("error", {}).get("code", "Unknown error")
|
|
182
182
|
|
|
183
|
-
return {
|
|
183
|
+
return {
|
|
184
|
+
"message": {
|
|
185
|
+
"error": {"status": status, "description": error_code},
|
|
186
|
+
"content": response,
|
|
187
|
+
}
|
|
188
|
+
}
|
|
184
189
|
|
|
185
190
|
else:
|
|
186
|
-
return {
|
|
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:
|