normalized-schema-generator 0.1.0__tar.gz → 0.2.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.1.0 → normalized_schema_generator-0.2.0}/PKG-INFO +1 -1
- {normalized_schema_generator-0.1.0 → normalized_schema_generator-0.2.0}/pyproject.toml +1 -1
- {normalized_schema_generator-0.1.0 → normalized_schema_generator-0.2.0}/src/normalized_schema_generator/__init__.py +1 -1
- {normalized_schema_generator-0.1.0 → normalized_schema_generator-0.2.0}/src/normalized_schema_generator/widget.py +102 -9
- {normalized_schema_generator-0.1.0 → normalized_schema_generator-0.2.0}/.gitignore +0 -0
- {normalized_schema_generator-0.1.0 → normalized_schema_generator-0.2.0}/LICENSE +0 -0
- {normalized_schema_generator-0.1.0 → normalized_schema_generator-0.2.0}/README.md +0 -0
- {normalized_schema_generator-0.1.0 → normalized_schema_generator-0.2.0}/src/normalized_schema_generator/widget.css +0 -0
- {normalized_schema_generator-0.1.0 → normalized_schema_generator-0.2.0}/src/normalized_schema_generator/widget.js +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: normalized-schema-generator
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.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
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "normalized-schema-generator"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.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"}
|
|
@@ -265,6 +265,9 @@ class NormalizedSchemaWidget(anywidget.AnyWidget):
|
|
|
265
265
|
# Events→Cases FK relationship: {fk_column, pk_column, join_key_type}
|
|
266
266
|
cases_relationship = traitlets.Dict({}).tag(sync=True)
|
|
267
267
|
|
|
268
|
+
# Attribute mapping
|
|
269
|
+
attributes_config = traitlets.List([]).tag(sync=True)
|
|
270
|
+
|
|
268
271
|
# Communication & output
|
|
269
272
|
fetch_columns_request = traitlets.Unicode("").tag(sync=True)
|
|
270
273
|
fetch_columns_response = traitlets.List([]).tag(sync=True)
|
|
@@ -409,6 +412,11 @@ class NormalizedSchemaWidget(anywidget.AnyWidget):
|
|
|
409
412
|
def _on_data_change(self, change):
|
|
410
413
|
self._generate_config()
|
|
411
414
|
|
|
415
|
+
@traitlets.observe("attributes_config")
|
|
416
|
+
def _on_attributes_change(self, change):
|
|
417
|
+
# Only regenerate JSON, don't re-sync attributes (avoid loop)
|
|
418
|
+
self._generate_config(sync_attributes=False)
|
|
419
|
+
|
|
412
420
|
# ── JSON generation ──────────────────────────────────
|
|
413
421
|
|
|
414
422
|
def _find_table_path(self, table_name):
|
|
@@ -417,13 +425,36 @@ class NormalizedSchemaWidget(anywidget.AnyWidget):
|
|
|
417
425
|
return t["path"]
|
|
418
426
|
return ""
|
|
419
427
|
|
|
420
|
-
|
|
428
|
+
@staticmethod
|
|
429
|
+
def _dataset_name(table_name):
|
|
430
|
+
"""Extract a clean dataset name: strip path and file extension."""
|
|
431
|
+
import posixpath
|
|
432
|
+
base = posixpath.basename(table_name)
|
|
433
|
+
# Remove known extensions (.csv, .parquet, .snappy.parquet)
|
|
434
|
+
for ext in (".snappy.parquet", ".parquet", ".csv"):
|
|
435
|
+
if base.lower().endswith(ext):
|
|
436
|
+
base = base[: -len(ext)]
|
|
437
|
+
break
|
|
438
|
+
return base
|
|
439
|
+
|
|
440
|
+
@staticmethod
|
|
441
|
+
def _dtype_to_source_data_type(dtype):
|
|
442
|
+
return {
|
|
443
|
+
"int": "Integer",
|
|
444
|
+
"double": "Float",
|
|
445
|
+
"string": "String",
|
|
446
|
+
"timestamp": "Date",
|
|
447
|
+
"boolean": "Boolean",
|
|
448
|
+
}.get(dtype, "String")
|
|
449
|
+
|
|
450
|
+
def _generate_config(self, sync_attributes=True):
|
|
421
451
|
self.error_message = ""
|
|
422
452
|
if not self.selected_lakehouse_id or not self.events_table_name:
|
|
423
453
|
self.config_json = ""
|
|
424
454
|
return
|
|
425
455
|
|
|
426
456
|
try:
|
|
457
|
+
dn = self._dataset_name
|
|
427
458
|
datasets = []
|
|
428
459
|
join_dataset_names = set()
|
|
429
460
|
|
|
@@ -446,19 +477,18 @@ class NormalizedSchemaWidget(anywidget.AnyWidget):
|
|
|
446
477
|
if c.get("included") and c["name"] not in events_fk_cols
|
|
447
478
|
]
|
|
448
479
|
events_join_arr = []
|
|
449
|
-
# Add the Events→Cases join first
|
|
450
480
|
if has_cases_rel:
|
|
451
481
|
events_join_arr.append({
|
|
452
482
|
"SourceColumnName": cases_rel["fk_column"],
|
|
453
483
|
"TargetColumnName": cases_rel["pk_column"],
|
|
454
|
-
"TargetDatasetName": self.cases_table_name,
|
|
484
|
+
"TargetDatasetName": dn(self.cases_table_name),
|
|
455
485
|
"JoinKeyType": cases_rel.get("join_key_type", "Integer"),
|
|
456
486
|
})
|
|
457
487
|
for j in self.events_joins:
|
|
458
488
|
entry = {
|
|
459
489
|
"SourceColumnName": j["source_column"],
|
|
460
490
|
"TargetColumnName": j["target_column"],
|
|
461
|
-
"TargetDatasetName": j["target_table"],
|
|
491
|
+
"TargetDatasetName": dn(j["target_table"]),
|
|
462
492
|
"JoinKeyType": j["join_key_type"],
|
|
463
493
|
}
|
|
464
494
|
if j.get("export_name"):
|
|
@@ -468,12 +498,19 @@ class NormalizedSchemaWidget(anywidget.AnyWidget):
|
|
|
468
498
|
|
|
469
499
|
datasets.append({
|
|
470
500
|
"Kind": 0,
|
|
471
|
-
"Name": self.events_table_name,
|
|
501
|
+
"Name": dn(self.events_table_name),
|
|
472
502
|
"Path": self._find_table_path(self.events_table_name),
|
|
473
503
|
"Columns": events_columns,
|
|
474
504
|
"Join": events_join_arr if events_join_arr else None,
|
|
475
505
|
})
|
|
476
506
|
|
|
507
|
+
# Collect attribute names + dtypes + level for Attributes section
|
|
508
|
+
# {col_name: {dtype, level}}
|
|
509
|
+
attr_info = {}
|
|
510
|
+
for c in self.events_columns:
|
|
511
|
+
if c.get("included") and c["name"] not in events_fk_cols:
|
|
512
|
+
attr_info[c["name"]] = {"dtype": c["dtype"], "level": "Event"}
|
|
513
|
+
|
|
477
514
|
# ── Cases dataset (Kind=1) ──
|
|
478
515
|
if self.cases_enabled and self.cases_table_name:
|
|
479
516
|
cases_fk_cols = {j["source_column"] for j in self.cases_joins}
|
|
@@ -487,7 +524,7 @@ class NormalizedSchemaWidget(anywidget.AnyWidget):
|
|
|
487
524
|
entry = {
|
|
488
525
|
"SourceColumnName": j["source_column"],
|
|
489
526
|
"TargetColumnName": j["target_column"],
|
|
490
|
-
"TargetDatasetName": j["target_table"],
|
|
527
|
+
"TargetDatasetName": dn(j["target_table"]),
|
|
491
528
|
"JoinKeyType": j["join_key_type"],
|
|
492
529
|
}
|
|
493
530
|
if j.get("export_name"):
|
|
@@ -497,12 +534,16 @@ class NormalizedSchemaWidget(anywidget.AnyWidget):
|
|
|
497
534
|
|
|
498
535
|
datasets.append({
|
|
499
536
|
"Kind": 1,
|
|
500
|
-
"Name": self.cases_table_name,
|
|
537
|
+
"Name": dn(self.cases_table_name),
|
|
501
538
|
"Path": self._find_table_path(self.cases_table_name),
|
|
502
539
|
"Columns": cases_columns,
|
|
503
540
|
"Join": cases_join_arr if cases_join_arr else None,
|
|
504
541
|
})
|
|
505
542
|
|
|
543
|
+
for c in self.cases_columns:
|
|
544
|
+
if c.get("included") and c["name"] not in cases_fk_cols:
|
|
545
|
+
attr_info[c["name"]] = {"dtype": c["dtype"], "level": "Case"}
|
|
546
|
+
|
|
506
547
|
# ── Join datasets (Kind=2) ──
|
|
507
548
|
join_table_cols = {}
|
|
508
549
|
for j in list(self.events_joins) + list(self.cases_joins):
|
|
@@ -524,11 +565,63 @@ class NormalizedSchemaWidget(anywidget.AnyWidget):
|
|
|
524
565
|
]
|
|
525
566
|
datasets.append({
|
|
526
567
|
"Kind": 2,
|
|
527
|
-
"Name": tname,
|
|
568
|
+
"Name": dn(tname),
|
|
528
569
|
"Path": self._find_table_path(tname),
|
|
529
570
|
"Columns": join_cols,
|
|
530
571
|
"Join": None,
|
|
531
572
|
})
|
|
573
|
+
# Join target columns become Event-level attributes
|
|
574
|
+
for c in cols_map.values():
|
|
575
|
+
if c.get("included"):
|
|
576
|
+
attr_info[c["name"]] = {"dtype": c["dtype"], "level": "Event"}
|
|
577
|
+
|
|
578
|
+
# Join ExportNames also become attributes
|
|
579
|
+
for j in list(self.events_joins) + list(self.cases_joins):
|
|
580
|
+
if j.get("export_name"):
|
|
581
|
+
attr_info[j["export_name"]] = {
|
|
582
|
+
"dtype": "int", # FK columns are typically int
|
|
583
|
+
"level": "Event",
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
# ── Sync attributes_config with current columns ──
|
|
587
|
+
if sync_attributes:
|
|
588
|
+
existing = {a["Name"]: a for a in self.attributes_config}
|
|
589
|
+
new_attrs = []
|
|
590
|
+
for col_name, info in attr_info.items():
|
|
591
|
+
if col_name in existing:
|
|
592
|
+
new_attrs.append(existing[col_name])
|
|
593
|
+
else:
|
|
594
|
+
new_attrs.append({
|
|
595
|
+
"Name": col_name,
|
|
596
|
+
"SourceDataType": self._dtype_to_source_data_type(info["dtype"]),
|
|
597
|
+
"ImportType": "Other",
|
|
598
|
+
"Level": info["level"],
|
|
599
|
+
"FinanceImportType": "None",
|
|
600
|
+
"CaseLevelAttributeHandling": "None",
|
|
601
|
+
"IsDisplayAttribute": False,
|
|
602
|
+
"IsExcluded": False,
|
|
603
|
+
"DefaultValue": "",
|
|
604
|
+
})
|
|
605
|
+
if new_attrs != list(self.attributes_config):
|
|
606
|
+
self.attributes_config = new_attrs
|
|
607
|
+
|
|
608
|
+
# ── Build Attributes for JSON from attributes_config ──
|
|
609
|
+
attrs_by_name = {a["Name"]: a for a in self.attributes_config}
|
|
610
|
+
attributes = []
|
|
611
|
+
for col_name in attr_info:
|
|
612
|
+
a = attrs_by_name.get(col_name)
|
|
613
|
+
if a:
|
|
614
|
+
attributes.append({
|
|
615
|
+
"Name": a["Name"],
|
|
616
|
+
"SourceDataType": a.get("SourceDataType", "String"),
|
|
617
|
+
"ImportType": a.get("ImportType", "Other"),
|
|
618
|
+
"Level": a.get("Level", "Event"),
|
|
619
|
+
"FinanceImportType": a.get("FinanceImportType", "None"),
|
|
620
|
+
"CaseLevelAttributeHandling": a.get("CaseLevelAttributeHandling", "None"),
|
|
621
|
+
"IsDisplayAttribute": a.get("IsDisplayAttribute", False),
|
|
622
|
+
"IsExcluded": a.get("IsExcluded", False),
|
|
623
|
+
"DefaultValue": a.get("DefaultValue", ""),
|
|
624
|
+
})
|
|
532
625
|
|
|
533
626
|
# ── Assemble full config ──
|
|
534
627
|
config = {
|
|
@@ -548,7 +641,7 @@ class NormalizedSchemaWidget(anywidget.AnyWidget):
|
|
|
548
641
|
"datasets": datasets,
|
|
549
642
|
},
|
|
550
643
|
"miningMetadata": {
|
|
551
|
-
"ImportConfiguration": {"Attributes":
|
|
644
|
+
"ImportConfiguration": {"Attributes": attributes},
|
|
552
645
|
"Views": [],
|
|
553
646
|
"ProcessExtendedMetadata": {
|
|
554
647
|
"CalculatedMetrics": [],
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|