normalized-schema-generator 0.3.0__tar.gz → 0.4.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.
- normalized_schema_generator-0.4.0/.git +1 -0
- {normalized_schema_generator-0.3.0 → normalized_schema_generator-0.4.0}/PKG-INFO +3 -4
- {normalized_schema_generator-0.3.0 → normalized_schema_generator-0.4.0}/README.md +1 -1
- {normalized_schema_generator-0.3.0 → normalized_schema_generator-0.4.0}/pyproject.toml +2 -2
- {normalized_schema_generator-0.3.0 → normalized_schema_generator-0.4.0}/src/normalized_schema_generator/widget.js +1 -1
- {normalized_schema_generator-0.3.0 → normalized_schema_generator-0.4.0}/src/normalized_schema_generator/widget.py +85 -14
- normalized_schema_generator-0.4.0/tests/test_widget_tables.py +92 -0
- {normalized_schema_generator-0.3.0 → normalized_schema_generator-0.4.0}/.gitignore +0 -0
- {normalized_schema_generator-0.3.0 → normalized_schema_generator-0.4.0}/LICENSE +0 -0
- {normalized_schema_generator-0.3.0 → normalized_schema_generator-0.4.0}/src/normalized_schema_generator/__init__.py +0 -0
- {normalized_schema_generator-0.3.0 → normalized_schema_generator-0.4.0}/src/normalized_schema_generator/widget.css +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
gitdir: /Users/hippo/Library/CloudStorage/OneDrive-Microsoft/Github/rosikm_normalized-schema-generator/.git/worktrees/rosikm-solid-fishstick
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: normalized-schema-generator
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: An anywidget for Microsoft Fabric notebooks that visually configures normalized schema JSON for Process Mining data import.
|
|
5
5
|
Project-URL: Homepage, https://github.com/rosikm/normalized-schema-generator
|
|
6
6
|
Project-URL: Repository, https://github.com/rosikm/normalized-schema-generator
|
|
7
7
|
Project-URL: Issues, https://github.com/rosikm/normalized-schema-generator/issues
|
|
8
8
|
Author: rosikm
|
|
9
9
|
License: MIT
|
|
10
|
-
License-File: LICENSE
|
|
11
10
|
Keywords: anywidget,fabric,jupyter,process-mining,widget
|
|
12
11
|
Classifier: Development Status :: 4 - Beta
|
|
13
12
|
Classifier: Framework :: Jupyter
|
|
@@ -61,7 +60,7 @@ jupyter lab
|
|
|
61
60
|
|
|
62
61
|
## Features
|
|
63
62
|
|
|
64
|
-
- **Lakehouse selection** from the current workspace
|
|
63
|
+
- **Lakehouse selection** from the current workspace, including schema-enabled lakehouses
|
|
65
64
|
- **Data source toggle**: Delta Tables or Files (CSV / Parquet)
|
|
66
65
|
- **Events table** (required): column checklist with data type badges, include/exclude toggles
|
|
67
66
|
- **Cases table** (optional): same column management, with required Events-to-Cases FK relationship
|
|
@@ -40,7 +40,7 @@ jupyter lab
|
|
|
40
40
|
|
|
41
41
|
## Features
|
|
42
42
|
|
|
43
|
-
- **Lakehouse selection** from the current workspace
|
|
43
|
+
- **Lakehouse selection** from the current workspace, including schema-enabled lakehouses
|
|
44
44
|
- **Data source toggle**: Delta Tables or Files (CSV / Parquet)
|
|
45
45
|
- **Events table** (required): column checklist with data type badges, include/exclude toggles
|
|
46
46
|
- **Cases table** (optional): same column management, with required Events-to-Cases FK relationship
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["hatchling"]
|
|
2
|
+
requires = ["hatchling<1.27"]
|
|
3
3
|
build-backend = "hatchling.build"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "normalized-schema-generator"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "An anywidget for Microsoft Fabric notebooks that visually configures normalized schema JSON for Process Mining data import."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -96,7 +96,7 @@ export function render({ model, el }) {
|
|
|
96
96
|
return h("div", { className: "nsw-header" },
|
|
97
97
|
h("div", { style: "display:flex;align-items:baseline;gap:8px" },
|
|
98
98
|
h("h2", { textContent: "Normalized Schema Generator" }),
|
|
99
|
-
h("span", { className: "nsw-version", textContent: "v0.
|
|
99
|
+
h("span", { className: "nsw-version", textContent: "v0.4.0" })
|
|
100
100
|
),
|
|
101
101
|
h("p", { textContent: "Configure your process mining star-schema and generate the dataSource JSON." })
|
|
102
102
|
);
|
|
@@ -112,23 +112,90 @@ def _list_files_recursive(base_path, lakehouse_id, file_type, prefix="", depth=0
|
|
|
112
112
|
return results
|
|
113
113
|
|
|
114
114
|
|
|
115
|
+
def _quote_spark_identifier(identifier):
|
|
116
|
+
return f"`{identifier.replace('`', '``')}`"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _row_value(row, *names):
|
|
120
|
+
if hasattr(row, "asDict"):
|
|
121
|
+
row = row.asDict()
|
|
122
|
+
if isinstance(row, dict):
|
|
123
|
+
for name in names:
|
|
124
|
+
if name in row:
|
|
125
|
+
return row[name]
|
|
126
|
+
return None
|
|
127
|
+
for name in names:
|
|
128
|
+
if hasattr(row, name):
|
|
129
|
+
return getattr(row, name)
|
|
130
|
+
try:
|
|
131
|
+
return row[0]
|
|
132
|
+
except (IndexError, KeyError, TypeError):
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _list_schema_tables(lakehouse_name, lakehouse_id):
|
|
137
|
+
"""Discover tables via Spark for schema-enabled lakehouses."""
|
|
138
|
+
if _spark is None:
|
|
139
|
+
return []
|
|
140
|
+
|
|
141
|
+
lakehouse = _quote_spark_identifier(lakehouse_name)
|
|
142
|
+
schemas = _spark.sql(f"SHOW SCHEMAS IN {lakehouse}").collect()
|
|
143
|
+
results = []
|
|
144
|
+
for row in schemas:
|
|
145
|
+
schema = _row_value(row, "namespace", "databaseName", "schemaName")
|
|
146
|
+
if not schema:
|
|
147
|
+
continue
|
|
148
|
+
schema_identifier = _quote_spark_identifier(schema)
|
|
149
|
+
tables = _spark.sql(
|
|
150
|
+
f"SHOW TABLES IN {lakehouse}.{schema_identifier}"
|
|
151
|
+
).collect()
|
|
152
|
+
for table_row in tables:
|
|
153
|
+
table_name = _row_value(table_row, "tableName", "table_name", "name")
|
|
154
|
+
if not table_name:
|
|
155
|
+
continue
|
|
156
|
+
results.append({
|
|
157
|
+
"name": f"{schema}.{table_name}",
|
|
158
|
+
"path": f"/{lakehouse_id}/Tables/{schema}/{table_name}",
|
|
159
|
+
"type": "delta",
|
|
160
|
+
})
|
|
161
|
+
return results
|
|
162
|
+
|
|
163
|
+
|
|
115
164
|
def _list_tables(lakehouse_name, lakehouse_id, file_type):
|
|
116
165
|
if FABRIC_AVAILABLE:
|
|
117
|
-
|
|
118
|
-
|
|
166
|
+
if file_type == 2:
|
|
167
|
+
try:
|
|
119
168
|
tables = _lh_utils.listTables(lakehouse_name)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
169
|
+
except Exception:
|
|
170
|
+
tables = None
|
|
171
|
+
|
|
172
|
+
if tables:
|
|
173
|
+
results = []
|
|
174
|
+
for table in tables:
|
|
175
|
+
name = table.name
|
|
176
|
+
schema = (
|
|
177
|
+
getattr(table, "schemaName", None)
|
|
178
|
+
or getattr(table, "schema", None)
|
|
179
|
+
)
|
|
180
|
+
if schema:
|
|
181
|
+
name = f"{schema}.{name}"
|
|
182
|
+
else:
|
|
183
|
+
schema = "dbo"
|
|
184
|
+
results.append({
|
|
185
|
+
"name": name,
|
|
186
|
+
"path": f"/{lakehouse_id}/Tables/{schema}/{table.name}",
|
|
124
187
|
"type": "delta",
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
188
|
+
})
|
|
189
|
+
return results
|
|
190
|
+
|
|
191
|
+
schema_tables = _list_schema_tables(lakehouse_name, lakehouse_id)
|
|
192
|
+
if schema_tables or tables == []:
|
|
193
|
+
return schema_tables
|
|
194
|
+
raise RuntimeError(
|
|
195
|
+
f"Unable to list tables in lakehouse {lakehouse_name!r}; "
|
|
196
|
+
"NotebookUtils listing failed and Spark catalog discovery is unavailable."
|
|
197
|
+
)
|
|
198
|
+
return _list_files_recursive("Files", lakehouse_id, file_type)
|
|
132
199
|
# Mock data
|
|
133
200
|
if file_type == 2:
|
|
134
201
|
return [
|
|
@@ -175,7 +242,11 @@ def _get_columns(table_name, lakehouse_name, lakehouse_id, file_type):
|
|
|
175
242
|
if FABRIC_AVAILABLE and _spark:
|
|
176
243
|
try:
|
|
177
244
|
if file_type == 2:
|
|
178
|
-
|
|
245
|
+
name_parts = table_name.split(".", 1)
|
|
246
|
+
identifiers = [lakehouse_name, *name_parts]
|
|
247
|
+
full_name = ".".join(
|
|
248
|
+
_quote_spark_identifier(part) for part in identifiers
|
|
249
|
+
)
|
|
179
250
|
df = _spark.sql(f"DESCRIBE TABLE {full_name}")
|
|
180
251
|
rows = df.collect()
|
|
181
252
|
return [
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from types import SimpleNamespace
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
|
|
5
|
+
from normalized_schema_generator import widget
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FakeSpark:
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self.queries = []
|
|
11
|
+
|
|
12
|
+
def sql(self, query):
|
|
13
|
+
self.queries.append(query)
|
|
14
|
+
if query == "SHOW SCHEMAS IN `Sales`":
|
|
15
|
+
rows = [SimpleNamespace(namespace="dbo"), SimpleNamespace(namespace="marketing")]
|
|
16
|
+
elif query == "SHOW TABLES IN `Sales`.`dbo`":
|
|
17
|
+
rows = [SimpleNamespace(tableName="customers")]
|
|
18
|
+
elif query == "SHOW TABLES IN `Sales`.`marketing`":
|
|
19
|
+
rows = [SimpleNamespace(tableName="orders")]
|
|
20
|
+
elif query == "DESCRIBE TABLE `Sales`.`marketing`.`orders`":
|
|
21
|
+
rows = [{"col_name": "order_id", "data_type": "int"}]
|
|
22
|
+
else:
|
|
23
|
+
raise AssertionError(f"Unexpected Spark SQL query: {query}")
|
|
24
|
+
return SimpleNamespace(collect=lambda: rows)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class LakehouseTableTests(unittest.TestCase):
|
|
28
|
+
def test_schema_catalog_fallback_lists_tables_with_schema_paths(self):
|
|
29
|
+
spark = FakeSpark()
|
|
30
|
+
lakehouse_utils = SimpleNamespace(
|
|
31
|
+
listTables=lambda _name: (_ for _ in ()).throw(
|
|
32
|
+
RuntimeError("schemas are unsupported")
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
with patch.object(widget, "FABRIC_AVAILABLE", True):
|
|
37
|
+
with patch.object(widget, "_lh_utils", lakehouse_utils):
|
|
38
|
+
with patch.object(widget, "_spark", spark):
|
|
39
|
+
tables = widget._list_tables("Sales", "lakehouse-id", 2)
|
|
40
|
+
|
|
41
|
+
self.assertEqual(
|
|
42
|
+
tables,
|
|
43
|
+
[
|
|
44
|
+
{
|
|
45
|
+
"name": "dbo.customers",
|
|
46
|
+
"path": "/lakehouse-id/Tables/dbo/customers",
|
|
47
|
+
"type": "delta",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"name": "marketing.orders",
|
|
51
|
+
"path": "/lakehouse-id/Tables/marketing/orders",
|
|
52
|
+
"type": "delta",
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def test_schema_qualified_table_name_is_used_for_column_lookup(self):
|
|
58
|
+
spark = FakeSpark()
|
|
59
|
+
|
|
60
|
+
with patch.object(widget, "FABRIC_AVAILABLE", True):
|
|
61
|
+
with patch.object(widget, "_spark", spark):
|
|
62
|
+
columns = widget._get_columns(
|
|
63
|
+
"marketing.orders", "Sales", "lakehouse-id", 2
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
self.assertEqual(columns, [{"name": "order_id", "dtype": "int"}])
|
|
67
|
+
self.assertIn(
|
|
68
|
+
"DESCRIBE TABLE `Sales`.`marketing`.`orders`",
|
|
69
|
+
spark.queries,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def test_legacy_listing_keeps_table_names_and_dbo_paths(self):
|
|
73
|
+
lakehouse_utils = SimpleNamespace(
|
|
74
|
+
listTables=lambda _name: [SimpleNamespace(name="events")]
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
with patch.object(widget, "FABRIC_AVAILABLE", True):
|
|
78
|
+
with patch.object(widget, "_lh_utils", lakehouse_utils):
|
|
79
|
+
tables = widget._list_tables("Legacy", "lakehouse-id", 2)
|
|
80
|
+
|
|
81
|
+
self.assertEqual(
|
|
82
|
+
tables,
|
|
83
|
+
[{
|
|
84
|
+
"name": "events",
|
|
85
|
+
"path": "/lakehouse-id/Tables/dbo/events",
|
|
86
|
+
"type": "delta",
|
|
87
|
+
}],
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|