brynq-sdk-azure 1.0.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.
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 1.0
2
+ Name: brynq_sdk_azure
3
+ Version: 1.0.0
4
+ Summary: Azure wrapper from BrynQ
5
+ Home-page: UNKNOWN
6
+ Author: BrynQ
7
+ Author-email: support@brynq.com
8
+ License: BrynQ License
9
+ Description: Azure wrapper from BrynQ
10
+ Platform: UNKNOWN
@@ -0,0 +1,3 @@
1
+ from brynq_sdk.azure.entra import Entra
2
+ from brynq_sdk.azure.azure_connection import AzureConnection
3
+ from brynq_sdk.azure.blob_storage import BlobStorage
@@ -0,0 +1,7 @@
1
+ from msal import ConfidentialClientApplication
2
+
3
+
4
+ class AzureAuthentication():
5
+ def __init__(self):
6
+ pass
7
+
@@ -0,0 +1,104 @@
1
+ """
2
+ See how-to on our confluence page for more details.
3
+ Use Azure python sdk to sync the files with Azure Files Share service.
4
+ The config file shall have a settings in a format like:
5
+ azure_config = {
6
+ 'azure_connection_string' : r'{the azure connection string}'
7
+ 'share_name' : "/sharename/",
8
+ 'parent_dir_path' : r"volume/data/" : ALAWAYS start with a test file, to make sure you don't mess up the other folders/files
9
+ }
10
+ """
11
+
12
+ import os
13
+ import sys
14
+ from azure.storage.fileshare import ShareClient
15
+
16
+ basedir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
17
+ sys.path.append(basedir)
18
+
19
+
20
+ class AzureConnection:
21
+ """
22
+ connection_string: the connection_string which functions as a token for the Azure connection
23
+ share_name: the share name in Azure
24
+ """
25
+ def __init__(self):
26
+ # This ugly fix is needed so the other packages can run without a config.py file
27
+ import config
28
+ self.connection_string = config.azure_config['azure_connection_string']
29
+ self.share_name = config.azure_config['share_name']
30
+ self.share_client = ShareClient.from_connection_string(self.connection_string, share_name=self.share_name)
31
+
32
+ def list_files_and_dirs(self, dir_client):
33
+ """
34
+ List all the files and folders under this directory_path.
35
+ :param dir_client: The connection to a specified directory
36
+ :return: two lists consist of files and subfolders separately
37
+ """
38
+ my_list = list(dir_client.list_directories_and_files())
39
+ subdir_list = [x['name'] for x in my_list if x['is_directory'] is True]
40
+ file_list = [x['name'] for x in my_list if x['is_directory'] is False]
41
+ return file_list, subdir_list
42
+
43
+ def create_directory(self, dir_path):
44
+ """
45
+ Create a ShareDirectoryClient from a connection string
46
+ :param dir_path: The directory_path
47
+ :return: a share_client which connects with the specified directory,
48
+ and a Flag to indicate if this directory exists before
49
+ """
50
+ dir_client = self.share_client.get_directory_client(directory_path=dir_path)
51
+ dir_already_existed = False
52
+ try:
53
+ dir_client.create_directory()
54
+ except:
55
+ dir_already_existed = True
56
+ return dir_client, dir_already_existed
57
+
58
+
59
+ def empty_folder(self, dir_client, delete_folder = False):
60
+ """
61
+ To empty a folder including all the subfolders and files within.
62
+ :param dir_client: The share to connect with this directory
63
+ :param delete_folder: To delete the folder as well. If yes then the whole folder will be removed, otherwise only remove the files.
64
+ """
65
+ file_list, subdir_list = self.list_files_and_dirs(dir_client)
66
+ if len(subdir_list)>0:
67
+ for subdir in subdir_list:
68
+ self.empty_folder(dir_client.get_subdirectory_client(subdir), delete_folder=True)
69
+ for file in file_list:
70
+ dir_client.delete_file(file)
71
+ if delete_folder:
72
+ dir_client.delete_directory()
73
+
74
+ def create_subdirectory_and_upload_file(self, parentdir_path, subdir_path, local_file_path, filename):
75
+ """
76
+ Create a subfolder and upload files to this folder
77
+ :param parentdir_path: The parent directory
78
+ :param subdir_path: the subfolder directory
79
+ :param local_file_path: local file directory from which to upload the files
80
+ :param filename: filename to be uploaded
81
+ :return:
82
+ """
83
+ _, _ = self.create_directory(parentdir_path)
84
+ dir_path = os.path.join(parentdir_path,subdir_path)
85
+ subdir, dir_already_existed = self.create_directory(dir_path)
86
+ if dir_already_existed:
87
+ self.empty_folder(subdir, delete_folder=False)
88
+ # Upload a file to the subdirectory
89
+ with open(os.path.join(local_file_path,filename), "rb") as source:
90
+ subdir.upload_file(file_name=filename, data=source)
91
+
92
+ def create_directory_and_upload_file(self, parentdir_path, local_file_path, filename):
93
+ """
94
+ Create a folder and upload files to this folder
95
+ :param parentdir_path: The parent directory
96
+ :param local_file_path: local file directory from which to upload the files
97
+ :param filename: filename to be uploaded
98
+ :return:
99
+ """
100
+ # Get the directory client
101
+ parentdir_dir, _ = self.create_directory(parentdir_path)
102
+ # Upload a file to the subdirectory
103
+ with open(os.path.join(local_file_path, filename), "rb") as source:
104
+ parentdir_dir.upload_file(file_name=filename, data=source)
@@ -0,0 +1,95 @@
1
+ from brynq_sdk.brynq import BrynQ
2
+ from azure.storage.blob import BlobServiceClient, BlobClient, ContainerClient, generate_account_sas, ResourceTypes, AccountSasPermissions
3
+ from typing import Union, List
4
+ from datetime import datetime, timedelta
5
+
6
+
7
+ class BlobStorage(BrynQ):
8
+ def __init__(self, label: Union[str, List]):
9
+ super().__init__()
10
+ self.blob_service_client = self.__get_authentication(label=label)
11
+
12
+ def __get_authentication(self, label):
13
+ credentials = self.get_system_credential(system='azure-blob-storage', label=label)
14
+ storage_account_name = credentials['storage_account_name']
15
+ storage_account_key = credentials['storage_account_key']
16
+ sas_token = generate_account_sas(
17
+ account_name=storage_account_name,
18
+ account_key=storage_account_key,
19
+ resource_types=ResourceTypes(service=True, container=True, object=True),
20
+ permission=AccountSasPermissions(read=True, write=True, list=True, delete=True, add=True, create=True, update=True, process=True),
21
+ expiry=datetime.utcnow() + timedelta(hours=1)
22
+ )
23
+ blob_service_client = BlobServiceClient(
24
+ account_url=f"https://{storage_account_name}.blob.core.windows.net",
25
+ credential=sas_token
26
+ )
27
+
28
+ return blob_service_client
29
+
30
+ def get_containers(self):
31
+ all_containers = self.blob_service_client.list_containers(include_metadata=True)
32
+ container_list = []
33
+ for container in all_containers:
34
+ container_info = {
35
+ 'name': container.name,
36
+ 'last_modified': container.last_modified,
37
+ 'etag': container.etag,
38
+ 'lease_state': container.lease,
39
+ 'has_immutability_policy': container.has_immutability_policy,
40
+ 'has_legal_hold': container.has_legal_hold,
41
+ 'metadata': container.metadata
42
+ }
43
+ container_list.append(container_info)
44
+
45
+ return container_list
46
+
47
+ def get_container(self, container_name: str):
48
+ """
49
+ Get a container from the blob storage
50
+ """
51
+ container = self.blob_service_client.get_container_client(container_name)
52
+ return container
53
+
54
+ def create_container(self, container_name: str):
55
+ """
56
+ Create a container in the blob storage
57
+ """
58
+ response = self.blob_service_client.create_container(container_name)
59
+ return response
60
+
61
+ def update_container(self):
62
+ pass
63
+
64
+ def delete_container(self):
65
+ pass
66
+
67
+ def get_blobs(self):
68
+ pass
69
+
70
+ def create_blob(self):
71
+ pass
72
+
73
+ def delete_blob(self):
74
+ pass
75
+
76
+ def get_folders(self):
77
+ pass
78
+
79
+ def create_folder(self, container_name: str, folder_name: str):
80
+ """
81
+ Create a file with a 0 as content. Because the file is created, the folder is also created. After that the file and the folder are created,
82
+ delete the file so the folder will stay. According to the azure docs, it should be possible to create empty files, but this is not working.
83
+ """
84
+ # Split the url and add the container and folder name in between the url
85
+ original_url = self.blob_service_client.url.split('?')
86
+ url = f"{original_url[0]}/{container_name}/{folder_name}/empty_file?{original_url[1]}"
87
+ blob = BlobClient.from_blob_url(blob_url=url)
88
+
89
+ # Now create the file and delete it so the folder will stay
90
+ response = blob.upload_blob(b"0", blob_type='AppendBlob')
91
+ blob.delete_blob()
92
+ return response
93
+
94
+ def delete_folder(self):
95
+ pass
@@ -0,0 +1,340 @@
1
+ import urllib.parse
2
+ import warnings
3
+ from brynq_sdk.brynq import BrynQ
4
+ import requests
5
+ import random
6
+ import string
7
+ import json
8
+ import pandas as pd
9
+ from pandas import json_normalize
10
+ from msal import ConfidentialClientApplication
11
+ from typing import Union, List
12
+ import os
13
+
14
+
15
+ class Entra(BrynQ):
16
+
17
+ def __init__(self, label: Union[str, List], debug: bool = False):
18
+ super().__init__()
19
+ self.headers = self.__get_headers(label=label)
20
+ self.endpoint = "https://graph.microsoft.com/v1.0"
21
+
22
+ def __get_headers(self, label):
23
+ credentials = self.get_system_credential(system='azure-entra-token', label=label)
24
+ tenant_id = credentials['tenant_id']
25
+ client_id = credentials['client_id']
26
+ client_secret = credentials['client_secret']
27
+ authority = f"https://login.microsoftonline.com/{tenant_id}"
28
+
29
+ # Create a ConfidentialClientApplication for authentication
30
+ app = ConfidentialClientApplication(
31
+ client_id,
32
+ authority=authority,
33
+ client_credential=client_secret,
34
+ )
35
+
36
+ # Get an access token for the Graph API
37
+ result = app.acquire_token_for_client(scopes=["https://graph.microsoft.com/.default"])
38
+ headers = {
39
+ 'Authorization': f"Bearer {result['access_token']}",
40
+ 'Content-Type': 'application/json'
41
+ }
42
+
43
+ return headers
44
+
45
+ def __add_attribute_information(self, payload, custom_attributes):
46
+ # First get the official name of the custom attribute and all the other information
47
+ payload.update({"customSecurityAttributes": {}})
48
+ metadata = requests.get('https://graph.microsoft.com/v1.0/directory/customSecurityAttributeDefinitions', headers=self.headers).json()
49
+ # Now loop through the given metadata and add the corresponding metadata and the values itself to the payload
50
+ for attr, value in custom_attributes.items():
51
+ for meta in metadata["value"]:
52
+ if meta["name"] == attr:
53
+ attr_set = meta["attributeSet"]
54
+ attr_type = meta["type"]
55
+ is_collection = meta["isCollection"]
56
+ if attr_set not in payload["customSecurityAttributes"]:
57
+ payload["customSecurityAttributes"][attr_set] = {"@odata.type": "#microsoft.graph.customSecurityAttributeValue"}
58
+ # In case of an integer, the field type should be given as well
59
+ if attr_type == "Integer":
60
+ if is_collection:
61
+ payload["customSecurityAttributes"][attr_set][f"{attr}@odata.type"] = "#Collection(Int32)"
62
+ else:
63
+ payload["customSecurityAttributes"][attr_set][f"{attr}@odata.type"] = "#Int32"
64
+ payload["customSecurityAttributes"][attr_set][attr] = value
65
+ # In case of a boolean, only the value should be given, the field type itself is not relevant
66
+ elif attr_type == "Boolean":
67
+ payload["customSecurityAttributes"][attr_set][attr] = value
68
+ # In case of a string, the field type should be given if the field is a collection of values. If it's a single value, the field type is not relevant
69
+ else:
70
+ if is_collection:
71
+ payload["customSecurityAttributes"][attr_set][f"{attr}@odata.type"] = "#Collection(String)"
72
+ payload["customSecurityAttributes"][attr_set][attr] = value
73
+ return payload
74
+
75
+ def __generate_password(self):
76
+ special_characters = string.punctuation
77
+ digits = string.digits
78
+ uppercase_letters = string.ascii_uppercase
79
+ lowercase_letters = string.ascii_lowercase
80
+
81
+ # Create a pool of characters
82
+ pool = special_characters + digits + uppercase_letters + lowercase_letters
83
+
84
+ # Ensure at least one character of each type
85
+ password = random.choice(special_characters)
86
+ password += random.choice(digits)
87
+ password += random.choice(uppercase_letters)
88
+ password += random.choice(lowercase_letters)
89
+
90
+ # Fill the remaining length with random characters
91
+ password += ''.join(random.choice(pool) for _ in range(20 - 4))
92
+
93
+ # Shuffle the characters to make the password more random
94
+ password_list = list(password)
95
+ random.shuffle(password_list)
96
+ password = ''.join(password_list)
97
+
98
+ return password
99
+
100
+ def get_groups(self) -> pd.DataFrame:
101
+ """
102
+ Get all groups from Azure Entra
103
+ :return: pd.DataFrame with the groups
104
+ """
105
+ endpoint = "https://graph.microsoft.com/v1.0"
106
+ df = pd.DataFrame()
107
+ loop = True
108
+ url = f"{endpoint}/groups"
109
+ while loop:
110
+ response = requests.get(url, headers=self.headers)
111
+ groups = response.json()['value']
112
+ df_temp = pd.json_normalize(groups)
113
+ df = pd.concat([df, df_temp], ignore_index=True)
114
+ if '@odata.nextLink' in response.json():
115
+ url = response.json()['@odata.nextLink']
116
+ else:
117
+ loop = False
118
+ df = df.reset_index(drop=True)
119
+ return df
120
+
121
+ def get_group_members(self, group_id: str = '') -> pd.DataFrame:
122
+ """
123
+ Get all users from a group in Azure Entra
124
+ :param group_id: ID of the group. If no ID is given, all possible groups will be returned
125
+ :return: pd.DataFrame with the users
126
+ """
127
+ group_url = "https://graph.microsoft.com/v1.0/groups/"
128
+ df = pd.DataFrame()
129
+ while group_url:
130
+ graph_r = requests.get(group_url, headers=self.headers)
131
+ graph_json = graph_r.json()
132
+ groups = graph_json.get('value')
133
+ for group in groups:
134
+ print(f"Group ID: {group['id']}, Group Name: {group['displayName']}")
135
+ # Get users in each group
136
+ next_url_members = f"https://graph.microsoft.com/v1.0/groups/{group['id']}/members"
137
+ while next_url_members:
138
+ members_r = requests.get(next_url_members, headers=self.headers)
139
+ members_json = members_r.json()
140
+ members = members_json.get('value')
141
+ df_temp = pd.json_normalize(members)
142
+ if len(df_temp) > 0:
143
+ df_temp['group_id'] = group['id']
144
+ df_temp['group'] = group['displayName']
145
+ df_temp.rename(columns={'id': 'user_id'}, inplace=True)
146
+ df = pd.concat([df, df_temp], ignore_index=True)
147
+ next_url_members = members_json.get('@odata.nextLink')
148
+ group_url = graph_json.get('@odata.nextLink')
149
+
150
+ df = df.reset_index(drop=True)
151
+ return df
152
+
153
+ def create_group(self, name: str = '', description: str = '', mail_enabled: bool = False, mail_nickname: str = '', security_enabled: bool = True):
154
+ """
155
+ Create a new group in Azure Entra
156
+ :param name: Name of the group
157
+ :param description: Description of the group
158
+ :param mail_enabled: Is the group mail enabled?
159
+ :param mail_nickname: Mail nickname of the group
160
+ :param security_enabled: Is the group security enabled?
161
+ :return: Response of the request
162
+ """
163
+ endpoint = "https://graph.microsoft.com/v1.0/groups"
164
+ payload = {
165
+ "displayName": f"{name}",
166
+ "description": f"{description}",
167
+ "mailEnabled": mail_enabled,
168
+ "mailNickname": f"{mail_nickname}",
169
+ "securityEnabled": security_enabled
170
+ }
171
+ response = requests.post(endpoint, headers=self.headers, json=payload)
172
+ return response
173
+
174
+ def update_group(self, id: int, name: str = '', description: str = '', mail_enabled: bool = False, mail_nickname: str = '', security_enabled: bool = True):
175
+ """
176
+ Create a new group in Azure Entra
177
+ :param id: ID of the group
178
+ :param name: Name of the group
179
+ :param description: Description of the group
180
+ :param mail_enabled: Is the group mail enabled?
181
+ :param mail_nickname: Mail nickname of the group
182
+ :param security_enabled: Is the group security enabled?
183
+ :return: Response of the request
184
+ """
185
+ endpoint = f"https://graph.microsoft.com/v1.0/groups/{id}"
186
+ payload = {
187
+ "displayName": f"{name}",
188
+ "description": f"{description}",
189
+ "mailEnabled": mail_enabled,
190
+ "mailNickname": f"{mail_nickname}",
191
+ "securityEnabled": security_enabled
192
+ }
193
+ response = requests.patch(endpoint, headers=self.headers, json=payload)
194
+ return response
195
+
196
+ def delete_group(self, group_id):
197
+ """
198
+ Delete a group in Azure Entra
199
+ :param group_id: ID of the group
200
+ :return: Response of the request
201
+ """
202
+ endpoint = f"https://graph.microsoft.com/v1.0/groups/{group_id}"
203
+ response = requests.delete(endpoint, headers=self.headers)
204
+ return response
205
+
206
+ def get_users(self, extra_fields: list = [], custom_attributes: bool = False, expand: str = '', expand_select: str = '') -> pd.DataFrame:
207
+ """
208
+ Get all users from Azure Entra
209
+ :param extra_fields: Besided the default fields, you can add extra fields to the request. Put them in a list
210
+ :param custom_attributes: Get the custom attributes of the users. If True, all the custom attributes will be returned
211
+ :return: pd.DataFrame with the users
212
+ """
213
+ fields = ['businessPhones', 'displayName', 'givenName', 'id', 'jobTitle', 'mail', 'mobilePhone',
214
+ 'officeLocation', 'preferredLanguage', 'surname', 'userPrincipalName'] + extra_fields
215
+ fields = ','.join(fields)
216
+ endpoint = f"https://graph.microsoft.com/v1.0/users?$select={fields}"
217
+ if custom_attributes:
218
+ endpoint = f"https://graph.microsoft.com/beta/users?$select={fields},customSecurityAttributes"
219
+ # Adding expand and select parameters if provided
220
+ if expand:
221
+ if expand_select:
222
+ endpoint += f",&$expand={expand}($select={expand_select})"
223
+ else:
224
+ endpoint += f",&$expand={expand}"
225
+
226
+ df = pd.DataFrame()
227
+ while endpoint:
228
+ response = requests.get(endpoint, headers=self.headers)
229
+ endpoint = response.json().get('@odata.nextLink')
230
+ data = response.json().get('value')
231
+ df_temp = json_normalize(data, sep='.')
232
+ df_temp = df_temp.drop([col for col in df_temp.columns if 'odata.type' in col], axis=1)
233
+ df = pd.concat([df, df_temp], ignore_index=True)
234
+ df = df.reset_index(drop=True)
235
+ return df
236
+
237
+ def create_user(self, account_enabled=True, display_name='', mail_nickname='', user_principal_name='', password='', force_change_password_next_sign_in=False, extra_fields={}, custom_attributes={}):
238
+ """
239
+ Create a new user in Azure Entra
240
+ :param account_enabled: Is the account enabled? By default True
241
+ :param display_name: Display name of the user
242
+ :param mail_nickname: Mail nickname of the user (the part before the @)
243
+ :param user_principal_name: User principal name of the user
244
+ :param password: Password of the user. If no password is given, a random password will be generated
245
+ :param force_change_password_next_sign_in: Force the user to change the password on the next sign in. By default False
246
+ :param extra_fields: Extra fields you want to add to the user. Put them in a dictionary
247
+ :param custom_attributes: A dictionary with the name of the custom attribute and the value. It could be multiple custom attributes
248
+ """
249
+ # Custom attributes are only available in the beta version of the API
250
+ endpoint = 'https://graph.microsoft.com/beta/users' if custom_attributes else 'https://graph.microsoft.com/v1.0/users'
251
+ if password == '':
252
+ password = self.__generate_password()
253
+
254
+ payload = {
255
+ "accountEnabled": account_enabled,
256
+ "displayName": f"{display_name}",
257
+ "mailNickname": f"{mail_nickname}",
258
+ "userPrincipalName": user_principal_name,
259
+ "passwordProfile": {
260
+ "forceChangePasswordNextSignIn": force_change_password_next_sign_in,
261
+ "password": f"{password}"
262
+ },
263
+ }
264
+ payload.update(extra_fields)
265
+
266
+ # If there are any custom attributes, add them to the payload. But since the endpoint needs extra metadata, we need to do some extra work
267
+ if len(custom_attributes) > 0:
268
+ payload = self.__add_attribute_information(payload, custom_attributes)
269
+ response = requests.post(endpoint, headers=self.headers, json=payload)
270
+ return response
271
+
272
+ def update_user(self, user_id, fields_to_update: dict = {}, custom_attributes: dict = {}, update_password: bool = False):
273
+ """
274
+ Update a user in Azure Entra
275
+ :param user_id: The Azure AD ID of the user
276
+ :param fields_to_update: A dictionary with the fields you want to update. Don't put the custom attributes in this dictionary
277
+ :param custom_attributes: A dictionary with the name of the custom attribute and the value. It could be multiple custom attributes
278
+ :param update_password: If True, the password will be updated with a random value. If False, the password will not be updated
279
+ """
280
+ endpoint = f'https://graph.microsoft.com/beta/users/{user_id}' if len(custom_attributes) > 0 else f'https://graph.microsoft.com/v1.0/users/{user_id}'
281
+ payload = fields_to_update
282
+ if update_password:
283
+ password = self.__generate_password()
284
+ payload.update({"passwordProfile": {
285
+ "forceChangePasswordNextSignIn": False,
286
+ "password": f"{password}"
287
+ }})
288
+ if len(custom_attributes) > 0:
289
+ payload = self.__add_attribute_information(payload, custom_attributes)
290
+ response = requests.patch(endpoint, headers=self.headers, json=payload)
291
+ return response
292
+
293
+ def delete_user(self, user_id, delete=False):
294
+ """
295
+ Delete (soft or hard) a user from Azure Entra
296
+ :param user_id: The Azure AD ID of the user
297
+ :param delete: If True, the user will be deleted permanently. If False, the user will be soft deleted
298
+ """
299
+ endpoint = f"https://graph.microsoft.com/v1.0/users/{user_id}"
300
+ if delete:
301
+ response = requests.delete(endpoint, headers=self.headers)
302
+ else:
303
+ payload = {"accountEnabled": False}
304
+ response = requests.patch(endpoint, headers=self.headers, data=json.dumps(payload))
305
+ return response
306
+
307
+ def assign_user_to_group(self, user_id, group_id):
308
+ """
309
+ Assign a user to a group
310
+ :param user_id: The Azure AD ID of the user
311
+ :param group_id: The Azure AD ID of the group
312
+ return: response
313
+ """
314
+ url = f"https://graph.microsoft.com/v1.0/groups/{group_id}/members/$ref"
315
+ data = {"@odata.id": f"https://graph.microsoft.com/v1.0/directoryObjects/{user_id}"}
316
+ response = requests.post(url, headers=self.headers, data=json.dumps(data))
317
+ return response
318
+
319
+ def update_manager(self, user_id, manager_id):
320
+ """
321
+ Update the manager of a user
322
+ :param user_id: The Azure AD ID of the user
323
+ :param manager_id: The Azure AD ID of the manager
324
+ return: response
325
+ """
326
+ url = f"https://graph.microsoft.com/v1.0/users/{user_id}/manager/$ref"
327
+ content ={f"@odata.id": f"https://graph.microsoft.com/v1.0/users/{manager_id}"}
328
+ response = requests.put(url, headers=self.headers, data=json.dumps(content))
329
+ return response
330
+
331
+ def remove_user_from_group(self, user_id, group_id):
332
+ """
333
+ Remove a user from a group
334
+ :param user_id: The Azure AD ID of the user
335
+ :param group_id: The Azure AD ID of the group
336
+ return: response
337
+ """
338
+ url = f"https://graph.microsoft.com/v1.0/groups/{group_id}/members/{user_id}/$ref"
339
+ response = requests.delete(url, headers=self.headers)
340
+ return response
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 1.0
2
+ Name: brynq-sdk-azure
3
+ Version: 1.0.0
4
+ Summary: Azure wrapper from BrynQ
5
+ Home-page: UNKNOWN
6
+ Author: BrynQ
7
+ Author-email: support@brynq.com
8
+ License: BrynQ License
9
+ Description: Azure wrapper from BrynQ
10
+ Platform: UNKNOWN
@@ -0,0 +1,12 @@
1
+ setup.py
2
+ brynq_sdk/azure/__init__.py
3
+ brynq_sdk/azure/authentication.py
4
+ brynq_sdk/azure/azure_connection.py
5
+ brynq_sdk/azure/blob_storage.py
6
+ brynq_sdk/azure/entra.py
7
+ brynq_sdk_azure.egg-info/PKG-INFO
8
+ brynq_sdk_azure.egg-info/SOURCES.txt
9
+ brynq_sdk_azure.egg-info/dependency_links.txt
10
+ brynq_sdk_azure.egg-info/not-zip-safe
11
+ brynq_sdk_azure.egg-info/requires.txt
12
+ brynq_sdk_azure.egg-info/top_level.txt
@@ -0,0 +1,4 @@
1
+ brynq-sdk-brynq>=1
2
+ azure-storage-file-share>=12.6.0
3
+ azure-storage-blob>=12.16.0
4
+ msal==1.22.0
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,19 @@
1
+ from setuptools import setup
2
+
3
+ setup(
4
+ name='brynq_sdk_azure',
5
+ version='1.0.0',
6
+ description='Azure wrapper from BrynQ',
7
+ long_description='Azure wrapper from BrynQ',
8
+ author='BrynQ',
9
+ author_email='support@brynq.com',
10
+ packages=["brynq_sdk.azure"],
11
+ license='BrynQ License',
12
+ install_requires=[
13
+ 'brynq-sdk-brynq>=1',
14
+ 'azure-storage-file-share>=12.6.0',
15
+ 'azure-storage-blob>=12.16.0',
16
+ 'msal==1.22.0'
17
+ ],
18
+ zip_safe=False,
19
+ )