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 ADDED
@@ -0,0 +1,3 @@
1
+ from .auth import Auth
2
+
3
+ __version__ = "0.1.0"
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)}