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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: normalized-schema-generator
3
- Version: 0.1.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.1.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"}
@@ -3,4 +3,4 @@
3
3
  from .widget import NormalizedSchemaWidget
4
4
 
5
5
  __all__ = ["NormalizedSchemaWidget"]
6
- __version__ = "0.1.0"
6
+ __version__ = "0.2.0"
@@ -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
- def _generate_config(self):
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": [],