tabalyst 0.1.1__tar.gz → 0.1.2__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.
Files changed (38) hide show
  1. {tabalyst-0.1.1/src/tabalyst.egg-info → tabalyst-0.1.2}/PKG-INFO +5 -6
  2. {tabalyst-0.1.1 → tabalyst-0.1.2}/README.md +4 -5
  3. {tabalyst-0.1.1 → tabalyst-0.1.2}/pyproject.toml +1 -1
  4. {tabalyst-0.1.1 → tabalyst-0.1.2}/src/tabalyst/analysis.py +102 -5
  5. {tabalyst-0.1.1 → tabalyst-0.1.2}/src/tabalyst/models.py +38 -1
  6. {tabalyst-0.1.1 → tabalyst-0.1.2}/src/tabalyst/reporting.py +21 -2
  7. tabalyst-0.1.2/src/tabalyst/static/report.js +442 -0
  8. tabalyst-0.1.2/src/tabalyst/static/theme.css +484 -0
  9. tabalyst-0.1.2/src/tabalyst/templates/report.html +83 -0
  10. {tabalyst-0.1.1 → tabalyst-0.1.2/src/tabalyst.egg-info}/PKG-INFO +5 -6
  11. {tabalyst-0.1.1 → tabalyst-0.1.2}/tests/test_analysis.py +21 -1
  12. {tabalyst-0.1.1 → tabalyst-0.1.2}/tests/test_cli.py +3 -2
  13. {tabalyst-0.1.1 → tabalyst-0.1.2}/tests/test_public_api.py +1 -1
  14. {tabalyst-0.1.1 → tabalyst-0.1.2}/tests/test_reporting.py +30 -20
  15. tabalyst-0.1.1/src/tabalyst/static/report.js +0 -502
  16. tabalyst-0.1.1/src/tabalyst/static/theme.css +0 -427
  17. tabalyst-0.1.1/src/tabalyst/templates/report.html +0 -370
  18. {tabalyst-0.1.1 → tabalyst-0.1.2}/LICENSE +0 -0
  19. {tabalyst-0.1.1 → tabalyst-0.1.2}/MANIFEST.in +0 -0
  20. {tabalyst-0.1.1 → tabalyst-0.1.2}/examples/README.md +0 -0
  21. {tabalyst-0.1.1 → tabalyst-0.1.2}/examples/config.json +0 -0
  22. {tabalyst-0.1.1 → tabalyst-0.1.2}/examples/input/basic.csv +0 -0
  23. {tabalyst-0.1.1 → tabalyst-0.1.2}/examples/input/insurance-customers.csv +0 -0
  24. {tabalyst-0.1.1 → tabalyst-0.1.2}/setup.cfg +0 -0
  25. {tabalyst-0.1.1 → tabalyst-0.1.2}/src/tabalyst/__init__.py +0 -0
  26. {tabalyst-0.1.1 → tabalyst-0.1.2}/src/tabalyst/__main__.py +0 -0
  27. {tabalyst-0.1.1 → tabalyst-0.1.2}/src/tabalyst/_version.py +0 -0
  28. {tabalyst-0.1.1 → tabalyst-0.1.2}/src/tabalyst/cli.py +0 -0
  29. {tabalyst-0.1.1 → tabalyst-0.1.2}/src/tabalyst/config.py +0 -0
  30. {tabalyst-0.1.1 → tabalyst-0.1.2}/src/tabalyst/errors.py +0 -0
  31. {tabalyst-0.1.1 → tabalyst-0.1.2}/src/tabalyst/execution_log.py +0 -0
  32. {tabalyst-0.1.1 → tabalyst-0.1.2}/src/tabalyst/ingestion.py +0 -0
  33. {tabalyst-0.1.1 → tabalyst-0.1.2}/src/tabalyst/service.py +0 -0
  34. {tabalyst-0.1.1 → tabalyst-0.1.2}/src/tabalyst.egg-info/SOURCES.txt +0 -0
  35. {tabalyst-0.1.1 → tabalyst-0.1.2}/src/tabalyst.egg-info/dependency_links.txt +0 -0
  36. {tabalyst-0.1.1 → tabalyst-0.1.2}/src/tabalyst.egg-info/entry_points.txt +0 -0
  37. {tabalyst-0.1.1 → tabalyst-0.1.2}/src/tabalyst.egg-info/requires.txt +0 -0
  38. {tabalyst-0.1.1 → tabalyst-0.1.2}/src/tabalyst.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tabalyst
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Open-source data profiling engine for structured JSON and customizable HTML reports.
5
5
  Author: Gregory Borelli
