Skip to content

M kovalsky/msgraphapi #385

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
project = 'semantic-link-labs'
copyright = '2024, Microsoft and community'
author = 'Microsoft and community'
release = '0.8.11'
release = '0.9.1'

# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ name="semantic-link-labs"
authors = [
{ name = "Microsoft Corporation" },
]
version="0.8.11"
version="0.9.1"
description="Semantic Link Labs for Microsoft Fabric"
readme="README.md"
requires-python=">=3.10,<3.12"
Expand Down
3 changes: 2 additions & 1 deletion src/sempy_labs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
update_on_premises_gateway,
bind_semantic_model_to_gateway,
)

from sempy_labs._authentication import (
ServicePrincipalTokenProvider,
service_principal_authentication,
)
from sempy_labs._mirrored_databases import (
get_mirrored_database_definition,
Expand Down Expand Up @@ -470,4 +470,5 @@
"bind_semantic_model_to_gateway",
"list_semantic_model_errors",
"list_item_job_instances",
"service_principal_authentication",
]
82 changes: 78 additions & 4 deletions src/sempy_labs/_authentication.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from typing import Literal
from typing import Literal, Optional
from sempy.fabric._token_provider import TokenProvider
from azure.identity import ClientSecretCredential
from sempy._utils._log import log
from contextlib import contextmanager
import contextvars


class ServicePrincipalTokenProvider(TokenProvider):
Expand Down Expand Up @@ -41,6 +44,10 @@ def from_aad_application_key_authentication(
tenant_id=tenant_id, client_id=client_id, client_secret=client_secret
)

cls.tenant_id = tenant_id
cls.client_id = client_id
cls.client_secret = client_secret

return cls(credential)

@classmethod
Expand Down Expand Up @@ -89,16 +96,26 @@ def from_azure_key_vault(
tenant_id=tenant_id, client_id=client_id, client_secret=client_secret
)

cls.tenant_id = tenant_id
cls.client_id = client_id
cls.client_secret = client_secret

return cls(credential)

