msdev-kit 0.1.0__py3-none-any.whl
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/__init__.py +3 -0
- msdev_kit/auth.py +35 -0
- msdev_kit/fabric/__init__.py +11 -0
- msdev_kit/fabric/admin.py +42 -0
- msdev_kit/fabric/capacity.py +186 -0
- msdev_kit/fabric/database.py +94 -0
- msdev_kit/fabric/dataflow.py +1801 -0
- msdev_kit/fabric/dataset.py +617 -0
- msdev_kit/fabric/kql.py +66 -0
- msdev_kit/fabric/notebook.py +88 -0
- msdev_kit/fabric/operations.py +108 -0
- msdev_kit/fabric/pipeline.py +505 -0
- msdev_kit/fabric/report.py +1012 -0
- msdev_kit/fabric/utilities.py +12 -0
- msdev_kit/fabric/workspace.py +516 -0
- msdev_kit/graph/__init__.py +1 -0
- msdev_kit/graph/client.py +91 -0
- msdev_kit/sharepoint/__init__.py +1 -0
- msdev_kit/sharepoint/client.py +105 -0
- msdev_kit-0.1.0.dist-info/METADATA +269 -0
- msdev_kit-0.1.0.dist-info/RECORD +22 -0
- msdev_kit-0.1.0.dist-info/WHEEL +4 -0
msdev_kit/__init__.py
ADDED
msdev_kit/auth.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from azure.identity import ClientSecretCredential, InteractiveBrowserCredential, TokenCachePersistenceOptions
|
|
2
|
+
|
|
3
|
+
_SCOPES = {
|
|
4
|
+
'pbi': 'https://analysis.windows.net/powerbi/api/.default',
|
|
5
|
+
'fabric': 'https://api.fabric.microsoft.com/.default',
|
|
6
|
+
'azure': 'https://management.azure.com/.default',
|
|
7
|
+
'graph': 'https://graph.microsoft.com/.default',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Auth:
|
|
12
|
+
|
|
13
|
+
def __init__(self, tenant_id: str, client_id: str, client_secret: str):
|
|
14
|
+
self.tenant_id = tenant_id
|
|
15
|
+
self.client_id = client_id
|
|
16
|
+
self.client_secret = client_secret
|
|
17
|
+
self._credential = ClientSecretCredential(
|
|
18
|
+
authority='https://login.microsoftonline.com/',
|
|
19
|
+
tenant_id=tenant_id,
|
|
20
|
+
client_id=client_id,
|
|
21
|
+
client_secret=client_secret,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def get_token(self, service: str = 'pbi') -> str:
|
|
25
|
+
scope = _SCOPES.get(service)
|
|
26
|
+
if not scope:
|
|
27
|
+
raise ValueError(f"Invalid service specified. Choose one of: {', '.join(_SCOPES)}")
|
|
28
|
+
return self._credential.get_token(scope).token
|
|
29
|
+
|
|
30
|
+
def get_token_for_user(self, service: str = 'pbi') -> str:
|
|
31
|
+
scope = _SCOPES.get(service)
|
|
32
|
+
if not scope:
|
|
33
|
+
raise ValueError(f"Invalid service specified. Choose one of: {', '.join(_SCOPES)}")
|
|
34
|
+
auth = InteractiveBrowserCredential(cache_persistence_options=TokenCachePersistenceOptions())
|
|
35
|
+
return auth.get_token(scope).token
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .workspace import Workspace
|
|
2
|
+
from .dataset import Dataset
|
|
3
|
+
from .report import Report
|
|
4
|
+
from .dataflow import Dataflow
|
|
5
|
+
from .capacity import Capacity
|
|
6
|
+
from .admin import Admin
|
|
7
|
+
from .operations import Operations
|
|
8
|
+
from .kql import KQLDatabase
|
|
9
|
+
from .database import Database
|
|
10
|
+
from .pipeline import Pipeline
|
|
11
|
+
from .notebook import Notebook
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import json
|
|
3
|
+
from typing import Dict
|
|
4
|
+
|
|
5
|
+
class Admin:
|
|
6
|
+
|
|
7
|
+
def __init__(self, token: str):
|
|
8
|
+
"""
|
|
9
|
+
Initialize variables for Power BI Admin API interactions.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
token (str): The bearer token for authorization.
|
|
13
|
+
"""
|
|
14
|
+
self.main_url = 'https://api.powerbi.com/v1.0/myorg/admin'
|
|
15
|
+
self.token = token
|
|
16
|
+
self.headers = {'Authorization': f'Bearer {self.token}'}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_report_users_as_admin(self, report_id: str) -> Dict:
|
|
20
|
+
"""
|
|
21
|
+
Retrieves a list of users with access to a specific report as an administrator.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
report_id (str): The ID of the report to get users for.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Dict: A dictionary containing the status message and content (list of users).
|
|
28
|
+
"""
|
|
29
|
+
request_url = f'{self.main_url}/reports/{report_id}/users'
|
|
30
|
+
|
|
31
|
+
r = requests.get(url=request_url, headers=self.headers)
|
|
32
|
+
|
|
33
|
+
status = r.status_code
|
|
34
|
+
response = json.loads(r.content)
|
|
35
|
+
|
|
36
|
+
if status == 200:
|
|
37
|
+
return {'message': 'Success', 'content': response.get('value', [])}
|
|
38
|
+
else:
|
|
39
|
+
error_message = response.get('error', {}).get('message', 'Unknown error')
|
|
40
|
+
return {'message': {'error': error_message, 'status_code': status}, 'content': ''}
|
|
41
|
+
|
|
42
|
+
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import requests
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from typing import Dict
|
|
6
|
+
from .utilities import create_directory
|
|
7
|
+
from .workspace import Workspace
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Capacity:
|
|
11
|
+
|
|
12
|
+
def __init__(self, pbi_token: str, fabric_token: str = None, azure_token: str = None):
|
|
13
|
+
"""
|
|
14
|
+
Initialize variables.
|
|
15
|
+
"""
|
|
16
|
+
# Power BI Capacity parameters
|
|
17
|
+
self.main_url = 'https://api.powerbi.com/v1.0/myorg'
|
|
18
|
+
|
|
19
|
+
# Fabric Capacity parameters
|
|
20
|
+
self.fabric_api_base_url = 'https://management.azure.com'
|
|
21
|
+
self.azure_subscription_id = None
|
|
22
|
+
self.azure_resource_group = None
|
|
23
|
+
|
|
24
|
+
# General parameters
|
|
25
|
+
self.token = pbi_token
|
|
26
|
+
self.headers = {'Authorization': f'Bearer {self.token}'}
|
|
27
|
+
self.workspace = Workspace(self.token)
|
|
28
|
+
|
|
29
|
+
# Directories
|
|
30
|
+
self.capacities_dir = './data/capacities'
|
|
31
|
+
self.directories = [self.capacities_dir]
|
|
32
|
+
|
|
33
|
+
for dir in self.directories:
|
|
34
|
+
create_directory(dir)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def list_powerbi_capacities(self) -> Dict:
|
|
38
|
+
"""
|
|
39
|
+
List all Power BI capacities that the user has access to.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
None.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Dict: status message and content.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
# Main URL
|
|
49
|
+
request_url = f'{self.main_url}/capacities'
|
|
50
|
+
|
|
51
|
+
filename = f'capacities_powerbi.xlsx'
|
|
52
|
+
|
|
53
|
+
# Make the request
|
|
54
|
+
r = requests.get(url=request_url, headers=self.headers)
|
|
55
|
+
|
|
56
|
+
# Get HTTP status and content
|
|
57
|
+
status = r.status_code
|
|
58
|
+
response = json.loads(r.content).get('value', '')
|
|
59
|
+
|
|
60
|
+
# If success...
|
|
61
|
+
if status == 200:
|
|
62
|
+
if type == 'fabric':
|
|
63
|
+
df = pd.json_normalize(response)
|
|
64
|
+
df['name'] = df['displayName']
|
|
65
|
+
df.drop(columns=['displayName'], inplace=True)
|
|
66
|
+
else:
|
|
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'))
|
|
70
|
+
|
|
71
|
+
return {'message': 'Success', 'content': result}
|
|
72
|
+
|
|
73
|
+
else:
|
|
74
|
+
# If any error happens, return message.
|
|
75
|
+
response = json.loads(r.content)
|
|
76
|
+
error_message = response['error']['message']
|
|
77
|
+
|
|
78
|
+
return {'message': {'error': error_message, 'content': response}}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def list_fabric_capacities(self, azure_subscription_id: str, azure_resource_group: str = None) -> Dict:
|
|
82
|
+
"""
|
|
83
|
+
List all Fabric capacities that the user has access to, to a given subscription.
|
|
84
|
+
|
|
85
|
+
If a resource group is provided, only capacities within that resource group will be listed.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
azure_subscription_id (str): Azure subscription ID.
|
|
89
|
+
azure_resource_group (str, optional): Azure resource group. Defaults to None.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Dict: status message and content.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
# Main URL
|
|
96
|
+
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
|
+
else:
|
|
99
|
+
request_url = f'{self.fabric_api_base_url}/subscriptions/{azure_subscription_id}/providers/Microsoft.Fabric/capacities?api-version=2023-11-01'
|
|
100
|
+
|
|
101
|
+
filename = f'capacities_powerbi.xlsx'
|
|
102
|
+
|
|
103
|
+
# Make the request
|
|
104
|
+
r = requests.get(url=request_url, headers=self.headers)
|
|
105
|
+
|
|
106
|
+
# Get HTTP status and content
|
|
107
|
+
status = r.status_code
|
|
108
|
+
response = json.loads(r.content).get('value', '')
|
|
109
|
+
|
|
110
|
+
# If success...
|
|
111
|
+
if status == 200:
|
|
112
|
+
if type == 'fabric':
|
|
113
|
+
df = pd.json_normalize(response)
|
|
114
|
+
df['name'] = df['displayName']
|
|
115
|
+
df.drop(columns=['displayName'], inplace=True)
|
|
116
|
+
else:
|
|
117
|
+
df = pd.DataFrame(response)
|
|
118
|
+
df.to_excel(f'{self.capacities_dir}/{filename}', index=False)
|
|
119
|
+
result = json.loads(df.to_json(orient='records'))
|
|
120
|
+
|
|
121
|
+
return {'message': 'Success', 'content': result}
|
|
122
|
+
|
|
123
|
+
else:
|
|
124
|
+
# If any error happens, return message.
|
|
125
|
+
response = json.loads(r.content)
|
|
126
|
+
error_message = response['error']['message']
|
|
127
|
+
|
|
128
|
+
return {'message': {'error': error_message, 'content': response}}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def assign_workspace_to_capacity(
|
|
132
|
+
self,
|
|
133
|
+
workspace_id: str = '',
|
|
134
|
+
capacity_id: str = '') -> Dict:
|
|
135
|
+
"""
|
|
136
|
+
Assign a workspace to a specific capacity.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
workspace_id (str): workspace id to add the user to.
|
|
140
|
+
capacity_id (str): capacity id to assign the workspace to.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Dict: status message.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
# If both workspace and capacity were provided...
|
|
147
|
+
if (workspace_id != '') & (capacity_id != ''):
|
|
148
|
+
|
|
149
|
+
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)
|
|
152
|
+
|
|
153
|
+
if current_capacity_id.lower() == capacity_id.lower():
|
|
154
|
+
return {'message': 'Workspace is already assigned to the specified capacity.'}
|
|
155
|
+
|
|
156
|
+
request_url = self.main_url + f'/groups/{workspace_id}/AssignToCapacity'
|
|
157
|
+
|
|
158
|
+
headers = {'Authorization': f'Bearer {self.token}'}
|
|
159
|
+
|
|
160
|
+
# https://learn.microsoft.com/en-us/rest/api/power-bi/capacities/groups-assign-to-capacity
|
|
161
|
+
data = {
|
|
162
|
+
"capacityId": capacity_id
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# Make the request
|
|
166
|
+
r = requests.post(url=request_url, headers=headers, json=data)
|
|
167
|
+
|
|
168
|
+
# Get HTTP status and content
|
|
169
|
+
status = r.status_code
|
|
170
|
+
|
|
171
|
+
# If success...
|
|
172
|
+
if status == 200:
|
|
173
|
+
return {'message': 'Success'}
|
|
174
|
+
|
|
175
|
+
elif status == 401:
|
|
176
|
+
return {'message': 'Unauthorized. Please check your access level. Workspace administration rights are required to perform this action.'}
|
|
177
|
+
|
|
178
|
+
else:
|
|
179
|
+
# If any error happens, return message.
|
|
180
|
+
response = json.loads(r.content)
|
|
181
|
+
error_message = response['error']['code']
|
|
182
|
+
|
|
183
|
+
return {'message': {'error': {'status': status, 'description': ''}, 'content': response.content}}
|
|
184
|
+
|
|
185
|
+
else:
|
|
186
|
+
return {'message': 'Missing parameters, please check.', 'content': ''}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from typing import Literal
|
|
4
|
+
from sqlalchemy import create_engine
|
|
5
|
+
from sqlalchemy.exc import SQLAlchemyError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Database:
|
|
9
|
+
|
|
10
|
+
def __init__(self, server: str, database: str, client_id: str, client_secret: str):
|
|
11
|
+
self.server = server
|
|
12
|
+
self.database = database
|
|
13
|
+
self.client_id = client_id
|
|
14
|
+
self.client_secret = client_secret
|
|
15
|
+
self.data_dir = f'./data/lakehouse/{database}'
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def __create_sqlalchemy_engine(self):
|
|
19
|
+
"""
|
|
20
|
+
Creates a SQLAlchemy engine for a SQL Server database using Active Directory Service Principal authentication.
|
|
21
|
+
"""
|
|
22
|
+
connection_string = (
|
|
23
|
+
f"mssql+pyodbc:///?odbc_connect="
|
|
24
|
+
f"DRIVER={{ODBC Driver 18 for SQL Server}};"
|
|
25
|
+
f"SERVER={self.server};"
|
|
26
|
+
f"DATABASE={self.database};"
|
|
27
|
+
f"Authentication=ActiveDirectoryServicePrincipal;"
|
|
28
|
+
f"UID={self.client_id};"
|
|
29
|
+
f"PWD={self.client_secret}"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
# Create the SQLAlchemy engine
|
|
34
|
+
engine = create_engine(connection_string)
|
|
35
|
+
return engine
|
|
36
|
+
except SQLAlchemyError as e:
|
|
37
|
+
print("Error while creating the SQLAlchemy engine:", e)
|
|
38
|
+
raise
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def execute_query(self, query: str):
|
|
42
|
+
|
|
43
|
+
# Create the SQLAlchemy engine
|
|
44
|
+
engine = self.__create_sqlalchemy_engine()
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
# Use a 'with' block to ensure the connection is closed
|
|
48
|
+
with engine.connect() as connection:
|
|
49
|
+
df = pd.read_sql(query, connection)
|
|
50
|
+
|
|
51
|
+
current_timestamp = pd.Timestamp.now().strftime('%Y%m%d_%H%M%S')
|
|
52
|
+
|
|
53
|
+
os.makedirs(f'{self.data_dir}/{self.database}', exist_ok=True)
|
|
54
|
+
df.to_csv(f'{self.data_dir}/{self.database}/query_result_{current_timestamp}.csv', index=False, encoding='utf-8-sig')
|
|
55
|
+
list_of_dicts = df.to_dict(orient='records')
|
|
56
|
+
|
|
57
|
+
return {'message': 'Success', 'content': {'rows': list_of_dicts}}
|
|
58
|
+
|
|
59
|
+
except (SQLAlchemyError, ConnectionError) as e:
|
|
60
|
+
return {'message': 'Error', 'content': str(e)}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def write_dataframe(
|
|
64
|
+
self,
|
|
65
|
+
df: pd.DataFrame,
|
|
66
|
+
table_name: str,
|
|
67
|
+
schema: str = "dbo",
|
|
68
|
+
if_exists: Literal["fail", "replace", "append", "delete_rows"] = "append",
|
|
69
|
+
chunksize: int = 10000
|
|
70
|
+
):
|
|
71
|
+
if df.empty:
|
|
72
|
+
return {'message': 'Skipped', 'content': 'DataFrame is empty'}
|
|
73
|
+
|
|
74
|
+
engine = self.__create_sqlalchemy_engine()
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
with engine.begin() as connection:
|
|
78
|
+
df.to_sql(
|
|
79
|
+
name=table_name,
|
|
80
|
+
con=connection,
|
|
81
|
+
schema=schema,
|
|
82
|
+
if_exists=if_exists,
|
|
83
|
+
index=False,
|
|
84
|
+
chunksize=chunksize,
|
|
85
|
+
method=None # required for fast_executemany
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
'message': 'Success',
|
|
90
|
+
'content': f'{len(df)} rows written to {schema}.{table_name}'
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
except SQLAlchemyError as e:
|
|
94
|
+
return {'message': 'Error', 'content': str(e)}
|