6
6
  License-Expression: MIT
@@ -162,13 +162,12 @@ See the complete layout and maintenance notes in the
162
162
  - Physical and semantic type inference with explicit confidence and error rates.
163
163
  - Numeric, date, string-length, normalization, and value-distribution profiles.
164
164
  - A bounded raw-data preview while all records are analyzed.
165
- - Sortable and filterable HTML tables.
165
+ - Sortable and filterable HTML tables in the self-contained Signature report.
166
166
 
167
167
  Raw strings are preserved. The complete CSV is currently loaded into memory.
168
- Report data remains local and Tabalyst adds no telemetry or remote analysis.
169
- Bootstrap, DataTables, and Google Fonts are loaded from pinned CDNs for the full
170
- interactive presentation; the Tabalyst template, theme, and report JavaScript are
171
- included in the Python package.
168
+ Report data remains local and Tabalyst adds no telemetry or remote analysis. The
169
+ report embeds its design system, font subsets and interaction code, so it works
170
+ without CDN or network access.
172
171
 
173
172
  ## Compatibility with alpha commands
174
173
 
@@ -128,13 +128,12 @@ See the complete layout and maintenance notes in the
128
128
  - Physical and semantic type inference with explicit confidence and error rates.
129
129
  - Numeric, date, string-length, normalization, and value-distribution profiles.
130
130
  - A bounded raw-data preview while all records are analyzed.
131
- - Sortable and filterable HTML tables.
131
+ - Sortable and filterable HTML tables in the self-contained Signature report.
132
132
 
133
133
  Raw strings are preserved. The complete CSV is currently loaded into memory.
134
- Report data remains local and Tabalyst adds no telemetry or remote analysis.
135
- Bootstrap, DataTables, and Google Fonts are loaded from pinned CDNs for the full
136
- interactive presentation; the Tabalyst template, theme, and report JavaScript are
137
- included in the Python package.
134
+ Report data remains local and Tabalyst adds no telemetry or remote analysis. The
135
+ report embeds its design system, font subsets and interaction code, so it works
136
+ without CDN or network access.
138
137
 
139
138
  ## Compatibility with alpha commands
140
139
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tabalyst"
7
- version = "0.1.1"
7
+ version = "0.1.2"
8
8
  description = "Open-source data profiling engine for structured JSON and customizable HTML reports."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -14,6 +14,8 @@ from tabalyst.config import AnalysisConfig
14
14
  from tabalyst.ingestion import CsvDataset