def __call__(
self, audience: Literal["pbi", "storage", "azure", "graph"] = "pbi"
self,
audience: Literal[
"pbi", "storage", "azure", "graph", "asazure", "keyvault"
] = "pbi",
region: Optional[str] = None,
) -> str:
"""
Parameters
----------
audience : Literal["pbi", "storage", "azure", "graph"] = "pbi") -> str
audience : Literal["pbi", "storage", "azure", "graph", "asazure", "keyvault"] = "pbi") -> str
Literal if it's for PBI/Fabric API call or OneLake/Storage Account call.
region : str, default=None
The region of the Azure Analysis Services. For example: 'westus2'.
"""
if audience == "pbi":
return self.credential.get_token(
Expand All @@ -114,12 +131,21 @@ def __call__(
return self.credential.get_token(
"https://graph.microsoft.com/.default"
).token
elif audience == "asazure":
return self.credential.get_token(
f"https://{region}.asazure.windows.net/.default"
).token
elif audience == "keyvault":
return self.credential.get_token("https://vault.azure.net/.default").token
else:
raise NotImplementedError


def _get_headers(
token_provider: str, audience: Literal["pbi", "storage", "azure", "graph"] = "azure"
token_provider: str,
audience: Literal[
"pbi", "storage", "azure", "graph", "asazure", "keyvault"
] = "azure",
):
"""
Generates headers for an API request.
Expand All @@ -135,3 +161,51 @@ def _get_headers(
headers["Content-Type"] = "application/json"

return headers


token_provider = contextvars.ContextVar("token_provider", default=None)


@log
@contextmanager
def service_principal_authentication(
key_vault_uri: str,
key_vault_tenant_id: str,
key_vault_client_id: str,
key_vault_client_secret: str,
):
"""
Establishes an authentication via Service Principal.

Parameters
----------
key_vault_uri : str
Azure Key Vault URI.
key_vault_tenant_id : str
Name of the secret in the Key Vault with the Fabric Tenant ID.
key_vault_client_id : str
Name of the secret in the Key Vault with the Service Principal Client ID.
key_vault_client_secret : str
Name of the secret in the Key Vault with the Service Principal Client Secret.
"""

# Save the prior state
prior_token = token_provider.get()

# Set the new token_provider in a thread-safe manner
token_provider.set(
ServicePrincipalTokenProvider.from_azure_key_vault(
key_vault_uri=key_vault_uri,
key_vault_tenant_id=key_vault_tenant_id,
key_vault_client_id=key_vault_client_id,
key_vault_client_secret=key_vault_client_secret,
)
)
try:
yield
finally:
# Restore the prior state
if prior_token is None:
token_provider.set(None)
else:
token_provider.set(prior_token)
114 changes: 114 additions & 0 deletions src/sempy_labs/_helper_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
from azure.core.credentials import TokenCredential, AccessToken
import numpy as np
from IPython.display import display, HTML
import requests
import sempy_labs._authentication as auth


def _build_url(url: str, params: dict) -> str:
Expand Down Expand Up @@ -1406,3 +1408,115 @@ def _get_fabric_context_setting(name: str):
def get_tenant_id():

_get_fabric_context_setting(name="trident.tenant.id")


def _base_api(
request: str,
client: str = "fabric",
method: str = "get",
payload: Optional[str] = None,
status_codes: Optional[int] = 200,
uses_pagination: bool = False,
lro_return_json: bool = False,
lro_return_status_code: bool = False,
):

from sempy_labs._authentication import _get_headers

if (lro_return_json or lro_return_status_code) and status_codes is None:
status_codes = [200, 202]

if isinstance(status_codes, int):
status_codes = [status_codes]

if client == "fabric":
c = fabric.FabricRestClient()
elif client == "fabric_sp":
c = fabric.FabricRestClient(token_provider=auth.token_provider.get())
elif client in ["azure", "graph"]:
pass
else:
raise ValueError(f"{icons.red_dot} The '{client}' client is not supported.")

if client not in ["azure", "graph"]:
if method == "get":
response = c.get(request)
elif method == "delete":
response = c.delete(request)
elif method == "post":
response = c.post(request, json=payload)
elif method == "patch":
response = c.patch(request, json=payload)
elif method == "put":
response = c.put(request, json=payload)
else:
raise NotImplementedError
else:
headers = _get_headers(auth.token_provider.get(), audience=client)
response = requests.request(
method.upper(),
f"https://graph.microsoft.com/v1.0/{request}",
headers=headers,
json=payload,
)

if lro_return_json:
return lro(c, response, status_codes).json()
elif lro_return_status_code:
return lro(c, response, status_codes, return_status_code=True)
else:
if response.status_code not in status_codes:
raise FabricHTTPException(response)
if uses_pagination:
responses = pagination(c, response)
return responses
else:
return response


def _create_dataframe(columns: dict) -> pd.DataFrame:

return pd.DataFrame(columns=list(columns.keys()))


def _update_dataframe_datatypes(dataframe: pd.DataFrame, column_map: dict):
"""
Updates the datatypes of columns in a pandas dataframe based on a column map.

Example:
{
"Order": "int",
"Public": "bool",
}
"""

for column, data_type in column_map.items():
if column in dataframe.columns:
if data_type == "int":
dataframe[column] = dataframe[column].astype(int)
elif data_type == "bool":
dataframe[column] = dataframe[column].astype(bool)
elif data_type == "float":
dataframe[column] = dataframe[column].astype(float)
elif data_type == "datetime":
dataframe[column] = pd.to_datetime(dataframe[column])
# This is for a special case in admin.list_reports where datetime itself does not work. Coerce fixes the issue.
elif data_type == "datetime_coerce":
dataframe[column] = pd.to_datetime(dataframe[column], errors="coerce")
elif data_type in ["str", "string"]:
dataframe[column] = dataframe[column].astype(str)
else:
raise NotImplementedError


def _print_success(item_name, item_type, workspace_name, action="created"):
if action == "created":
print(
f"{icons.green_dot} The '{item_name}' {item_type} has been successfully created in the '{workspace_name}' workspace."
)
elif action == "deleted":
print(
f"{icons.green_dot} The '{item_name}' {item_type} has been successfully deleted from the '{workspace_name}' workspace."
)
else:
raise NotImplementedError
33 changes: 33 additions & 0 deletions src/sempy_labs/graph/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from sempy_labs.graph._groups import (
list_groups,
list_group_owners,
list_group_members,
add_group_members,
add_group_owners,
resolve_group_id,
renew_group,
)
from sempy_labs.graph._users import (
resolve_user_id,
get_user,
list_users,
send_mail,
)
from sempy_labs.graph._teams import (
list_teams,
)

__all__ = [
"list_groups",
"list_group_owners",
"list_group_members",
"add_group_members",
"add_group_owners",
"renew_group",
"resolve_group_id",
"resolve_user_id",
"get_user",
"list_users",
"send_mail",
"list_teams",
]
Loading