fdsempy 1.3.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.
fdsempy-1.3.0/PKG-INFO ADDED
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: fdsempy
3
+ Version: 1.3.0
4
+ Summary: Fairday Sempy - semantic model utilities
5
+ Author: Peter Johnson - Fairday Research Limited
6
+ Requires-Dist: semantic-link-labs
7
+ Requires-Dist: pandas
8
+ Requires-Dist: json
9
+ Requires-Dist: requests
10
+ Requires-Dist: sempy
11
+ Requires-Dist: notebookutils
12
+ Dynamic: author
13
+ Dynamic: requires-dist
14
+ Dynamic: summary
File without changes
@@ -0,0 +1,376 @@
1
+ #######################################
2
+ # fdsempy.py
3
+ # (c)2025 Fairday Research Limited
4
+ # Dependencies: semantic-link-labs
5
+ #######################################
6
+
7
+ ### imports ###
8
+ import pandas as pd
9
+ import json
10
+ import requests
11
+ from sempy.fabric import list_tables, list_columns, list_workspaces
12
+ from sempy_labs import deploy_semantic_model, refresh_semantic_model
13
+ from sempy_labs.directlake import update_direct_lake_model_connection
14
+ from sempy_labs.environment import list_environments
15
+ from sempy_labs.report import clone_report
16
+ from sempy_labs.tom import connect_semantic_model
17
+ from sempy_labs.warehouse import get_warehouse_columns
18
+ from notebookutils.notebook import list, getDefinition, updateDefinition, update, create
19
+ from notebookutils.lakehouse import get as lhGet
20
+ from notebookutils.credentials import getToken
21
+
22
+ ########################################
23
+ # Helper functions
24
+ ########################################
25
+ def _wh_convert_dtype(input_data_type: str) -> str:
26
+
27
+ if not input_data_type:
28
+ return None
29
+
30
+ input_data_type = input_data_type.lower()
31
+
32
+ data_type_mapping = {
33
+ "varchar": "String",
34
+ "int": "Int64",
35
+ "tinyint": "Int64",
36
+ "smallint": "Int64",
37
+ "bigint": "Int64",
38
+ "boolean": "Boolean",
39
+ "timestamp": "DateTime",
40
+ "date": "DateTime",
41
+ "decimal": "Double",
42
+ "numeric": "Double",
43
+ "double": "Double",
44
+ "float": "Double",
45
+ "binary": "Boolean",
46
+ "bit": "Boolean",
47
+ "long": "Int64",
48
+ }
49
+
50
+ if input_data_type.startswith("decimal"):
51
+ return "Double"
52
+ elif input_data_type.startswith("datetime"):
53
+ return "DateTime"
54
+
55
+ return data_type_mapping.get(input_data_type)
56
+
57
+ ########################################
58
+ # Version
59
+ ########################################
60
+ def libver():
61
+ print("fdsempy.py (c)2025 Fairday Research Ltd\nVersion 1.3.0 - July 2026")
62
+
63
+ ########################################
64
+ # Add warehouse table to semantic model
65
+ ########################################
66
+ def sempy_add_warehouse_table(workspace: str, dataset: str, table_name: str, warehouse: str, schema: str, source: str):
67
+ # get meta data of warehouse columns
68
+ meta_df = get_warehouse_columns(warehouse=warehouse, workspace=workspace)
69
+ # filter on target schema & table
70
+ tbl_meta_df = meta_df[(meta_df['Schema'] == schema) & (meta_df['Table Name'] == source)]
71
+
72
+ with connect_semantic_model(dataset=dataset, readonly=False, workspace=workspace) as tom:
73
+ # create table
74
+ tom.add_table(name=table_name)
75
+ tom.add_entity_partition(table_name=table_name, entity_name=source, schema_name=schema)
76
+
77
+ # iterate through columns and convert SQ data types to SM data types
78
+ # then add the columns to the new table
79
+ count = 0
80
+ for _, row in tbl_meta_df.iterrows():
81
+ cname = row["Column Name"]
82
+ src_dtype = row["Data Type"]
83
+ tom_dtype = _wh_convert_dtype(src_dtype)
84
+ tom.add_data_column(
85
+ table_name=table_name,
86
+ column_name=cname,
87
+ source_column=cname,
88
+ data_type=tom_dtype,
89
+ )
90
+ count+=1
91
+ print(f"\U00002705 Columns added: {count}")
92
+
93
+ ########################################
94
+ # Format semantic model tables
95
+ ########################################
96
+ def sempy_format_tables(workspace: str, dataset: str):
97
+
98
+ # read tables & columns from model
99
+ tables = list_tables(dataset=dataset, workspace=workspace)
100
+
101
+ # read tables from mapping file and join to sempy list
102
+ fmtmap = pd.read_csv("/lakehouse/default/Files/fdsempymap.csv").set_index("Name")
103
+ fmttabs = tables.join(fmtmap, on="Name", how="left", lsuffix="_sm", rsuffix="_map")
104
+
105
+ with connect_semantic_model(dataset=dataset, readonly=False, workspace=workspace) as tom:
106
+ count = 0
107
+ # rename tables
108
+ for _, row in fmttabs.iterrows():
109
+ tablename = row["Name"]
110
+ # rename table
111
+ if not pd.isna(row["Model Name"]):
112
+ print(f"Changing {tablename} to {row['Model Name']}")
113
+ tablename = row["Name"]
114
+ tab = tom.model.Tables[tablename]
115
+ tab.Name = row["Model Name"]
116
+ tom.add_changed_property(tab,"Name")
117
+ count+=1
118
+ if not pd.isna(row["Description_map"]):
119
+ tab.Description = row["Description_map"]
120
+ tom.add_changed_property(tab,"Description")
121
+ print(f"\U00002705 Formatted tables: {count}")
122
+
123
+ ########################################
124
+ # Format semantic model columns
125
+ ########################################
126
+ def sempy_format_columns(workspace: str, dataset: str, table: str):
127
+
128
+ cols = list_columns(dataset=dataset, workspace=workspace)
129
+ hide_n = 0
130
+ date_n = 0
131
+ dp6_n = 0
132
+ dp4_n = 0
133
+ int_n = 0
134
+ with connect_semantic_model(dataset=dataset, readonly=False, workspace=workspace) as tom:
135
+ print("Updating columns")
136
+ # go through columns making changes
137
+ for _, row in cols.iterrows():
138
+ if (table == None) | (row["Table Name"] == table):
139
+ # hide columns
140
+ if row["Column Name"].startswith("scd_") | row["Column Name"].startswith("nk_"): # hide scd/nk columns
141
+ #print(f"Set {row['Table Name']}.{row['Column Name']} to hidden")
142
+ tom.update_column(table_name=row["Table Name"], column_name=row["Column Name"], hidden=True)
143
+ hide_n+=1
144
+ if row["Column Name"].endswith("id") & (row["Data Type"] == "Int64"): # hide numeric id columns
145
+ #print(f"Set {row['Table Name']}.{row['Column Name']} to hidden")
146
+ tom.update_column(table_name=row["Table Name"], column_name=row["Column Name"], hidden=True)
147
+ hide_n+=1
148
+ # set date format
149
+ if row["Data Type"].startswith("Date") & (not row["Column Name"].startswith("scd")): # change non scd date formats
150
+ #print(f"Set {row['Table Name']}.{row['Column Name']} to short date")
151
+ tom.update_column(table_name=row["Table Name"], column_name=row["Column Name"], format_string='Short Date')
152
+ date_n+=1
153
+ if (row["Data Type"] == "Double") & ("(6 dp)" in row['Column Name']): # decimal type add comma, set dps
154
+ # set thousands separator (comma) and six dps
155
+ #print(f"Set {row['Table Name']}.{row['Column Name']} to #,0.000000")
156
+ tom.update_column(table_name=row["Table Name"], column_name=row["Column Name"], format_string='#,0.000000')
157
+ dp6_n+=1
158
+ elif row["Data Type"] == "Double": # decimal type add comma, set dps
159
+ # set thousands separator (comma) and four dps
160
+ #print(f"Set {row['Table Name']}.{row['Column Name']} to #,0.0000")
161
+ tom.update_column(table_name=row["Table Name"], column_name=row["Column Name"], format_string='#,0.0000')
162
+ dp4_n+=1
163
+ if row["Data Type"].startswith("Int") & (not row["Column Name"].endswith("id")) & (not row["Table Name"].startswith("Date")) & (not row["Table Name"].startswith("dim_date")): # int type addcomma
164
+ # set thousands separator (comma) and four dps
165
+ #print(f"Set {row['Table Name']}.{row['Column Name']} to #,0")
166
+ tom.update_column(table_name=row["Table Name"], column_name=row["Column Name"], format_string='#,0')
167
+ int_n+=1
168
+ print(f"\U00002705 Hidden: {hide_n}, Date formatted: {date_n}, 6dp formatted {dp6_n}, 4dp formatted: {dp4_n}, int formatted: {int_n}")
169
+
170
+ ########################################
171
+ # Deploy Notebooks to Workspace
172
+ ########################################
173
+ def sempy_notebooks_deploy(src_workspace_id: str, tgt_workspace_id: str, core_notebooks: list, util_notebooks: list=[]):
174
+ ## get the id of the DWH environment in the target workspace (assume that it exists)
175
+ dwh_env = 'DWH'
176
+ env_df = list_environments(tgt_workspace_id) # get all the environments in the target workspace
177
+ tgt_dwh_env_id = env_df.loc[env_df['Environment Name']==dwh_env]['Environment Id'].values[0] # get the id of the first one that matches DWH
178
+
179
+ # create a dict in the expected json format
180
+ tgt_env_ws_dict = dict(environmentId=tgt_dwh_env_id, workspaceId=tgt_workspace_id)
181
+
182
+ # feedback to user
183
+ aw_df = list_workspaces()
184
+
185
+ src_name = aw_df[aw_df['Id'] == src_workspace_id]['Name'].values[0]
186
+ tgt_name = aw_df[aw_df['Id'] == tgt_workspace_id]['Name'].values[0]
187
+
188
+ print(f"\U00002139 Cloning notebooks from \033[1m{src_name}\033[0m to \033[1m{tgt_name}\033[0m")
189
+ for nb in core_notebooks:
190
+ print(f"\U000025AA {nb}")
191
+ if "_dev_" in tgt_name:
192
+ for nb in util_notebooks:
193
+ print(f"\U000025AA {nb}")
194
+
195
+ count = 0
196
+ # get a list of artifacts in target
197
+ print(f"Building lists from target workspace...")
198
+ namelist = []
199
+ artifacts = list(tgt_workspace_id)
200
+ for artifact in artifacts:
201
+ if (artifact.type == 'Notebook') & ((artifact.displayName in core_notebooks) | (("_dev_" in tgt_name) & (artifact.displayName in util_notebooks))):
202
+ namelist.append(artifact.displayName)
203
+
204
+ # Loop through notebook artifacts in source:
205
+ artifacts = list(src_workspace_id)
206
+ for artifact in artifacts:
207
+ json_hacked = False
208
+ if (artifact.type == 'Notebook') & ((artifact.displayName in core_notebooks) | (("_dev_" in tgt_name) & (artifact.displayName in util_notebooks))):
209
+ print(f"\U000025B6 Processing {artifact.displayName}")
210
+ nb_name = artifact.displayName # grab notebook name
211
+ print(f"\U0000231B getting definition of {artifact.displayName} from {src_name}...")
212
+ content = getDefinition(name=nb_name,workspaceId=src_workspace_id) # get notebook ipynb definition
213
+
214
+ ### hack of target nb json ###
215
+ # turn content into json
216
+ json_def = json.loads(content)
217
+
218
+ # if content contains a default lakehouse name
219
+ if json_def.get("metadata", {}).get("dependencies", {}).get("lakehouse", {}).get("default_lakehouse_name", {}):
220
+ # grab the default lakehouse name
221
+ src_nb_lh_name = json_def.get("metadata", {}).get("dependencies", {}).get("lakehouse", {}).get("default_lakehouse_name", {})
222
+
223
+ # then get id of tgt lakehouse id with same name as default lakehouse in src notebook
224
+ tgt_lh = lhGet(name=src_nb_lh_name, workspaceId=tgt_workspace_id)
225
+ tgt_def_lh_id = tgt_lh.id
226
+
227
+ # then create a dict containing tgt lakehouse id and workspace id
228
+ tgt_def_lh_dict_list = [dict(id=tgt_def_lh_id)]
229
+
230
+ # if the known_lakehouses key is populated in the json
231
+ if json_def.get("metadata", {}).get("dependencies", {}).get("lakehouse", {}).get("known_lakehouses", {}):
232
+ print(f"\U0000231B modifying [lakehouses] json key...")
233
+ json_def["metadata"]["dependencies"]["lakehouse"]["known_lakehouses"] = tgt_def_lh_dict_list
234
+ json_def["metadata"]["dependencies"]["lakehouse"]["default_lakehouse"] = tgt_def_lh_id
235
+ json_def["metadata"]["dependencies"]["lakehouse"]["default_lakehouse_workspace_id"] = workspaceId=tgt_workspace_id
236
+ #print(f"\U0000267B new: %s" % (json_def["metadata"]["dependencies"]["lakehouse"]))
237
+ json_hacked = True
238
+
239
+ # if the environment is populated in the json
240
+ if json_def.get("metadata", {}).get("dependencies", {}).get("environment", {}):
241
+ print("\U0000231B modifying [environment] json key...")
242
+ json_def["metadata"]["dependencies"]["environment"] = tgt_env_ws_dict
243
+ #print(f"\U0000267B new: %s" % (json_def["metadata"]["dependencies"]["environment"]))
244
+ json_hacked = True
245
+
246
+ if json_hacked:
247
+ # save this definition to a string. Otherwise we'll use the original content
248
+ content = json.dumps(json_def)
249
+
250
+ # check if notebook exists in target (from artifact list)
251
+ if nb_name in namelist: # if your name's not on the list you ain't getting in mate
252
+ print(f"\U0000231B updating {nb_name} in {tgt_name}")
253
+ # if it does exist update the content:
254
+ updateDefinition(name=nb_name, content=content, workspaceId=tgt_workspace_id)
255
+ # then update the properties
256
+ update(name=nb_name, newName=nb_name, description=artifact.description, workspaceId=tgt_workspace_id)
257
+ count+=1
258
+ else:
259
+ # if notebook doesn't exist create it
260
+ print(f"\U0000231B creating {nb_name} in {tgt_name}")
261
+ create(name=nb_name, description=artifact.description, content=content, workspaceId=tgt_workspace_id)
262
+ count+=1
263
+ print(f"\U0001F197 {artifact.displayName} complete\n")
264
+
265
+ # and we're done
266
+ print(f"\U00002705 Done. Notebooks processed: {count}")
267
+
268
+
269
+
270
+ ########################################
271
+ # Deploy Models to Workspace
272
+ ########################################
273
+ def sempy_models_deploy(src_workspace: str, tgt_workspace: str, models: list, source: str, source_type: str, refresh: bool=False):
274
+ count = 0
275
+ for dataset in models:
276
+ deploy_semantic_model(
277
+ source_dataset=dataset,
278
+ source_workspace=src_workspace,
279
+ target_dataset=dataset,
280
+ target_workspace=tgt_workspace,
281
+ refresh_target_dataset=False,
282
+ overwrite=True
283
+ )
284
+
285
+ # update models to point to local workspace
286
+ update_direct_lake_model_connection(
287
+ dataset = dataset,
288
+ workspace = tgt_workspace,
289
+ source = source,
290
+ source_type = source_type,
291
+ source_workspace = tgt_workspace
292
+ )
293
+ count+=1
294
+ # refresh the model if selected
295
+ if refresh:
296
+ refresh_semantic_model(dataset=dataset, workspace=tgt_workspace)
297
+ print(f"\U00002705 Models deployed: {count}")
298
+
299
+ ########################################
300
+ # Deploy Reports to Workspace
301
+ ########################################
302
+ def sempy_reports_deploy(src_workspace: str, tgt_workspace: str, dataset: str, reports: list):
303
+ count = 0
304
+ for report in reports:
305
+ clone_report(
306
+ report = report,
307
+ cloned_report = report,
308
+ workspace = src_workspace,
309
+ target_workspace = tgt_workspace,
310
+ target_dataset = dataset,
311
+ target_dataset_workspace = tgt_workspace
312
+ )
313
+ count+=1
314
+ print(f"\U00002705 Reports deployed: {count}")
315
+
316
+ ########################################
317
+ # Advanvced Deploy Reports to Workspace
318
+ ########################################
319
+ def sempy_reports_deploy_adv(src_workspace: str, tgt_workspace: str, dataset: str, ds_workspace: str, reports: list):
320
+ count = 0
321
+ for (src_report, tgt_report) in reports:
322
+ if tgt_report == None:
323
+ tgt_report = src_report
324
+ clone_report(
325
+ report = src_report,
326
+ cloned_report = tgt_report,
327
+ workspace = src_workspace,
328
+ target_workspace = tgt_workspace,
329
+ target_dataset = dataset,
330
+ target_dataset_workspace = ds_workspace
331
+ )
332
+ count+=1
333
+ print(f"\U00002705 Reports deployed: {count}")
334
+
335
+ ########################################
336
+ # Bind SM Connection (REST API call)
337
+ ########################################
338
+ def set_conn(workspace_id: str, semantic_model_id: str, connection_id: str, connection_path: str) -> str:
339
+ # Authentication/token stuff
340
+ access_url = "https://analysis.windows.net/powerbi/api"
341
+ token = getToken(access_url)
342
+
343
+ bind_url = f"https://api.fabric.microsoft.com/v1/workspaces/{workspace_id}/semanticModels/{semantic_model_id}/bindConnection"
344
+
345
+ headers = {
346
+ "Authorization": f"Bearer {token}",
347
+ "Content-Type": "application/json"
348
+ }
349
+
350
+ payload = {
351
+ "connectionBinding": {
352
+ "id": connection_id,
353
+ "connectivityType": "ShareableCloud",
354
+ "connectionDetails": {
355
+ "type": "SQL",
356
+ "path": connection_path
357
+ }
358
+ }
359
+ }
360
+
361
+ # do it
362
+ response = requests.post(bind_url, json=payload, headers=headers)
363
+ if response.status_code == 200:
364
+ ret = 'Success'
365
+ else:
366
+ ret = f'Error: {response.text}'
367
+ return ret
368
+
369
+ ########################################
370
+ # Connection selector
371
+ ########################################
372
+ def sel_conn(connections: list, workspace: str) -> str:
373
+ for ws, conn in connections:
374
+ if ws == workspace:
375
+ ret = conn
376
+ return(ret)
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: fdsempy
3
+ Version: 1.3.0
4
+ Summary: Fairday Sempy - semantic model utilities
5
+ Author: Peter Johnson - Fairday Research Limited
6
+ Requires-Dist: semantic-link-labs
7
+ Requires-Dist: pandas
8
+ Requires-Dist: json
9
+ Requires-Dist: requests
10
+ Requires-Dist: sempy
11
+ Requires-Dist: notebookutils
12
+ Dynamic: author
13
+ Dynamic: requires-dist
14
+ Dynamic: summary
@@ -0,0 +1,8 @@
1
+ setup.py
2
+ fdsempy/__init__.py
3
+ fdsempy/fdsempy.py
4
+ fdsempy.egg-info/PKG-INFO
5
+ fdsempy.egg-info/SOURCES.txt
6
+ fdsempy.egg-info/dependency_links.txt
7
+ fdsempy.egg-info/requires.txt
8
+ fdsempy.egg-info/top_level.txt
@@ -0,0 +1,6 @@
1
+ semantic-link-labs
2
+ pandas
3
+ json
4
+ requests
5
+ sempy
6
+ notebookutils
@@ -0,0 +1 @@
1
+ fdsempy
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
fdsempy-1.3.0/setup.py ADDED
@@ -0,0 +1,15 @@
1
+ from setuptools import setup
2
+ setup(name='fdsempy',
3
+ description='Fairday Sempy - semantic model utilities',
4
+ author='Peter Johnson - Fairday Research Limited',
5
+ version='1.3.0',
6
+ packages=['fdsempy'],
7
+ install_requires=[
8
+ 'semantic-link-labs',
9
+ 'pandas',
10
+ 'json',
11
+ 'requests',
12
+ 'sempy',
13
+ 'notebookutils'
14
+ ]
15
+ )