15
15
  from tabalyst.models import (
16
16
  ColumnProfile,
17
+ DatasetDateColumnSummary,
18
+ DatasetDateSummary,
17
19
  DatasetProfile,
18
20
  DatasetSummary,
19
21
  DateBreakdownItem,
@@ -313,10 +315,15 @@ def build_date_profile(
313
315
  return (
314
316
  DateProfile(
315
317
  status=status,
318
+ present_count=total,
316
319
  valid_count=valid_count,
320
+ valid_percent=percent(valid_count, total),
317
321
  ambiguous_count=ambiguous_count,
322
+ ambiguous_percent=percent(ambiguous_count, total),
318
323
  invalid_date_count=invalid_count,
324
+ invalid_date_percent=percent(invalid_count, total),
319
325
  not_date_count=not_date_count,
326
+ not_date_percent=percent(not_date_count, total),
320
327
  resolved_ambiguous_order=resolved_order,
321
328
  ambiguous_order_source=resolution_source,
322
329
  formats=format_items,
@@ -406,15 +413,19 @@ def build_string_profile(
406
413
  status = "very_long"
407
414
  distribution = []
408
415
  if maximum <= settings.length_distribution_max_length:
416
+ length_counts = lengths.value_counts()
417
+ maximum_length_count = int(length_counts.max())
409
418
  for length in sorted({int(value) for value in lengths}):
410
419
  matching = [
411
420
  (value, count) for value, count in frequencies if len(value) == length
412
421
  ]
422
+ length_count = sum(count for _, count in matching)
413
423
  distribution.append(
414
424
  StringLengthDistribution(
415
425
  length=length,
416
- count=sum(count for _, count in matching),
417
- percent=percent(sum(count for _, count in matching), len(present)),
426
+ count=length_count,
427
+ percent=percent(length_count, len(present)),
428
+ relative_percent=percent(length_count, maximum_length_count),
418
429
  distinct_count=len(matching),
419
430
  examples=[
420
431
  StringLengthExample(value=value, count=count)
@@ -432,6 +443,11 @@ def build_string_profile(
432
443
  distinct_length_count=int(lengths.nunique()),
433
444
  fixed_length=minimum if minimum == maximum else None,
434
445
  length_distribution=distribution,
446
+ representative_examples=[
447
+ item.examples[0].value
448
+ for item in sorted(distribution, key=lambda item: -item.count)[:3]
449
+ if item.examples
450
+ ],
435
451
  )
436
452
 
437
453
 
@@ -632,7 +648,11 @@ def analyze_column(
632
648
  stats = [numbers.min(), numbers.max(), numbers.mean(), numbers.median()]
633
649
  if numbers.notna().all() and all(math.isfinite(value) for value in stats):
634
650
  numeric = NumericStats(
635
- minimum=stats[0], maximum=stats[1], mean=stats[2], median=stats[3]
651
+ minimum=stats[0],
652
+ maximum=stats[1],
653
+ range=stats[1] - stats[0],
654
+ mean=stats[2],
655
+ median=stats[3],
636
656
  )
637
657
  value_profile = build_value_profile(
638
658
  frequencies,
@@ -649,6 +669,7 @@ def analyze_column(
649
669
  if enum and semantic_type is None:
650
670
  semantic_type = "enum"
651
671
  string_profile = build_string_profile(present, frequencies, inferred, config)
672
+ missing_count = int(absent.sum())
652
673
  return ColumnProfile(
653
674
  id=f"column_{position}",
654
675
  name=name if name is not None else str(values.name or ""),
@@ -658,8 +679,9 @@ def analyze_column(
658
679
  type_confidence=round(type_confidence, 4),
659
680
  type_error_count=type_error_count,
660
681
  type_error_percent=type_error_percent,
661
- missing_count=int(absent.sum()),
662
- missing_percent=percent(int(absent.sum()), len(values)),
682
+ missing_count=missing_count,
683
+ missing_percent=percent(missing_count, len(values)),
684
+ with_issues=missing_count > 0 or inferred == "mixed",
663
685
  normalization=normalization,
664
686
  distinct_count=len(frequencies),
665
687
  examples=[
@@ -691,6 +713,40 @@ def analyze_dataset(dataset: CsvDataset, config: AnalysisConfig) -> DatasetProfi
691
713
  empty = [column.id for column in columns if column.inferred_type == "empty"]
692
714
  constant = [column.id for column in columns if column.distinct_count == 1]
693
715
  mixed = [column.id for column in columns if column.inferred_type == "mixed"]
716
+ numeric_columns = [column for column in columns if column.numeric is not None]
717
+ date_columns = [column for column in columns if column.date_profile is not None]
718
+ string_columns = [column for column in columns if column.string_profile is not None]
719
+ inferred_type_counts = Counter(column.inferred_type for column in columns)
720
+ semantic_type_counts = Counter(column.semantic_type or "none" for column in columns)
721
+ date_present_count = sum(
722
+ column.date_profile.present_count for column in date_columns if column.date_profile
723
+ )
724
+ date_valid_count = sum(
725
+ column.date_profile.valid_count for column in date_columns if column.date_profile
726
+ )
727
+ date_ambiguous_count = sum(
728
+ column.date_profile.ambiguous_count
729
+ for column in date_columns
730
+ if column.date_profile
731
+ )
732
+ date_invalid_count = sum(
733
+ column.date_profile.invalid_date_count
734
+ for column in date_columns
735
+ if column.date_profile
736
+ )
737
+ date_not_date_count = sum(
738
+ column.date_profile.not_date_count
739
+ for column in date_columns
740
+ if column.date_profile
741
+ )
742
+ maximum_column_ambiguous_count = max(
743
+ (
744
+ column.date_profile.ambiguous_count
745
+ for column in date_columns
746
+ if column.date_profile
747
+ ),
748
+ default=0,
749
+ )
694
750
  issues = []
695
751
 
696
752
  def add_issue(
@@ -774,6 +830,47 @@ def analyze_dataset(dataset: CsvDataset, config: AnalysisConfig) -> DatasetProfi
774
830
  empty_row_count=int(absent.all(axis=1).sum()),
775
831
  empty_column_count=len(empty),
776
832
  constant_column_count=len(constant),
833
+ with_issues_column_count=sum(column.with_issues for column in columns),
834
+ numeric_column_count=len(numeric_columns),
835
+ date_column_count=len(date_columns),
836
+ string_column_count=len(string_columns),
837
+ inferred_type_counts=dict(inferred_type_counts),
838
+ inferred_type_percents={
839
+ name: percent(count, len(columns))
840
+ for name, count in inferred_type_counts.items()
841
+ },
842
+ semantic_type_counts=dict(semantic_type_counts),
843
+ semantic_type_percents={
844
+ name: percent(count, len(columns))
845
+ for name, count in semantic_type_counts.items()
846
+ },
847
+ ),
848
+ date_summary=(
849
+ DatasetDateSummary(
850
+ present_count=date_present_count,
851
+ valid_count=date_valid_count,
852
+ ambiguous_count=date_ambiguous_count,
853
+ ambiguous_percent=percent(date_ambiguous_count, date_present_count),
854
+ invalid_date_count=date_invalid_count,
855
+ not_date_count=date_not_date_count,
856
+ maximum_column_ambiguous_count=maximum_column_ambiguous_count,
857
+ columns=[
858
+ DatasetDateColumnSummary(
859
+ column_id=column.id,
860
+ name=column.name,
861
+ position=column.position,
862
+ ambiguous_count=column.date_profile.ambiguous_count,
863
+ relative_ambiguous_percent=percent(
864
+ column.date_profile.ambiguous_count,
865
+ maximum_column_ambiguous_count,
866
+ ),
867
+ )
868
+ for column in date_columns
869
+ if column.date_profile and column.date_profile.ambiguous_count
870
+ ],
871
+ )
872
+ if date_columns
873
+ else None
777
874
  ),
778
875
  columns=columns,
779
876
  issues=issues,
@@ -23,6 +23,7 @@ class SourceInfo(ResultModel):
23
23
  class NumericStats(ResultModel):
24
24
  minimum: FiniteFloat
25
25
  maximum: FiniteFloat
26
+ range: FiniteFloat
26
27
  mean: FiniteFloat
27
28
  median: FiniteFloat
28
29
 
@@ -52,10 +53,15 @@ class DateBreakdownItem(ResultModel):
52
53
 
53
54
  class DateProfile(ResultModel):
54
55
  status: Literal["valid", "multiple_formats", "mixed", "ambiguous", "invalid"]
56
+ present_count: int
55
57
  valid_count: int
58
+ valid_percent: float
56
59
  ambiguous_count: int
60
+ ambiguous_percent: float
57
61
  invalid_date_count: int
62
+ invalid_date_percent: float
58
63
  not_date_count: int
64
+ not_date_percent: float
59
65
  resolved_ambiguous_order: Literal["MDY", "DMY"] | None = None
60
66
  ambiguous_order_source: Literal["config", "column"] | None = None
61
67
  formats: list[DateFormatCount]
@@ -73,6 +79,7 @@ class StringLengthDistribution(ResultModel):
73
79
  length: int
74
80
  count: int
75
81
  percent: float
82
+ relative_percent: float
76
83
  distinct_count: int
77
84
  examples: list[StringLengthExample]
78
85
 
@@ -87,6 +94,7 @@ class StringProfile(ResultModel):
87
94
  distinct_length_count: int
88
95
  fixed_length: int | None = None
89
96
  length_distribution: list[StringLengthDistribution]
97
+ representative_examples: list[str]
90
98
 
91
99
 
92
100
  class ValueOccurrence(ResultModel):
@@ -122,6 +130,7 @@ class ColumnProfile(ResultModel):
122
130
  type_error_percent: float | None
123
131
  missing_count: int
124
132
  missing_percent: float
133
+ with_issues: bool
125
134
  normalization: NormalizationStats
126
135
  distinct_count: int
127
136
  examples: list[str]
@@ -145,6 +154,33 @@ class DatasetSummary(ResultModel):
145
154
  empty_row_count: int
146
155
  empty_column_count: int
147
156
  constant_column_count: int
157
+ with_issues_column_count: int
158
+ numeric_column_count: int
159
+ date_column_count: int
160
+ string_column_count: int
161
+ inferred_type_counts: dict[str, int]
162
+ inferred_type_percents: dict[str, float]
163
+ semantic_type_counts: dict[str, int]
164
+ semantic_type_percents: dict[str, float]
165
+
166
+
167
+ class DatasetDateColumnSummary(ResultModel):
168
+ column_id: str
169
+ name: str
170
+ position: int
171
+ ambiguous_count: int
172
+ relative_ambiguous_percent: float
173
+
174
+
175
+ class DatasetDateSummary(ResultModel):
176
+ present_count: int
177
+ valid_count: int
178
+ ambiguous_count: int
179
+ ambiguous_percent: float
180
+ invalid_date_count: int
181
+ not_date_count: int
182
+ maximum_column_ambiguous_count: int
183
+ columns: list[DatasetDateColumnSummary]
148
184
 
149
185
 
150
186
  class Issue(ResultModel):
@@ -163,12 +199,13 @@ class PreviewRow(ResultModel):
163
199
 
164
200
  class DatasetProfile(ResultModel):
165
201
  format_version: Literal["0.1.0a"] = "0.1.0a"
166
- format_revision: Literal[1] = 1
202
+ format_revision: Literal[2] = 2
167
203
  generated_at: datetime
168
204
  processing_seconds: FiniteFloat
169
205
  source: SourceInfo
170
206
  config: AnalysisConfig
171
207
  summary: DatasetSummary
208
+ date_summary: DatasetDateSummary | None = None
172
209
  columns: list[ColumnProfile]
173
210
  issues: list[Issue]
174
211
  preview: list[PreviewRow]
@@ -1,6 +1,5 @@
1
1
  """Render self-contained analysis results as an interactive HTML report."""
2
2
 
3
- from collections import Counter
4
3
  from importlib.resources import files
5
4
  from pathlib import Path
6
5
 
@@ -45,10 +44,30 @@ def render_report(profile: DatasetProfile) -> str:
45
44
  template = environment.from_string(
46
45
  resources.joinpath("templates/report.html").read_text(encoding="utf-8")
47
46
  )
47
+ type_colors = {
48
+ "text": "var(--d2)",
49
+ "mixed": "var(--attn)",
50
+ "integer": "var(--d3)",
51
+ "number": "var(--d1)",
52
+ "boolean": "var(--ok)",
53
+ "date": "var(--d1)",
54
+ "empty": "var(--line-strong)",
55
+ }
56
+ semantic_colors = {
57
+ "enum": "var(--d2)",
58
+ "date": "var(--d1)",
59
+ "none": "var(--d3)",
60
+ }
48
61
  return template.render(
49
62
  report=profile,
50
63
  tabalyst_version=get_version(),
51
- types=Counter(column.inferred_type for column in profile.columns),
64
+ source_stem=Path(profile.source.filename).stem,
65
+ source_suffix=Path(profile.source.filename).suffix,
66
+ numeric_columns=[column for column in profile.columns if column.numeric],
67
+ date_columns=[column for column in profile.columns if column.date_profile],
68
+ string_columns=[column for column in profile.columns if column.string_profile],
69
+ type_colors=type_colors,
70
+ semantic_colors=semantic_colors,
52
71
  theme_css=resources.joinpath("static/theme.css").read_text(encoding="utf-8"),
53
72
  report_js=resources.joinpath("static/report.js").read_text(encoding="utf-8"),
54
73
  )