CPILake-Utils 11.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.
- cpilake_utils-11.0.0/CPILake_Utils/CPILake_Utils.py +967 -0
- cpilake_utils-11.0.0/CPILake_Utils/__init__.py +16 -0
- cpilake_utils-11.0.0/CPILake_Utils.egg-info/PKG-INFO +97 -0
- cpilake_utils-11.0.0/CPILake_Utils.egg-info/SOURCES.txt +9 -0
- cpilake_utils-11.0.0/CPILake_Utils.egg-info/dependency_links.txt +1 -0
- cpilake_utils-11.0.0/CPILake_Utils.egg-info/requires.txt +1 -0
- cpilake_utils-11.0.0/CPILake_Utils.egg-info/top_level.txt +1 -0
- cpilake_utils-11.0.0/PKG-INFO +97 -0
- cpilake_utils-11.0.0/README.md +88 -0
- cpilake_utils-11.0.0/pyproject.toml +15 -0
- cpilake_utils-11.0.0/setup.cfg +4 -0
|
@@ -0,0 +1,967 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import json
|
|
3
|
+
import time
|
|
4
|
+
import pytz
|
|
5
|
+
import html
|
|
6
|
+
import base64
|
|
7
|
+
import requests
|
|
8
|
+
import pandas as pd
|
|
9
|
+
# import sempy.fabric as fabric
|
|
10
|
+
from pyspark.sql import SparkSession
|
|
11
|
+
from pyspark.sql import DataFrame, functions as F
|
|
12
|
+
from pyspark.sql.functions import unix_timestamp, col, max, from_utc_timestamp
|
|
13
|
+
from pyspark.conf import SparkConf
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
# from notebookutils.credentials import getSecret
|
|
16
|
+
# from azure.identity import CertificateCredential
|
|
17
|
+
# from tqdm.auto import tqdm
|
|
18
|
+
from typing import Optional, List, Dict, Tuple, Union
|
|
19
|
+
# from fabric.analytics.environment.credentials import SetFabricAnalyticsDefaultTokenCredentials
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
## <<<<<<<<<<<<<<<< hash_function
|
|
23
|
+
|
|
24
|
+
def hash_function(s):
|
|
25
|
+
"""Hash function for alphanumeric strings"""
|
|
26
|
+
if s is None:
|
|
27
|
+
return None
|
|
28
|
+
s = str(s).upper()
|
|
29
|
+
s = re.sub(r'[^A-Z0-9]', '', s)
|
|
30
|
+
base36_map = {ch: idx for idx, ch in enumerate("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ")}
|
|
31
|
+
result = 0
|
|
32
|
+
for i, ch in enumerate(reversed(s)):
|
|
33
|
+
result += base36_map.get(ch, 0) * (36 ** i)
|
|
34
|
+
result += len(s) * (36 ** (len(s) + 1))
|
|
35
|
+
return result
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
## <<<<<<<<<<<<<<<< send_email_via_http
|
|
39
|
+
|
|
40
|
+
def send_email_via_http(params):
|
|
41
|
+
"""
|
|
42
|
+
Sends an email using an HTTP endpoint (uses global endpoint_url & access_token set by init_mail()).
|
|
43
|
+
|
|
44
|
+
Required:
|
|
45
|
+
- to (str | list), subject (str), body (str) # body will be replaced if df_in_body=True
|
|
46
|
+
|
|
47
|
+
Optional:
|
|
48
|
+
- cc, bcc, from_addr, headers (dict), attachments (list), timeout (int, default 15)
|
|
49
|
+
- df : Spark or Pandas DataFrame to render into the email body
|
|
50
|
+
- df_limit : limit rows if Spark DF (default 1000)
|
|
51
|
+
- df_in_body : if True (default), replace body with styled DF HTML (your format)
|
|
52
|
+
- df_attach : if True, also attach the same DF as HTML file (default False)
|
|
53
|
+
- df_name : attachment filename (default "data.html")
|
|
54
|
+
- tz_name : timezone string for timestamp header (default "America/Los_Angeles")
|
|
55
|
+
"""
|
|
56
|
+
# Ensure init_mail() ran
|
|
57
|
+
try:
|
|
58
|
+
_ = endpoint_url
|
|
59
|
+
except NameError:
|
|
60
|
+
raise RuntimeError("endpoint_url not set. Call init_mail(...) once in this session before send_email_via_http().")
|
|
61
|
+
try:
|
|
62
|
+
_ = access_token
|
|
63
|
+
except NameError:
|
|
64
|
+
raise RuntimeError("access_token not set. Call init_mail(...) once in this session before send_email_via_http().")
|
|
65
|
+
|
|
66
|
+
# Required checks
|
|
67
|
+
required = ['to', 'subject', 'body']
|
|
68
|
+
missing = [f for f in required if not params.get(f)]
|
|
69
|
+
if missing:
|
|
70
|
+
return None, f"Missing required fields: {', '.join(missing)}"
|
|
71
|
+
|
|
72
|
+
# Base payload
|
|
73
|
+
payload = {
|
|
74
|
+
"to": ";".join(params["to"]) if isinstance(params["to"], list) else params["to"],
|
|
75
|
+
"subject": params["subject"],
|
|
76
|
+
"body": params["body"],
|
|
77
|
+
}
|
|
78
|
+
if params.get("cc"):
|
|
79
|
+
payload["cc"] = params["cc"] if isinstance(params["cc"], list) else [params["cc"]]
|
|
80
|
+
if params.get("bcc"):
|
|
81
|
+
payload["bcc"] = params["bcc"] if isinstance(params["bcc"], list) else [params["bcc"]]
|
|
82
|
+
if params.get("from_addr"):
|
|
83
|
+
payload["from"] = params["from_addr"]
|
|
84
|
+
if params.get("attachments"):
|
|
85
|
+
payload["attachments"] = params["attachments"]
|
|
86
|
+
|
|
87
|
+
# ---- DataFrame → HTML body (your existing style) ----
|
|
88
|
+
df = params.get("df")
|
|
89
|
+
if df is not None:
|
|
90
|
+
df_limit = int(params.get("df_limit", 1000))
|
|
91
|
+
tz_name = params.get("tz_name", "America/Los_Angeles")
|
|
92
|
+
df_in_body = params.get("df_in_body", True)
|
|
93
|
+
df_attach = params.get("df_attach", False)
|
|
94
|
+
df_name = params.get("df_name", "data.html")
|
|
95
|
+
|
|
96
|
+
# Get pandas DataFrame
|
|
97
|
+
pdf = None
|
|
98
|
+
try:
|
|
99
|
+
from pyspark.sql import DataFrame as SparkDF
|
|
100
|
+
if isinstance(df, SparkDF):
|
|
101
|
+
pdf = df.limit(df_limit).toPandas()
|
|
102
|
+
else:
|
|
103
|
+
pdf = df # assume already pandas
|
|
104
|
+
except Exception:
|
|
105
|
+
pdf = df
|
|
106
|
+
|
|
107
|
+
html_body = _df_to_html_table(pdf, tz_name=tz_name)
|
|
108
|
+
|
|
109
|
+
if df_in_body:
|
|
110
|
+
subject = str(params.get("subject", ""))
|
|
111
|
+
if "QA Success" in subject:
|
|
112
|
+
payload["body"] ='<html><body><h4>No data available to display.</h4></body></html>'
|
|
113
|
+
else:
|
|
114
|
+
payload["body"] = html_body
|
|
115
|
+
else:
|
|
116
|
+
# append to body if you prefer not to replace
|
|
117
|
+
payload["body"] = f'{payload["body"]}{html_body}'
|
|
118
|
+
|
|
119
|
+
if df_attach:
|
|
120
|
+
content_b64 = base64.b64encode(html_body.encode("utf-8")).decode("utf-8")
|
|
121
|
+
attach = {"name": df_name, "contentBytes": content_b64, "contentType": "text/html"}
|
|
122
|
+
if "attachments" in payload and isinstance(payload["attachments"], list):
|
|
123
|
+
payload["attachments"].append(attach)
|
|
124
|
+
else:
|
|
125
|
+
payload["attachments"] = [attach]
|
|
126
|
+
|
|
127
|
+
# Auth header
|
|
128
|
+
req_headers = {"Authorization": f"Bearer {access_token}"}
|
|
129
|
+
if params.get("headers"):
|
|
130
|
+
req_headers.update(params["headers"])
|
|
131
|
+
|
|
132
|
+
timeout = params.get("timeout", 15)
|
|
133
|
+
|
|
134
|
+
# Send
|
|
135
|
+
try:
|
|
136
|
+
response = requests.post(endpoint_url, json=payload, headers=req_headers, timeout=timeout)
|
|
137
|
+
status_msg = "✅ Success" if response.status_code == 200 else f"❌ Failed ({response.status_code})"
|
|
138
|
+
print(f"Email send: {status_msg}")
|
|
139
|
+
return response.status_code, response.text, req_headers
|
|
140
|
+
except requests.RequestException as e:
|
|
141
|
+
error_msg = f"Request failed: {str(e)}"
|
|
142
|
+
print(f"❌ {error_msg}")
|
|
143
|
+
return None, error_msg
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
## <<<<<<<<<<<<<<<< _df_to_html_table
|
|
147
|
+
|
|
148
|
+
def _df_to_html_table(pdf, tz_name="America/Los_Angeles"):
|
|
149
|
+
"""Render a pandas DataFrame to your styled HTML table."""
|
|
150
|
+
# Empty DF → simple message
|
|
151
|
+
if pdf is None or len(pdf.index) == 0:
|
|
152
|
+
return '<html><body><h4>No data available to display.</h4></body></html>'
|
|
153
|
+
|
|
154
|
+
# Header with PST time
|
|
155
|
+
pst = pytz.timezone(tz_name)
|
|
156
|
+
now_pst = datetime.now(pst).strftime("%Y-%m-%d %H:%M:%S")
|
|
157
|
+
|
|
158
|
+
html_Table = []
|
|
159
|
+
html_Table.append('<html><head><style>')
|
|
160
|
+
html_Table.append('table {border-collapse: collapse; width: 100%} '
|
|
161
|
+
'table, td, th {border: 1px solid black; padding: 3px; font-size: 9pt;} '
|
|
162
|
+
'td, th {text-align: left;}')
|
|
163
|
+
html_Table.append('</style></head><body>')
|
|
164
|
+
html_Table.append(f'<h4>(Refresh Time: {now_pst})</h4><hr>')
|
|
165
|
+
html_Table.append('<table style="width:100%; border-collapse: collapse;">')
|
|
166
|
+
html_Table.append('<thead style="background-color:#000000; color:#ffffff;"><tr>')
|
|
167
|
+
|
|
168
|
+
# Columns (skip FailureFlag in header, to match your code)
|
|
169
|
+
cols = list(pdf.columns)
|
|
170
|
+
visible_cols = [c for c in cols if c != "FailureFlag"]
|
|
171
|
+
for c in visible_cols:
|
|
172
|
+
html_Table.append(f'<th style="border: 1px solid black; padding: 5px;">{html.escape(str(c))}</th>')
|
|
173
|
+
html_Table.append('</tr></thead><tbody>')
|
|
174
|
+
|
|
175
|
+
# Rows (highlight red if FailureFlag == 'Yes', else light green default)
|
|
176
|
+
ff_present = "FailureFlag" in cols
|
|
177
|
+
for _, row in pdf.iterrows():
|
|
178
|
+
row_bg_color = '#ccff66' # default
|
|
179
|
+
if ff_present:
|
|
180
|
+
try:
|
|
181
|
+
if str(row["FailureFlag"]).strip().lower() == "yes":
|
|
182
|
+
row_bg_color = '#ff8080'
|
|
183
|
+
except Exception:
|
|
184
|
+
pass
|
|
185
|
+
html_Table.append(f'<tr style="background-color:{row_bg_color};">')
|
|
186
|
+
for c in visible_cols:
|
|
187
|
+
val = row[c]
|
|
188
|
+
html_Table.append(f'<td>{html.escape("" if val is None else str(val))}</td>')
|
|
189
|
+
html_Table.append('</tr>')
|
|
190
|
+
|
|
191
|
+
html_Table.append('</tbody></table></body></html>')
|
|
192
|
+
return "".join(html_Table)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# s<<<<<<<<<<< end_email_no_attachment
|
|
196
|
+
|
|
197
|
+
def send_email_no_attachment(
|
|
198
|
+
body: str,
|
|
199
|
+
recipients: List[str],
|
|
200
|
+
tenant_id: str,
|
|
201
|
+
client_id: str,
|
|
202
|
+
certificate_secret_name: str,
|
|
203
|
+
keyvault_url: str,
|
|
204
|
+
endpoint_url: Optional[str] = None,
|
|
205
|
+
scope: Optional[str] = None,
|
|
206
|
+
subject: Optional[str] = None,
|
|
207
|
+
headers: Optional[Dict[str, str]] = None,
|
|
208
|
+
timeout: int = 15
|
|
209
|
+
) -> Tuple[Optional[int], str]:
|
|
210
|
+
|
|
211
|
+
from notebookutils.credentials import getSecret
|
|
212
|
+
from azure.identity import CertificateCredential
|
|
213
|
+
|
|
214
|
+
# Defaults
|
|
215
|
+
if endpoint_url is None:
|
|
216
|
+
endpoint_url = (
|
|
217
|
+
"https://fdne-inframail-logicapp01.azurewebsites.net:443/"
|
|
218
|
+
"api/fdne-infra-appmail-sender/triggers/"
|
|
219
|
+
"When_a_HTTP_request_is_received/invoke"
|
|
220
|
+
"?api-version=2022-05-01" )
|
|
221
|
+
|
|
222
|
+
if scope is None:
|
|
223
|
+
scope = "api://27d45411-0d7a-4f27-bc5f-412d74ea249b/.default"
|
|
224
|
+
|
|
225
|
+
# Payload
|
|
226
|
+
payload = {
|
|
227
|
+
"to": ";".join(recipients),
|
|
228
|
+
"subject": subject or "",
|
|
229
|
+
"body": body, }
|
|
230
|
+
|
|
231
|
+
# Credential
|
|
232
|
+
secret_value = getSecret(keyvault_url, certificate_secret_name)
|
|
233
|
+
certificate_data = base64.b64decode(secret_value)
|
|
234
|
+
|
|
235
|
+
credential = CertificateCredential(
|
|
236
|
+
tenant_id=tenant_id,
|
|
237
|
+
client_id=client_id,
|
|
238
|
+
certificate_data=certificate_data,
|
|
239
|
+
send_certificate_chain=True
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
access_token = credential.get_token(scope).token
|
|
243
|
+
|
|
244
|
+
# Headers
|
|
245
|
+
request_headers = {
|
|
246
|
+
"Content-Type": "application/json",
|
|
247
|
+
"Authorization": f"Bearer {access_token}",
|
|
248
|
+
**(headers or {}),
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
# Call API
|
|
252
|
+
try:
|
|
253
|
+
resp = requests.post(
|
|
254
|
+
endpoint_url,
|
|
255
|
+
json=payload,
|
|
256
|
+
headers=request_headers,
|
|
257
|
+
timeout=timeout,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
if resp.status_code in (200, 201, 202):
|
|
261
|
+
return resp.status_code, resp.text
|
|
262
|
+
|
|
263
|
+
return resp.status_code, f"Failed: {resp.text}"
|
|
264
|
+
|
|
265
|
+
except requests.RequestException as e:
|
|
266
|
+
return None, str(e)
|
|
267
|
+
"""
|
|
268
|
+
# call the function
|
|
269
|
+
status, response = send_email_no_attachment(
|
|
270
|
+
body=markdown,
|
|
271
|
+
recipients=recipients,
|
|
272
|
+
subject=subject,
|
|
273
|
+
tenant_id=tenant_id,
|
|
274
|
+
client_id=client_id,
|
|
275
|
+
certificate_secret_name = certificate_secret_name,
|
|
276
|
+
keyvault_url=keyvault_url
|
|
277
|
+
)
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
# >>>>>>>>>>>>>>>>> end_email_no_attachment
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
## <<<<<<<<<<<<<<<< QA_CheckUtil
|
|
284
|
+
"""
|
|
285
|
+
A status column: PASS / FAIL / SKIPPED
|
|
286
|
+
A skip_reason column
|
|
287
|
+
Checks are skipped instead of failing when:
|
|
288
|
+
One or both DataFrames are empty
|
|
289
|
+
Required columns are missing
|
|
290
|
+
Aggregation column exists but contains only nulls
|
|
291
|
+
match becomes None when skipped (clearer than False)
|
|
292
|
+
"""
|
|
293
|
+
def QA_CheckUtil(
|
|
294
|
+
source_df: DataFrame,
|
|
295
|
+
qa_df: DataFrame
|
|
296
|
+
) -> DataFrame:
|
|
297
|
+
|
|
298
|
+
spark = source_df.sparkSession
|
|
299
|
+
qa_rows: List[tuple] = []
|
|
300
|
+
|
|
301
|
+
def calc_diff(src, qa):
|
|
302
|
+
if src is None or qa is None:
|
|
303
|
+
return None
|
|
304
|
+
return float(src) - float(qa)
|
|
305
|
+
|
|
306
|
+
def add_row(check_type, check_name, column, src, qa, skip_reason=None):
|
|
307
|
+
if skip_reason:
|
|
308
|
+
qa_rows.append((
|
|
309
|
+
check_type,
|
|
310
|
+
check_name,
|
|
311
|
+
column,
|
|
312
|
+
src,
|
|
313
|
+
qa,
|
|
314
|
+
None,
|
|
315
|
+
None,
|
|
316
|
+
"SKIPPED",
|
|
317
|
+
skip_reason
|
|
318
|
+
))
|
|
319
|
+
else:
|
|
320
|
+
match = src == qa
|
|
321
|
+
qa_rows.append((
|
|
322
|
+
check_type,
|
|
323
|
+
check_name,
|
|
324
|
+
column,
|
|
325
|
+
src,
|
|
326
|
+
qa,
|
|
327
|
+
calc_diff(src, qa),
|
|
328
|
+
match,
|
|
329
|
+
"PASS" if match else "FAIL",
|
|
330
|
+
None
|
|
331
|
+
))
|
|
332
|
+
|
|
333
|
+
# Row count
|
|
334
|
+
src_count = source_df.count()
|
|
335
|
+
qa_count = qa_df.count()
|
|
336
|
+
|
|
337
|
+
add_row(
|
|
338
|
+
"ROW_COUNT",
|
|
339
|
+
"row_count",
|
|
340
|
+
None,
|
|
341
|
+
float(src_count),
|
|
342
|
+
float(qa_count)
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# Null check
|
|
346
|
+
common_cols = set(source_df.columns).intersection(set(qa_df.columns))
|
|
347
|
+
|
|
348
|
+
if not common_cols:
|
|
349
|
+
add_row(
|
|
350
|
+
"NULL_CHECK",
|
|
351
|
+
"null_count",
|
|
352
|
+
None,
|
|
353
|
+
None,
|
|
354
|
+
None,
|
|
355
|
+
"No common columns between source and QA"
|
|
356
|
+
)
|
|
357
|
+
else:
|
|
358
|
+
for col in common_cols:
|
|
359
|
+
src_nulls = source_df.filter(F.col(col).isNull()).count()
|
|
360
|
+
qa_nulls = qa_df.filter(F.col(col).isNull()).count()
|
|
361
|
+
|
|
362
|
+
add_row(
|
|
363
|
+
"NULL_CHECK",
|
|
364
|
+
"null_count",
|
|
365
|
+
col,
|
|
366
|
+
float(src_nulls),
|
|
367
|
+
float(qa_nulls)
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
# Aggregation check
|
|
372
|
+
if "amount" not in source_df.columns or "amount" not in qa_df.columns:
|
|
373
|
+
add_row(
|
|
374
|
+
"AGG_CHECK",
|
|
375
|
+
"sum",
|
|
376
|
+
"amount",
|
|
377
|
+
None,
|
|
378
|
+
None,
|
|
379
|
+
"Column 'amount' missing in one or both DataFrames"
|
|
380
|
+
)
|
|
381
|
+
else:
|
|
382
|
+
src_sum = source_df.select(F.sum("amount")).collect()[0][0]
|
|
383
|
+
qa_sum = qa_df.select(F.sum("amount")).collect()[0][0]
|
|
384
|
+
|
|
385
|
+
if src_sum is None and qa_sum is None:
|
|
386
|
+
add_row(
|
|
387
|
+
"AGG_CHECK",
|
|
388
|
+
"sum",
|
|
389
|
+
"amount",
|
|
390
|
+
None,
|
|
391
|
+
None,
|
|
392
|
+
"All values are NULL in both DataFrames"
|
|
393
|
+
)
|
|
394
|
+
else:
|
|
395
|
+
add_row(
|
|
396
|
+
"AGG_CHECK",
|
|
397
|
+
"sum",
|
|
398
|
+
"amount",
|
|
399
|
+
float(src_sum or 0.0),
|
|
400
|
+
float(qa_sum or 0.0)
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
# Duplicate check on id column
|
|
405
|
+
if "id" not in source_df.columns or "id" not in qa_df.columns:
|
|
406
|
+
add_row(
|
|
407
|
+
"DUPLICATE_CHECK",
|
|
408
|
+
"duplicate_id",
|
|
409
|
+
"id",
|
|
410
|
+
None,
|
|
411
|
+
None,
|
|
412
|
+
"Column 'id' missing in one or both DataFrames"
|
|
413
|
+
)
|
|
414
|
+
else:
|
|
415
|
+
src_dupes = source_df.count() - source_df.select("id").distinct().count()
|
|
416
|
+
qa_dupes = qa_df.count() - qa_df.select("id").distinct().count()
|
|
417
|
+
|
|
418
|
+
add_row(
|
|
419
|
+
"DUPLICATE_CHECK",
|
|
420
|
+
"duplicate_id",
|
|
421
|
+
"id",
|
|
422
|
+
float(src_dupes),
|
|
423
|
+
float(qa_dupes)
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
# Create final QA DataFrame
|
|
427
|
+
return spark.createDataFrame(
|
|
428
|
+
qa_rows,
|
|
429
|
+
[
|
|
430
|
+
"check_type",
|
|
431
|
+
"check_name",
|
|
432
|
+
"column_name",
|
|
433
|
+
"source_value",
|
|
434
|
+
"qa_value",
|
|
435
|
+
"diff",
|
|
436
|
+
"match",
|
|
437
|
+
"status",
|
|
438
|
+
"skip_reason"
|
|
439
|
+
]
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
## <<<<<<<<<<<<<<<< create_lakehouse_shortcuts
|
|
443
|
+
|
|
444
|
+
def create_lakehouse_shortcuts(shortcut_configs, workspace_id, lakehouse_id, target_schema):
|
|
445
|
+
access_token = mssparkutils.credentials.getToken("https://api.fabric.microsoft.com/.default")
|
|
446
|
+
headers = {
|
|
447
|
+
"Authorization": f"Bearer {access_token}",
|
|
448
|
+
"Content-Type": "application/json"
|
|
449
|
+
}
|
|
450
|
+
print("Access token starts with:", access_token[:20])
|
|
451
|
+
|
|
452
|
+
for config in shortcut_configs:
|
|
453
|
+
source_path = config["source_subpath"]
|
|
454
|
+
target_shortcut_name = config["target_shortcut_name"]
|
|
455
|
+
source_workspace_id = config["source_workspace_id"]
|
|
456
|
+
source_lakehouse_id = config["source_lakehouse_id"]
|
|
457
|
+
|
|
458
|
+
target_path = f"Tables/{target_schema or 'dbo'}/"
|
|
459
|
+
|
|
460
|
+
payload = {
|
|
461
|
+
"path": target_path,
|
|
462
|
+
"name": target_shortcut_name,
|
|
463
|
+
"target": {
|
|
464
|
+
"type": "OneLake",
|
|
465
|
+
"oneLake": {
|
|
466
|
+
"workspaceId": source_workspace_id,
|
|
467
|
+
"itemId": source_lakehouse_id,
|
|
468
|
+
"path": source_path
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
url = f"https://api.fabric.microsoft.com/v1/workspaces/{workspace_id}/items/{lakehouse_id}/shortcuts"
|
|
474
|
+
print(f"Creating shortcut '{target_shortcut_name}' → {target_path}")
|
|
475
|
+
print(json.dumps(payload, indent=2))
|
|
476
|
+
|
|
477
|
+
# --- Send POST request ---
|
|
478
|
+
response = requests.post(url, headers=headers, json=payload)
|
|
479
|
+
|
|
480
|
+
if response.status_code in [200, 201]:
|
|
481
|
+
print(f"Shortcut '{target_shortcut_name}' created successfully.")
|
|
482
|
+
print(response.json())
|
|
483
|
+
else:
|
|
484
|
+
print(f"Failed to create shortcut '{target_shortcut_name}'.")
|
|
485
|
+
print("Status Code:", response.status_code)
|
|
486
|
+
print("Response:", response.text)
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
## <<<<<<<<<<<<<<<< create_lakehouse_shortcuts_01
|
|
490
|
+
|
|
491
|
+
def create_lakehouse_shortcuts_01(shortcut_configs):
|
|
492
|
+
access_token = mssparkutils.credentials.getToken("https://api.fabric.microsoft.com/.default")
|
|
493
|
+
headers = {"Authorization": f"Bearer {access_token}", "Content-Type": "application/json" }
|
|
494
|
+
print("Access token starts with:", access_token[:20])
|
|
495
|
+
|
|
496
|
+
for config in shortcut_configs:
|
|
497
|
+
source_path = config["source_subpath"]
|
|
498
|
+
target_schema = config["target_schema"]
|
|
499
|
+
workspace_name = config["workspace_name"]
|
|
500
|
+
lakehouse_name = config["lakehouse_name"]
|
|
501
|
+
target_shortcut_name = config["target_shortcut_name"]
|
|
502
|
+
|
|
503
|
+
resp_ws = requests.get("https://api.fabric.microsoft.com/v1/workspaces", headers=headers)
|
|
504
|
+
resp_ws.raise_for_status()
|
|
505
|
+
workspace_id = next(ws["id"] for ws in resp_ws.json()["value"] if ws["displayName"] == workspace_name)
|
|
506
|
+
|
|
507
|
+
resp_lh = requests.get(f"https://api.fabric.microsoft.com/v1/workspaces/{workspace_id}/lakehouses", headers=headers)
|
|
508
|
+
resp_lh.raise_for_status()
|
|
509
|
+
lakehouse_id = next(lh["id"] for lh in resp_lh.json()["value"] if lh["displayName"] == lakehouse_name)
|
|
510
|
+
|
|
511
|
+
target_path = f"Tables/{target_schema or 'dbo'}/"
|
|
512
|
+
|
|
513
|
+
payload = {
|
|
514
|
+
"path": target_path,
|
|
515
|
+
"name": target_shortcut_name,
|
|
516
|
+
"target": {
|
|
517
|
+
"type": "OneLake",
|
|
518
|
+
"oneLake": {
|
|
519
|
+
"workspaceId" : workspace_id,
|
|
520
|
+
"itemId" : lakehouse_id,
|
|
521
|
+
"path" : source_path,
|
|
522
|
+
"target_schema" : config["target_schema"]
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
url = f"https://api.fabric.microsoft.com/v1/workspaces/{workspace_id}/items/{lakehouse_id}/shortcuts"
|
|
528
|
+
print(f"Creating shortcut '{target_shortcut_name}' → {target_path}")
|
|
529
|
+
print(json.dumps(payload, indent=2))
|
|
530
|
+
|
|
531
|
+
# --- Send POST request ---
|
|
532
|
+
response = requests.post(url, headers=headers, json=payload)
|
|
533
|
+
|
|
534
|
+
if response.status_code in [200, 201]:
|
|
535
|
+
print(f"Shortcut '{target_shortcut_name}' created successfully.")
|
|
536
|
+
print(response.json())
|
|
537
|
+
else:
|
|
538
|
+
print(f"Failed to create shortcut '{target_shortcut_name}'.")
|
|
539
|
+
print("Status Code:", response.status_code)
|
|
540
|
+
print("Response:", response.text)
|
|
541
|
+
|
|
542
|
+
"""
|
|
543
|
+
# How to call function
|
|
544
|
+
shortcut_configs = [
|
|
545
|
+
{
|
|
546
|
+
"target_shortcut_name" : "DIM_Date",
|
|
547
|
+
"workspace_name" : "FDnECostHubReporting_DEV",
|
|
548
|
+
"lakehouse_name" : "Cost_Hub",
|
|
549
|
+
"source_subpath" : "Tables/DIM_Date",
|
|
550
|
+
"target_schema" : "CostHub",
|
|
551
|
+
}
|
|
552
|
+
]
|
|
553
|
+
create_lakehouse_shortcuts_01(shortcut_configs)
|
|
554
|
+
"""
|
|
555
|
+
|
|
556
|
+
## <<<<<<<<<<<<<<<< create_adls_shortcuts
|
|
557
|
+
|
|
558
|
+
def create_adls_shortcuts(shortcut_configs, workspace_id, lakehouse_id, target_schema):
|
|
559
|
+
access_token = mssparkutils.credentials.getToken("https://api.fabric.microsoft.com/.default")
|
|
560
|
+
headers = {
|
|
561
|
+
"Authorization": f"Bearer {access_token}",
|
|
562
|
+
"Content-Type": "application/json"
|
|
563
|
+
}
|
|
564
|
+
print("Access token starts with:", access_token[:20])
|
|
565
|
+
|
|
566
|
+
target_path = f"Tables/{target_schema}/"
|
|
567
|
+
|
|
568
|
+
for config in shortcut_configs:
|
|
569
|
+
payload = {
|
|
570
|
+
"name": config["name"],
|
|
571
|
+
"path": target_path,
|
|
572
|
+
"target": {
|
|
573
|
+
"type": "AdlsGen2",
|
|
574
|
+
"adlsGen2": {
|
|
575
|
+
"connectionId": config["connection_id"],
|
|
576
|
+
"location": config["location"],
|
|
577
|
+
"subpath": config["subpath"]
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
url = f"https://api.fabric.microsoft.com/v1/workspaces/{workspace_id}/items/{lakehouse_id}/shortcuts"
|
|
583
|
+
response = requests.post(url, headers=headers, json=payload)
|
|
584
|
+
|
|
585
|
+
if response.status_code in [200, 201]:
|
|
586
|
+
print(f"Shortcut '{config['name']}' created successfully.")
|
|
587
|
+
else:
|
|
588
|
+
print(f"Failed to create shortcut '{config['name']}'.")
|
|
589
|
+
print("Status Code:", response.status_code)
|
|
590
|
+
print("Response:", response.text)
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
## <<<<<<<<<<<<<<<< create_adls_shortcuts_01
|
|
594
|
+
|
|
595
|
+
def create_adls_shortcuts_01(shortcut_configs):
|
|
596
|
+
access_token = mssparkutils.credentials.getToken("https://api.fabric.microsoft.com/.default")
|
|
597
|
+
headers = {"Authorization": f"Bearer {access_token}", "Content-Type": "application/json" }
|
|
598
|
+
print("Access token starts with:", access_token[:20])
|
|
599
|
+
|
|
600
|
+
for config in shortcut_configs:
|
|
601
|
+
target_schema = config["target_schema"]
|
|
602
|
+
workspace_name = config["workspace_name"]
|
|
603
|
+
lakehouse_name = config["lakehouse_name"]
|
|
604
|
+
connection_name = config["connection_name"]
|
|
605
|
+
target_path = f"Tables/{target_schema}/"
|
|
606
|
+
|
|
607
|
+
resp_ws = requests.get("https://api.fabric.microsoft.com/v1/workspaces", headers=headers)
|
|
608
|
+
resp_ws.raise_for_status()
|
|
609
|
+
workspace_id = next(ws["id"] for ws in resp_ws.json()["value"] if ws["displayName"] == workspace_name)
|
|
610
|
+
|
|
611
|
+
resp_lh = requests.get(f"https://api.fabric.microsoft.com/v1/workspaces/{workspace_id}/lakehouses", headers=headers)
|
|
612
|
+
resp_lh.raise_for_status()
|
|
613
|
+
lakehouse_id = next(lh["id"] for lh in resp_lh.json()["value"] if lh["displayName"] == lakehouse_name)
|
|
614
|
+
|
|
615
|
+
resp_cn = requests.get(f"https://api.fabric.microsoft.com/v1/connections", headers=headers)
|
|
616
|
+
resp_cn.raise_for_status()
|
|
617
|
+
connection_id = next(conn["id"] for conn in resp_cn.json()["value"] if conn["displayName"] == connection_name)
|
|
618
|
+
|
|
619
|
+
conn_loc = next(conn for conn in resp_cn.json()["value"] if conn["displayName"] == connection_name)
|
|
620
|
+
location = conn_loc["connectionDetails"]["path"]
|
|
621
|
+
|
|
622
|
+
payload = {
|
|
623
|
+
"name": config["name"],
|
|
624
|
+
"path": target_path,
|
|
625
|
+
"target": {
|
|
626
|
+
"type": "AdlsGen2",
|
|
627
|
+
"adlsGen2": {
|
|
628
|
+
"connectionId" : connection_id,
|
|
629
|
+
"location" : location,
|
|
630
|
+
"subpath" : config["subpath"]
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
url = f"https://api.fabric.microsoft.com/v1/workspaces/{workspace_id}/items/{lakehouse_id}/shortcuts"
|
|
636
|
+
response = requests.post(url, headers=headers, json=payload)
|
|
637
|
+
|
|
638
|
+
if response.status_code in [200, 201]:
|
|
639
|
+
print(f"Shortcut '{config['name']}' created successfully.")
|
|
640
|
+
else:
|
|
641
|
+
print(f"Failed to create shortcut '{config['name']}'.")
|
|
642
|
+
print("Status Code:", response.status_code)
|
|
643
|
+
print("Response:", response.text)
|
|
644
|
+
|
|
645
|
+
"""
|
|
646
|
+
# How to call function
|
|
647
|
+
|
|
648
|
+
# Define shortcut configurations
|
|
649
|
+
shortcut_configs = [
|
|
650
|
+
{
|
|
651
|
+
"name" : "Bridge_ExecOrgSummary",
|
|
652
|
+
"target_schema" : "CostHub",
|
|
653
|
+
"workspace_name" : "FDnECostHubReporting_DEV",
|
|
654
|
+
"lakehouse_name" : "Cost_Hub",
|
|
655
|
+
"connection_name" : "CostHub_ADLS abibrahi",
|
|
656
|
+
"subpath" : "/abidatamercury/MercuryDataProd/CostHub/Bridge_ExecOrgSummary"
|
|
657
|
+
}]
|
|
658
|
+
|
|
659
|
+
# Call the function
|
|
660
|
+
create_adls_shortcuts_01(shortcut_configs)
|
|
661
|
+
"""
|
|
662
|
+
|
|
663
|
+
## <<<<<<<<<<<<<<<< lakehouse_metadata_sync
|
|
664
|
+
|
|
665
|
+
def pad_or_truncate_string(input_string, length, pad_char=' '):
|
|
666
|
+
if len(input_string) > length:
|
|
667
|
+
return input_string[:length]
|
|
668
|
+
return input_string.ljust(length, pad_char)
|
|
669
|
+
|
|
670
|
+
def lakehouse_metadata_sync(workspace_id, lakehouse_id):
|
|
671
|
+
client = fabric.FabricRestClient()
|
|
672
|
+
|
|
673
|
+
# Get the SQL endpoint ID from the lakehouse
|
|
674
|
+
lakehouse_props = client.get(f"/v1/workspaces/{workspace_id}/lakehouses/{lakehouse_id}").json()
|
|
675
|
+
sqlendpoint = lakehouse_props['properties']['sqlEndpointProperties']['id']
|
|
676
|
+
|
|
677
|
+
# Prepare the metadata refresh payload
|
|
678
|
+
uri = f"/v1.0/myorg/lhdatamarts/{sqlendpoint}"
|
|
679
|
+
payload = {
|
|
680
|
+
"commands": [
|
|
681
|
+
{"$type": "MetadataRefreshExternalCommand"}
|
|
682
|
+
]
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
try:
|
|
686
|
+
response = client.post(uri, json=payload)
|
|
687
|
+
response_data = response.json()
|
|
688
|
+
|
|
689
|
+
batchId = response_data["batchId"]
|
|
690
|
+
progressState = response_data["progressState"]
|
|
691
|
+
statusuri = f"/v1.0/myorg/lhdatamarts/{sqlendpoint}/batches/{batchId}"
|
|
692
|
+
|
|
693
|
+
# Poll the status until it's no longer "inProgress"
|
|
694
|
+
while progressState == 'inProgress':
|
|
695
|
+
time.sleep(2)
|
|
696
|
+
status_response = client.get(statusuri).json()
|
|
697
|
+
progressState = status_response["progressState"]
|
|
698
|
+
display(f"Sync state: {progressState}")
|
|
699
|
+
|
|
700
|
+
# Handle success
|
|
701
|
+
if progressState == 'success':
|
|
702
|
+
table_details = [
|
|
703
|
+
{
|
|
704
|
+
'tableName': t['tableName'],
|
|
705
|
+
'warningMessages': t.get('warningMessages', []),
|
|
706
|
+
'lastSuccessfulUpdate': t.get('lastSuccessfulUpdate', 'N/A'),
|
|
707
|
+
'tableSyncState': t['tableSyncState'],
|
|
708
|
+
'sqlSyncState': t['sqlSyncState']
|
|
709
|
+
}
|
|
710
|
+
for t in status_response['operationInformation'][0]['progressDetail']['tablesSyncStatus']
|
|
711
|
+
]
|
|
712
|
+
|
|
713
|
+
print("✅ Extracted Table Details:")
|
|
714
|
+
for detail in table_details:
|
|
715
|
+
print(
|
|
716
|
+
f"Table: {pad_or_truncate_string(detail['tableName'], 30)}"
|
|
717
|
+
f" | Last Update: {detail['lastSuccessfulUpdate']}"
|
|
718
|
+
f" | tableSyncState: {detail['tableSyncState']}"
|
|
719
|
+
f" | Warnings: {detail['warningMessages']}"
|
|
720
|
+
)
|
|
721
|
+
return {"status": "success", "details": table_details}
|
|
722
|
+
|
|
723
|
+
# Handle failure
|
|
724
|
+
elif progressState == 'failure':
|
|
725
|
+
print("❌ Metadata sync failed.")
|
|
726
|
+
display(status_response)
|
|
727
|
+
return {"status": "failure", "error": status_response}
|
|
728
|
+
|
|
729
|
+
else:
|
|
730
|
+
print(f"⚠️ Unexpected progress state: {progressState}")
|
|
731
|
+
return {"status": "unknown", "raw_response": status_response}
|
|
732
|
+
|
|
733
|
+
except Exception as e:
|
|
734
|
+
print("🚨 Error during metadata sync:", str(e))
|
|
735
|
+
return {"status": "exception", "error": str(e)}
|
|
736
|
+
|
|
737
|
+
"""
|
|
738
|
+
# How to call function ( lakehouse_metadata_sync )
|
|
739
|
+
workspace_id = spark.conf.get("trident.workspace.id")
|
|
740
|
+
lakehouse_id = spark.conf.get("trident.lakehouse.id")
|
|
741
|
+
|
|
742
|
+
# Call the function
|
|
743
|
+
result = lakehouse_metadata_sync(workspace_id, lakehouse_id)
|
|
744
|
+
display(result)
|
|
745
|
+
"""
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
# <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< Archieve
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
## <<<<<<<<<<<<<<<< QA_CheckUtil_01
|
|
757
|
+
def QA_CheckUtil_01(
|
|
758
|
+
source_df: DataFrame,
|
|
759
|
+
qa_df: DataFrame
|
|
760
|
+
) -> DataFrame:
|
|
761
|
+
|
|
762
|
+
spark = source_df.sparkSession
|
|
763
|
+
qa_rows: List[tuple] = []
|
|
764
|
+
|
|
765
|
+
def calc_diff(src: Optional[Union[int, float]], qa: Optional[Union[int, float]]) -> Optional[float]:
|
|
766
|
+
if src is None or qa is None:
|
|
767
|
+
return None
|
|
768
|
+
return float(src) - float(qa)
|
|
769
|
+
|
|
770
|
+
# Row count
|
|
771
|
+
src_count = float(source_df.count())
|
|
772
|
+
qa_count = float(qa_df.count())
|
|
773
|
+
qa_rows.append((
|
|
774
|
+
"ROW_COUNT",
|
|
775
|
+
"row_count",
|
|
776
|
+
None,
|
|
777
|
+
src_count,
|
|
778
|
+
qa_count,
|
|
779
|
+
calc_diff(src_count, qa_count),
|
|
780
|
+
src_count == qa_count
|
|
781
|
+
))
|
|
782
|
+
|
|
783
|
+
# Null check
|
|
784
|
+
common_cols = set(source_df.columns).intersection(set(qa_df.columns))
|
|
785
|
+
for col in common_cols:
|
|
786
|
+
src_nulls = float(source_df.filter(F.col(col).isNull()).count())
|
|
787
|
+
qa_nulls = float(qa_df.filter(F.col(col).isNull()).count())
|
|
788
|
+
qa_rows.append((
|
|
789
|
+
"NULL_CHECK",
|
|
790
|
+
"null_count",
|
|
791
|
+
col,
|
|
792
|
+
src_nulls,
|
|
793
|
+
qa_nulls,
|
|
794
|
+
calc_diff(src_nulls, qa_nulls),
|
|
795
|
+
src_nulls == qa_nulls
|
|
796
|
+
))
|
|
797
|
+
|
|
798
|
+
# Aggregation check (SUM for amount)
|
|
799
|
+
if "amount" in source_df.columns and "amount" in qa_df.columns:
|
|
800
|
+
src_sum = float(source_df.select(F.sum("amount")).collect()[0][0] or 0.0)
|
|
801
|
+
qa_sum = float(qa_df.select(F.sum("amount")).collect()[0][0] or 0.0)
|
|
802
|
+
qa_rows.append((
|
|
803
|
+
"AGG_CHECK",
|
|
804
|
+
"sum",
|
|
805
|
+
"amount",
|
|
806
|
+
src_sum,
|
|
807
|
+
qa_sum,
|
|
808
|
+
calc_diff(src_sum, qa_sum),
|
|
809
|
+
src_sum == qa_sum
|
|
810
|
+
))
|
|
811
|
+
|
|
812
|
+
# Duplicate check on id column
|
|
813
|
+
if "id" in source_df.columns and "id" in qa_df.columns:
|
|
814
|
+
src_dupes = float(source_df.count() - source_df.select("id").distinct().count())
|
|
815
|
+
qa_dupes = float(qa_df.count() - qa_df.select("id").distinct().count())
|
|
816
|
+
qa_rows.append((
|
|
817
|
+
"DUPLICATE_CHECK",
|
|
818
|
+
"duplicate_id",
|
|
819
|
+
"id",
|
|
820
|
+
src_dupes,
|
|
821
|
+
qa_dupes,
|
|
822
|
+
calc_diff(src_dupes, qa_dupes),
|
|
823
|
+
src_dupes == qa_dupes
|
|
824
|
+
))
|
|
825
|
+
|
|
826
|
+
# Create final QA DataFrame
|
|
827
|
+
qa_df_result = spark.createDataFrame(
|
|
828
|
+
qa_rows,
|
|
829
|
+
[
|
|
830
|
+
"check_type",
|
|
831
|
+
"check_name",
|
|
832
|
+
"column_name",
|
|
833
|
+
"source_value",
|
|
834
|
+
"qa_value",
|
|
835
|
+
"diff",
|
|
836
|
+
"match"
|
|
837
|
+
]
|
|
838
|
+
)
|
|
839
|
+
return qa_df_result
|
|
840
|
+
|
|
841
|
+
## >>>>>>>>>>>>>>>>>> QA_CheckUtil_01
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
## <<<<<<<<<<<<<<<< send_email_no_attachment_02
|
|
845
|
+
|
|
846
|
+
def send_email_no_attachment_02(p, endpoint_url=None, access_token=None):
|
|
847
|
+
"""
|
|
848
|
+
Send email via POST API without attachment.
|
|
849
|
+
|
|
850
|
+
Parameters:
|
|
851
|
+
p (dict): {
|
|
852
|
+
"to": str | list[str],
|
|
853
|
+
"subject": str,
|
|
854
|
+
"body": str,
|
|
855
|
+
"headers": dict (optional),
|
|
856
|
+
"timeout": int (optional)
|
|
857
|
+
}
|
|
858
|
+
endpoint_url (str): API endpoint for sending mail
|
|
859
|
+
access_token (str): Bearer token
|
|
860
|
+
|
|
861
|
+
Returns:
|
|
862
|
+
(status_code, response_text) or (None, error_message)
|
|
863
|
+
"""
|
|
864
|
+
if not endpoint_url:
|
|
865
|
+
raise ValueError("endpoint_url is required")
|
|
866
|
+
if not access_token:
|
|
867
|
+
raise ValueError("access_token is required")
|
|
868
|
+
|
|
869
|
+
missing = [k for k in ("to", "subject", "body") if not p.get(k)]
|
|
870
|
+
if missing:
|
|
871
|
+
return None, f"Missing required fields: {', '.join(missing)}"
|
|
872
|
+
|
|
873
|
+
payload = {
|
|
874
|
+
"to": ";".join(p["to"]) if isinstance(p["to"], list) else p["to"],
|
|
875
|
+
"subject": p["subject"],
|
|
876
|
+
"body": p["body"],
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
headers = {
|
|
880
|
+
"Authorization": f"Bearer {access_token}",
|
|
881
|
+
**p.get("headers", {})
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
try:
|
|
885
|
+
resp = requests.post(
|
|
886
|
+
endpoint_url,
|
|
887
|
+
json=payload,
|
|
888
|
+
headers=headers,
|
|
889
|
+
timeout=p.get("timeout", 15)
|
|
890
|
+
)
|
|
891
|
+
success_codes = (200, 201, 202)
|
|
892
|
+
return resp.status_code, resp.text
|
|
893
|
+
except requests.RequestException as e:
|
|
894
|
+
return None, str(e)
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
## <<<<<<<<<<<<<<<< send_email_no_attachment_01
|
|
898
|
+
|
|
899
|
+
def send_email_no_attachment_01(
|
|
900
|
+
body : Optional[str] = None,
|
|
901
|
+
endpoint_url: Optional[str] = None,
|
|
902
|
+
access_token: Optional[str] = None,
|
|
903
|
+
subject : Optional[str] = None,
|
|
904
|
+
recipients : Optional[List[str]] = None,
|
|
905
|
+
headers : Optional[Dict[str, str]] = None,
|
|
906
|
+
timeout : int = 15,
|
|
907
|
+
tz_name : str = "America/Los_Angeles"
|
|
908
|
+
) -> Tuple[Optional[int], str]:
|
|
909
|
+
"""
|
|
910
|
+
Send email via POST API without attachments. All parameters are optional.
|
|
911
|
+
If required information is missing, returns a descriptive message instead of sending.
|
|
912
|
+
"""
|
|
913
|
+
# If endpoint or token not provided, skip sending
|
|
914
|
+
if not endpoint_url or not access_token:
|
|
915
|
+
return None, "Skipping send: endpoint_url or access_token not provided."
|
|
916
|
+
|
|
917
|
+
# Determine recipients
|
|
918
|
+
final_recipients = recipients
|
|
919
|
+
if not final_recipients:
|
|
920
|
+
return None, "Skipping send: no recipients provided."
|
|
921
|
+
|
|
922
|
+
# Determine body content
|
|
923
|
+
final_body = body
|
|
924
|
+
if not final_body:
|
|
925
|
+
return None, "Skipping send: no body content provided."
|
|
926
|
+
|
|
927
|
+
payload = {
|
|
928
|
+
"to": ";".join(final_recipients) if isinstance(final_recipients, list) else final_recipients,
|
|
929
|
+
"subject": subject or "",
|
|
930
|
+
"body": final_body
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
request_headers = {
|
|
934
|
+
"Authorization": f"Bearer {access_token}",
|
|
935
|
+
**(headers or {})
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
try:
|
|
939
|
+
resp = requests.post(
|
|
940
|
+
endpoint_url,
|
|
941
|
+
json=payload,
|
|
942
|
+
headers=request_headers,
|
|
943
|
+
timeout=timeout
|
|
944
|
+
)
|
|
945
|
+
if resp.status_code in (200, 201, 202):
|
|
946
|
+
return resp.status_code, resp.text
|
|
947
|
+
else:
|
|
948
|
+
return resp.status_code, f"Failed: {resp.text}"
|
|
949
|
+
except requests.RequestException as e:
|
|
950
|
+
return None, str(e)
|
|
951
|
+
|
|
952
|
+
"""
|
|
953
|
+
# How to call function
|
|
954
|
+
|
|
955
|
+
apiid = spark.conf.get("spark.scopeid")
|
|
956
|
+
scope = f"api://{apiid}/.default"
|
|
957
|
+
access_token = credential.get_token(scope).token
|
|
958
|
+
endpoint_base = "https://fdne-inframail-logicapp01.azurewebsites.net:443/api/fdne-infra-appmail-sender"
|
|
959
|
+
endpoint_url = f"{endpoint_base}/triggers/When_a_HTTP_request_is_received/invoke?api-version=2022-05-01"
|
|
960
|
+
|
|
961
|
+
status, response = send_email_no_attachment_01(
|
|
962
|
+
body=markdown,
|
|
963
|
+
recipients=recipients,
|
|
964
|
+
endpoint_url=endpoint_url,
|
|
965
|
+
access_token=access_token,
|
|
966
|
+
subject=subject)
|
|
967
|
+
"""
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .CPILake_Utils import hash_function, \
|
|
2
|
+
send_email_via_http, \
|
|
3
|
+
_df_to_html_table, \
|
|
4
|
+
send_email_no_attachment, \
|
|
5
|
+
QA_CheckUtil, \
|
|
6
|
+
create_lakehouse_shortcuts, \
|
|
7
|
+
create_lakehouse_shortcuts_01, \
|
|
8
|
+
create_adls_shortcuts, \
|
|
9
|
+
create_adls_shortcuts_01, \
|
|
10
|
+
lakehouse_metadata_sync
|
|
11
|
+
|
|
12
|
+
__all__ = [ "hash_function", "send_email_via_http", "_df_to_html_table", "send_email_no_attachment",
|
|
13
|
+
"QA_CheckUtil", "create_lakehouse_shortcuts",
|
|
14
|
+
"create_lakehouse_shortcuts_01", "create_adls_shortcuts", "create_adls_shortcuts_01",
|
|
15
|
+
"lakehouse_metadata_sync"]
|
|
16
|
+
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: CPILake_Utils
|
|
3
|
+
Version: 11.0.0
|
|
4
|
+
Summary: Reusable common utility functions including email and hash functions
|
|
5
|
+
Author: Abhilash Ibrahimpatnam
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: requests
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# CPILake_Utils
|
|
12
|
+
|
|
13
|
+
## Introduction
|
|
14
|
+
|
|
15
|
+
A lightweight internal Python library providing reusable email-sending utilities and helper functions for the Fabric Notebook.
|
|
16
|
+
|
|
17
|
+
This package is distributed as a Python wheel (`.whl`) and can be used across notebooks, scripts, or internal pipelines.
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- Send emails via a simple POST API
|
|
22
|
+
- Supports multiple recipients
|
|
23
|
+
- Handles HTTP 200, 201, 202 as successful response codes
|
|
24
|
+
- Minimal dependencies (`requests` only)
|
|
25
|
+
- Includes a pure-Python hash function for alphanumeric strings
|
|
26
|
+
|
|
27
|
+
## Package Structure
|
|
28
|
+
|
|
29
|
+
common_functions/ <- root folder
|
|
30
|
+
├─ CPILake_Utils/ <- package folder
|
|
31
|
+
│ ├─ __init__.py <- import all functions here
|
|
32
|
+
│ └─ CPILake_Utils.py <- all utilities go here (email, hash, etc.)
|
|
33
|
+
├─ pyproject.toml
|
|
34
|
+
└─ README.md
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
# Install the wheel file directly
|
|
40
|
+
pip install CPILake-Utils-1.0.0-py3-none-any.whl
|
|
41
|
+
|
|
42
|
+
# OR
|
|
43
|
+
pip install build # Install the build tool
|
|
44
|
+
python -m build # Creates the .whl file in dist/
|
|
45
|
+
twine upload dist/CPILake-Utils-1.0.0-py3-none-any.whl
|
|
46
|
+
|
|
47
|
+
# Usage Example
|
|
48
|
+
|
|
49
|
+
pip install CPILake-Utils==1.0.0
|
|
50
|
+
|
|
51
|
+
# OR
|
|
52
|
+
from CPILake-Utils import send_email_no_attachment, hash_function, send_email_no_attachment_01
|
|
53
|
+
|
|
54
|
+
print(hash_function("test"))
|
|
55
|
+
|
|
56
|
+
status, response = send_email_no_attachment(
|
|
57
|
+
{"to": ["user@domain.com"], "subject": "Hi", "body": "Test email"},
|
|
58
|
+
endpoint_url="https://your-endpoint",
|
|
59
|
+
access_token="YOUR_ACCESS_TOKEN"
|
|
60
|
+
)
|
|
61
|
+
print(status, response)
|
|
62
|
+
|
|
63
|
+
print(status) # 200 or None
|
|
64
|
+
print(response) # Response text or skip response
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# Upload to PyPI (Token-based Authentication)
|
|
68
|
+
-- upload it to PyPI using an API token instead of username/password
|
|
69
|
+
|
|
70
|
+
# Replace $env:PYPI_API_TOKEN with your PyPI API token (PowerShell)
|
|
71
|
+
twine upload dist/* -u __token__ -p $env:PYPI_API_TOKEN
|
|
72
|
+
|
|
73
|
+
This securely uploads all files in the dist/ folder, After upload, your package will be available at:
|
|
74
|
+
https://pypi.org/project/CPILake-Utils/1.0.0/
|
|
75
|
+
|
|
76
|
+
## Author
|
|
77
|
+
|
|
78
|
+
Abhilash Ibrahimpatnam
|
|
79
|
+
|
|
80
|
+
## Summary
|
|
81
|
+
|
|
82
|
+
This README includes:
|
|
83
|
+
|
|
84
|
+
1. Clean introduction and feature list
|
|
85
|
+
2. Clear package structure explanation
|
|
86
|
+
3. Local installation and build instructions
|
|
87
|
+
4. PyPI install instructions
|
|
88
|
+
5. Usage examples with both functions
|
|
89
|
+
6. Token-based upload instructions for PyPI
|
|
90
|
+
7. Dependencies and author info
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
## Clean old build artifacts
|
|
94
|
+
Remove-Item -Recurse -Force build, dist
|
|
95
|
+
Remove-Item -Recurse -Force *.egg-info
|
|
96
|
+
|
|
97
|
+
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
CPILake_Utils/CPILake_Utils.py
|
|
4
|
+
CPILake_Utils/__init__.py
|
|
5
|
+
CPILake_Utils.egg-info/PKG-INFO
|
|
6
|
+
CPILake_Utils.egg-info/SOURCES.txt
|
|
7
|
+
CPILake_Utils.egg-info/dependency_links.txt
|
|
8
|
+
CPILake_Utils.egg-info/requires.txt
|
|
9
|
+
CPILake_Utils.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
CPILake_Utils
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: CPILake_Utils
|
|
3
|
+
Version: 11.0.0
|
|
4
|
+
Summary: Reusable common utility functions including email and hash functions
|
|
5
|
+
Author: Abhilash Ibrahimpatnam
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: requests
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# CPILake_Utils
|
|
12
|
+
|
|
13
|
+
## Introduction
|
|
14
|
+
|
|
15
|
+
A lightweight internal Python library providing reusable email-sending utilities and helper functions for the Fabric Notebook.
|
|
16
|
+
|
|
17
|
+
This package is distributed as a Python wheel (`.whl`) and can be used across notebooks, scripts, or internal pipelines.
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- Send emails via a simple POST API
|
|
22
|
+
- Supports multiple recipients
|
|
23
|
+
- Handles HTTP 200, 201, 202 as successful response codes
|
|
24
|
+
- Minimal dependencies (`requests` only)
|
|
25
|
+
- Includes a pure-Python hash function for alphanumeric strings
|
|
26
|
+
|
|
27
|
+
## Package Structure
|
|
28
|
+
|
|
29
|
+
common_functions/ <- root folder
|
|
30
|
+
├─ CPILake_Utils/ <- package folder
|
|
31
|
+
│ ├─ __init__.py <- import all functions here
|
|
32
|
+
│ └─ CPILake_Utils.py <- all utilities go here (email, hash, etc.)
|
|
33
|
+
├─ pyproject.toml
|
|
34
|
+
└─ README.md
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
# Install the wheel file directly
|
|
40
|
+
pip install CPILake-Utils-1.0.0-py3-none-any.whl
|
|
41
|
+
|
|
42
|
+
# OR
|
|
43
|
+
pip install build # Install the build tool
|
|
44
|
+
python -m build # Creates the .whl file in dist/
|
|
45
|
+
twine upload dist/CPILake-Utils-1.0.0-py3-none-any.whl
|
|
46
|
+
|
|
47
|
+
# Usage Example
|
|
48
|
+
|
|
49
|
+
pip install CPILake-Utils==1.0.0
|
|
50
|
+
|
|
51
|
+
# OR
|
|
52
|
+
from CPILake-Utils import send_email_no_attachment, hash_function, send_email_no_attachment_01
|
|
53
|
+
|
|
54
|
+
print(hash_function("test"))
|
|
55
|
+
|
|
56
|
+
status, response = send_email_no_attachment(
|
|
57
|
+
{"to": ["user@domain.com"], "subject": "Hi", "body": "Test email"},
|
|
58
|
+
endpoint_url="https://your-endpoint",
|
|
59
|
+
access_token="YOUR_ACCESS_TOKEN"
|
|
60
|
+
)
|
|
61
|
+
print(status, response)
|
|
62
|
+
|
|
63
|
+
print(status) # 200 or None
|
|
64
|
+
print(response) # Response text or skip response
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# Upload to PyPI (Token-based Authentication)
|
|
68
|
+
-- upload it to PyPI using an API token instead of username/password
|
|
69
|
+
|
|
70
|
+
# Replace $env:PYPI_API_TOKEN with your PyPI API token (PowerShell)
|
|
71
|
+
twine upload dist/* -u __token__ -p $env:PYPI_API_TOKEN
|
|
72
|
+
|
|
73
|
+
This securely uploads all files in the dist/ folder, After upload, your package will be available at:
|
|
74
|
+
https://pypi.org/project/CPILake-Utils/1.0.0/
|
|
75
|
+
|
|
76
|
+
## Author
|
|
77
|
+
|
|
78
|
+
Abhilash Ibrahimpatnam
|
|
79
|
+
|
|
80
|
+
## Summary
|
|
81
|
+
|
|
82
|
+
This README includes:
|
|
83
|
+
|
|
84
|
+
1. Clean introduction and feature list
|
|
85
|
+
2. Clear package structure explanation
|
|
86
|
+
3. Local installation and build instructions
|
|
87
|
+
4. PyPI install instructions
|
|
88
|
+
5. Usage examples with both functions
|
|
89
|
+
6. Token-based upload instructions for PyPI
|
|
90
|
+
7. Dependencies and author info
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
## Clean old build artifacts
|
|
94
|
+
Remove-Item -Recurse -Force build, dist
|
|
95
|
+
Remove-Item -Recurse -Force *.egg-info
|
|
96
|
+
|
|
97
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
|
|
2
|
+
# CPILake_Utils
|
|
3
|
+
|
|
4
|
+
## Introduction
|
|
5
|
+
|
|
6
|
+
A lightweight internal Python library providing reusable email-sending utilities and helper functions for the Fabric Notebook.
|
|
7
|
+
|
|
8
|
+
This package is distributed as a Python wheel (`.whl`) and can be used across notebooks, scripts, or internal pipelines.
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- Send emails via a simple POST API
|
|
13
|
+
- Supports multiple recipients
|
|
14
|
+
- Handles HTTP 200, 201, 202 as successful response codes
|
|
15
|
+
- Minimal dependencies (`requests` only)
|
|
16
|
+
- Includes a pure-Python hash function for alphanumeric strings
|
|
17
|
+
|
|
18
|
+
## Package Structure
|
|
19
|
+
|
|
20
|
+
common_functions/ <- root folder
|
|
21
|
+
├─ CPILake_Utils/ <- package folder
|
|
22
|
+
│ ├─ __init__.py <- import all functions here
|
|
23
|
+
│ └─ CPILake_Utils.py <- all utilities go here (email, hash, etc.)
|
|
24
|
+
├─ pyproject.toml
|
|
25
|
+
└─ README.md
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
# Install the wheel file directly
|
|
31
|
+
pip install CPILake-Utils-1.0.0-py3-none-any.whl
|
|
32
|
+
|
|
33
|
+
# OR
|
|
34
|
+
pip install build # Install the build tool
|
|
35
|
+
python -m build # Creates the .whl file in dist/
|
|
36
|
+
twine upload dist/CPILake-Utils-1.0.0-py3-none-any.whl
|
|
37
|
+
|
|
38
|
+
# Usage Example
|
|
39
|
+
|
|
40
|
+
pip install CPILake-Utils==1.0.0
|
|
41
|
+
|
|
42
|
+
# OR
|
|
43
|
+
from CPILake-Utils import send_email_no_attachment, hash_function, send_email_no_attachment_01
|
|
44
|
+
|
|
45
|
+
print(hash_function("test"))
|
|
46
|
+
|
|
47
|
+
status, response = send_email_no_attachment(
|
|
48
|
+
{"to": ["user@domain.com"], "subject": "Hi", "body": "Test email"},
|
|
49
|
+
endpoint_url="https://your-endpoint",
|
|
50
|
+
access_token="YOUR_ACCESS_TOKEN"
|
|
51
|
+
)
|
|
52
|
+
print(status, response)
|
|
53
|
+
|
|
54
|
+
print(status) # 200 or None
|
|
55
|
+
print(response) # Response text or skip response
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Upload to PyPI (Token-based Authentication)
|
|
59
|
+
-- upload it to PyPI using an API token instead of username/password
|
|
60
|
+
|
|
61
|
+
# Replace $env:PYPI_API_TOKEN with your PyPI API token (PowerShell)
|
|
62
|
+
twine upload dist/* -u __token__ -p $env:PYPI_API_TOKEN
|
|
63
|
+
|
|
64
|
+
This securely uploads all files in the dist/ folder, After upload, your package will be available at:
|
|
65
|
+
https://pypi.org/project/CPILake-Utils/1.0.0/
|
|
66
|
+
|
|
67
|
+
## Author
|
|
68
|
+
|
|
69
|
+
Abhilash Ibrahimpatnam
|
|
70
|
+
|
|
71
|
+
## Summary
|
|
72
|
+
|
|
73
|
+
This README includes:
|
|
74
|
+
|
|
75
|
+
1. Clean introduction and feature list
|
|
76
|
+
2. Clear package structure explanation
|
|
77
|
+
3. Local installation and build instructions
|
|
78
|
+
4. PyPI install instructions
|
|
79
|
+
5. Usage examples with both functions
|
|
80
|
+
6. Token-based upload instructions for PyPI
|
|
81
|
+
7. Dependencies and author info
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
## Clean old build artifacts
|
|
85
|
+
Remove-Item -Recurse -Force build, dist
|
|
86
|
+
Remove-Item -Recurse -Force *.egg-info
|
|
87
|
+
|
|
88
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "CPILake_Utils"
|
|
7
|
+
version = "11.0.0"
|
|
8
|
+
description = "Reusable common utility functions including email and hash functions"
|
|
9
|
+
authors = [{ name="Abhilash Ibrahimpatnam" }]
|
|
10
|
+
dependencies = ["requests"]
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
requires-python = ">=3.8"
|
|
13
|
+
|
|
14
|
+
[tool.setuptools]
|
|
15
|
+
packages = ["CPILake_Utils"]
|