tabalyst 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,318 @@
1
+ {% macro percent_label(value) %}{{ '' if value == 0 else value ~ '%' }}{% endmacro %}
2
+ {% set numeric_columns = report.columns | selectattr('numeric') | list %}
3
+ {% set date_columns = report.columns | selectattr('date_profile') | list %}
4
+ {% set string_columns = report.columns | selectattr('string_profile') | list %}
5
+ <!doctype html>
6
+ <html lang="en">
7
+ <head>
8
+ <meta charset="utf-8">
9
+ <meta name="viewport" content="width=device-width, initial-scale=1">
10
+ <title>{{ report.source.filename }} | Tabalyst</title>
11
+ <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='14' fill='%2300051a'/%3E%3Cpath d='M14 15h36v10H37v25H27V25H14z' fill='%23f98510'/%3E%3C/svg%3E">
12
+ <link rel="preconnect" href="https://fonts.googleapis.com">
13
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
14
+ <link href="https://fonts.googleapis.com/css2?family=Caveat:wght@500&family=Oswald:wght@500;600&family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
15
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"
16
+ integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
17
+ <link href="https://cdn.datatables.net/3.0.4/css/dataTables.bootstrap5.min.css" rel="stylesheet"
18
+ integrity="sha384-OZKa6QSlaaq/LGR1sBFkYhC0c/nacIFh1chsblhDUxggC9Zb0XLEEMs95i1Kydnt" crossorigin="anonymous">
19
+ <link href="https://cdn.datatables.net/columncontrol/2.0.2/css/columnControl.bootstrap5.min.css" rel="stylesheet"
20
+ integrity="sha384-Wk/gyUq2KPW83FLwX2RxZfE6EJg34PaQBWKfWAGJFHAiO60FC3On+ZjtZ70V+0BP" crossorigin="anonymous">
21
+ <script>
22
+ (() => {
23
+ let theme = "dark";
24
+ try {
25
+ const saved = localStorage.getItem("tabalyst-theme");
26
+ if (saved === "light" || saved === "dark") theme = saved;
27
+ } catch (error) {
28
+ // Local files can disable storage; dark remains the deterministic default.
29
+ }
30
+ document.documentElement.dataset.theme = theme;
31
+ document.documentElement.dataset.bsTheme = theme;
32
+ })();
33
+ </script>
34
+ <style>{{ theme_css | safe }}</style>
35
+ </head>
36
+ <body>
37
+ <a class="visually-hidden-focusable" href="#overview">Skip to report</a>
38
+ <div class="app-shell">
39
+ <header class="topbar sidebar">
40
+ <div class="topbar-inner">
41
+ <a class="brand" href="#overview"><span class="brand-mark" aria-hidden="true">T</span>Tabalyst</a>
42
+ <nav aria-label="Report sections">
43
+ <p class="nav-label">Report</p>
44
+ <a href="#overview">Overview <span class="nav-count">({{ report.issues | length }})</span></a>
45
+ <a href="#columns">Columns <span class="nav-count">({{ report.summary.column_count }})</span></a>
46
+ <a href="#transformations">Transformations <span class="nav-count">({{ report.columns | length }})</span></a>
47
+ <p class="nav-label">Analysis</p>
48
+ <a href="#numeric">Numeric analysis <span class="nav-count">({{ numeric_columns | length }})</span></a>
49
+ <a href="#dates">Date analysis <span class="nav-count">({{ date_columns | length }})</span></a>
50
+ <a href="#strings">String analysis <span class="nav-count">({{ string_columns | length }})</span></a>
51
+ <p class="nav-label">Data</p>
52
+ <a href="#sample">Data sample <span class="nav-count">({{ report.preview | length }})</span></a>
53
+ <p class="nav-label">Settings</p>
54
+ <a href="#settings">Analysis settings</a>
55
+ </nav>
56
+ <div class="topbar-actions">
57
+ <button type="button" id="theme-toggle" class="theme-toggle" aria-label="Switch to light mode" title="Switch to light mode">
58
+ <span data-theme-icon aria-hidden="true">&#9788;</span>
59
+ </button>
60
+ </div>
61
+ </div>
62
+ </header>
63
+
64
+ <div class="report-page">
65
+ <main class="report-container report-content">
66
+ <section class="report-section">
67
+ <div class="report-heading">
68
+ <div>
69
+ <p class="eyebrow">Dataset report</p>
70
+ <h1>{{ report.source.filename }}</h1>
71
+ <p class="metadata">{{ report.source.size_bytes | size }}
72
+ <span aria-hidden="true"> / </span> {{ report.source.encoding }}
73
+ <span aria-hidden="true"> / </span> Delimiter: <code>{{ report.source.delimiter }}</code></p>
74
+ </div>
75
+ <div class="generation"><span class="status-dot" aria-hidden="true"></span> Analysis complete in {{ report.processing_seconds | seconds }} s
76
+ <time datetime="{{ report.generated_at.isoformat() }}">{{ report.generated_at.strftime('%Y-%m-%d %H:%M UTC') }}</time>
77
+ </div>
78
+ </div>
79
+
80
+ <dl class="metric-grid">
81
+ <div><dt>Rows</dt><dd>{{ report.summary.row_count | count }}</dd><span>{{ 'No empty rows' if report.summary.empty_row_count == 0 else (report.summary.empty_row_count | count) ~ (' empty row' if report.summary.empty_row_count == 1 else ' empty rows') }}</span></div>
82
+ <div><dt>Columns</dt><dd>{{ report.summary.column_count | count }}</dd><span>{{ report.summary.cell_count | count }} cells analyzed</span></div>
83
+ <div><dt>Missing cells</dt><dd class="{{ 'metric-warning' if report.summary.missing_count }}">{{ report.summary.missing_percent }}<small>%</small></dd><span>{{ report.summary.missing_count | count }} cells</span></div>
84
+ <div><dt>Duplicate rows</dt><dd>{{ report.summary.duplicate_row_count | count }}</dd><span>Beyond the first occurrence</span></div>
85
+ </dl>
86
+
87
+ <details id="overview" class="report-details quality-details" open>
88
+ <summary>Quality information <span class="section-count">({{ report.issues | length }})</span></summary>
89
+ <div class="overview-grid">
90
+ <div>
91
+ {% if report.issues %}
92
+ <ul class="observations">
93
+ {% for issue in report.issues %}
94
+ <li><span class="observation-dot {{ issue.severity }}" aria-hidden="true"></span>
95
+ <span><span class="visually-hidden">{{ issue.severity }}: </span>{{ issue.message }}</span>
96
+ <strong>{{ issue.count | count }}</strong></li>
97
+ {% endfor %}
98
+ </ul>
99
+ {% else %}<p class="empty-state">No issues detected by the current checks.</p>{% endif %}
100
+ </div>
101
+ <div class="type-overview">
102
+ <h2>Column types</h2>
103
+ <div class="type-bar" aria-hidden="true">
104
+ {% for kind, count in types.items() %}<span class="type-fill type-{{ kind }}" style="width: {{ 100 * count / report.summary.column_count }}%"></span>{% endfor %}
105
+ </div>
106
+ <ul class="type-legend">
107
+ {% for kind, count in types.items() %}
108
+ <li><span class="legend-dot type-{{ kind }}" aria-hidden="true"></span><span>{{ kind | capitalize }}</span><strong>{{ count }}</strong></li>
109
+ {% endfor %}
110
+ </ul>
111
+ </div>
112
+ </div>
113
+ </details>
114
+ </section>
115
+
116
+ <section class="report-section">
117
+ <details id="columns" class="report-details columns-details" open>
118
+ <summary>Columns <span class="section-count">({{ report.summary.column_count }})</span></summary>
119
+ <div class="table-actions">
120
+ <div class="column-search">
121
+ <label class="visually-hidden" for="column-search">Filter columns by name</label>
122
+ <input type="search" id="column-search" class="form-control form-control-sm" placeholder="Filter columns" autocomplete="off">
123
+ </div>
124
+ <button type="button" class="btn btn-sm btn-outline-secondary table-reset" data-reset-table="columns-table" hidden>Reset</button>
125
+ </div>
126
+ <div class="table-responsive" tabindex="0" role="region" aria-label="Column summary">
127
+ <table id="columns-table" class="table table-hover align-middle column-table">
128
+ <thead><tr><th scope="col">Column</th><th scope="col" class="percentage-column">Missing (%)</th><th scope="col">Distinct</th><th scope="col">Inferred type</th><th scope="col">Semantic type</th><th scope="col" class="percentage-column">Error (%)</th><th scope="col">Examples</th></tr></thead>
129
+ <tbody>
130
+ {% for column in report.columns %}
131
+ <tr data-column-name="{{ column.name | lower }}" id="{{ column.id }}">
132
+ <th scope="row" data-order="{{ column.name }}" data-search="#{{ column.position }} {{ column.name or '(unnamed)' }}"><span class="column-index">{{ column.position }}</span><span class="column-name">{{ column.name or '(unnamed)' }}</span></th>
133
+ <td class="percentage-column" data-order="{{ column.missing_percent }}" data-search="{{ column.missing_percent }}">{% if column.missing_percent != 0 %}<div class="missing-value"><span>{{ percent_label(column.missing_percent) }}</span><small>{{ column.missing_count | count }}</small></div><div class="missing-track" aria-hidden="true"><div style="width: {{ column.missing_percent }}%"></div></div>{% endif %}</td>
134
+ <td class="tabular" data-order="{{ column.distinct_count }}" data-search="{{ column.distinct_count }}">{{ column.distinct_count | count }}</td>
135
+ <td data-order="{{ column.inferred_type }}" data-search="{{ column.inferred_type }}"><span class="type-badge badge-{{ column.inferred_type }}">{{ column.inferred_type }}</span></td>
136
+ <td data-order="{{ column.semantic_type or '(none)' }}" data-search="{{ column.semantic_type or '(none)' }}">{% if column.semantic_type %}<span class="type-badge semantic-badge semantic-{{ column.semantic_type }}">{{ column.semantic_type }}</span>{% endif %}</td>
137
+ <td class="percentage-column" data-order="{{ column.type_error_percent if column.type_error_percent is not none else -1 }}" data-search="{{ column.type_error_percent if column.type_error_percent is not none else '' }}">{% if column.type_error_percent is not none and column.type_error_percent != 0 %}<div class="percentage-value"><span>{{ percent_label(column.type_error_percent) }}</span><small>{{ column.type_error_count | count }}</small></div><div class="percentage-track track-danger" aria-hidden="true"><div style="width: {{ column.type_error_percent }}%"></div></div>{% endif %}</td>
138
+ <td class="examples" tabindex="0" aria-describedby="{{ column.id }}-examples-tooltip">
139
+ {% set inline_count = report.config.value_examples.inline_display_size %}
140
+ {% for item in column.value_profile.values[:inline_count] %}<span class="example-value">{{ item.value }}</span>{% else %}<span class="muted">No present values</span>{% endfor %}
141
+ {% set remaining = column.value_profile.values[inline_count:] %}
142
+ {% if remaining %}<span class="example-more" aria-hidden="true">{{ '++' if column.value_profile.selection != 'complete' else '+' ~ (remaining | length) }}</span>{% endif %}
143
+ {% if column.value_profile.values %}
144
+ <div id="{{ column.id }}-examples-tooltip" class="examples-tooltip" role="tooltip">
145
+ <div class="examples-tooltip-title">Values <span>{{ column.value_profile.values | length }}{% if column.value_profile.selection != 'complete' %} / {{ column.distinct_count | count }}{% endif %}</span></div>
146
+ <div class="examples-tooltip-list">
147
+ {% for item in column.value_profile.values %}<div><span>{{ item.value }}</span><small>({{ item.count | count }})</small></div>{% endfor %}
148
+ </div>
149
+ </div>
150
+ {% endif %}
151
+ </td>
152
+ </tr>
153
+ {% endfor %}
154
+ </tbody>
155
+ </table>
156
+ </div>
157
+ <p id="filter-result" class="metadata filter-result" data-table-status="columns-table" role="status" hidden></p>
158
+ </details>
159
+
160
+ <details id="transformations" class="report-details secondary-details transformation-details">
161
+ <summary>Transformations <span class="section-count">({{ report.columns | length }})</span></summary>
162
+ <div class="table-actions details-actions"><button type="button" class="btn btn-sm btn-outline-secondary table-reset" data-reset-table="transformations-table" hidden>Reset</button></div>
163
+ <div class="table-responsive secondary-table-wrap" tabindex="0" role="region" aria-label="Value transformations">
164
+ <table id="transformations-table" class="table table-sm transformations-table">
165
+ <thead><tr><th scope="col">Column</th><th scope="col" class="percentage-column">Missing (%)</th><th scope="col" class="percentage-column">Trimmed (%)</th><th scope="col" class="percentage-column">Whitespace collapsed (%)</th><th scope="col">Distinct</th></tr></thead>
166
+ <tbody>
167
+ {% for column in report.columns %}
168
+ <tr>
169
+ <th scope="row" data-order="{{ column.name }}" data-search="#{{ column.position }} {{ column.name or '(unnamed)' }}"><span class="column-index">{{ column.position }}</span><span class="column-name">{{ column.name or '(unnamed)' }}</span></th>
170
+ {% for percent, count, track in [(column.missing_percent, column.missing_count, 'danger'), (column.normalization.trim_percent, column.normalization.trim_count, 'concept'), (column.normalization.collapse_internal_whitespace_percent, column.normalization.collapse_internal_whitespace_count, 'concept')] %}
171
+ <td class="percentage-column" data-order="{{ percent }}" data-search="{{ percent }}">{% if percent != 0 %}<div class="percentage-value"><span>{{ percent_label(percent) }}</span><small>{{ count | count }}</small></div><div class="percentage-track track-{{ track }}" aria-hidden="true"><div style="width: {{ percent }}%"></div></div>{% endif %}</td>
172
+ {% endfor %}
173
+ <td class="tabular" data-order="{{ column.distinct_count }}" data-search="{{ column.distinct_count }}">{{ column.distinct_count | count }}</td>
174
+ </tr>
175
+ {% endfor %}
176
+ </tbody>
177
+ </table>
178
+ </div>
179
+ <p class="metadata filter-result" data-table-status="transformations-table" role="status" hidden></p>
180
+ </details>
181
+
182
+ {% if numeric_columns %}
183
+ <details id="numeric" class="report-details secondary-details numeric-details">
184
+ <summary>Numeric analysis <span class="section-count">({{ numeric_columns | length }})</span></summary>
185
+ <div class="table-actions details-actions"><button type="button" class="btn btn-sm btn-outline-secondary table-reset" data-reset-table="numeric-table" hidden>Reset</button></div>
186
+ <div class="table-responsive secondary-table-wrap" tabindex="0" role="region" aria-label="Numeric statistics">
187
+ <table id="numeric-table" class="table table-sm numeric-table">
188
+ <thead><tr><th scope="col">Column</th><th scope="col">Minimum</th><th scope="col">Maximum</th><th scope="col">Mean</th><th scope="col">Median</th></tr></thead>
189
+ <tbody>{% for column in numeric_columns %}<tr><th scope="row" data-order="{{ column.name }}" data-search="#{{ column.position }} {{ column.name or '(unnamed)' }}"><span class="column-index">{{ column.position }}</span><span class="column-name">{{ column.name or '(unnamed)' }}</span></th>{% for value in [column.numeric.minimum, column.numeric.maximum, column.numeric.mean, column.numeric.median] %}<td data-order="{{ value }}" data-search="{{ value }}">{{ value | number }}</td>{% endfor %}</tr>{% endfor %}</tbody>
190
+ </table>
191
+ </div>
192
+ <p class="metadata filter-result" data-table-status="numeric-table" role="status" hidden></p>
193
+ </details>
194
+ {% endif %}
195
+
196
+ {% if date_columns %}
197
+ <details id="dates" class="report-details secondary-details date-details">
198
+ <summary>Date analysis <span class="section-count">({{ date_columns | length }})</span></summary>
199
+ <div class="table-actions details-actions"><button type="button" class="btn btn-sm btn-outline-secondary table-reset" data-reset-table="date-table" hidden>Reset</button></div>
200
+ <div class="table-responsive secondary-table-wrap" tabindex="0" role="region" aria-label="Date analysis">
201
+ <table id="date-table" class="table table-sm date-table">
202
+ <thead><tr><th scope="col">Column</th><th scope="col">Status</th><th scope="col" class="percentage-column">Valid (%)</th><th scope="col" class="percentage-column">Ambiguous (%)</th><th scope="col" class="percentage-column">Invalid (%)</th><th scope="col" class="percentage-column">Other (%)</th><th scope="col">Formats</th></tr></thead>
203
+ <tbody>
204
+ {% for column in date_columns %}
205
+ <tr>
206
+ <th scope="row" data-order="{{ column.name }}" data-search="#{{ column.position }} {{ column.name or '(unnamed)' }}"><span class="column-index">{{ column.position }}</span><span class="column-name">{{ column.name or '(unnamed)' }}</span></th>
207
+ <td data-order="{{ column.date_profile.status }}" data-search="{{ column.date_profile.status }}"><span class="type-badge date-status status-{{ column.date_profile.status }}">{{ column.date_profile.status | replace('_', ' ') }}</span></td>
208
+ {% set present_count = column.date_profile.valid_count + column.date_profile.ambiguous_count + column.date_profile.invalid_date_count + column.date_profile.not_date_count %}
209
+ {% for value, track in [(column.date_profile.valid_count, 'success'), (column.date_profile.ambiguous_count, 'accent'), (column.date_profile.invalid_date_count, 'danger'), (column.date_profile.not_date_count, 'muted')] %}{% set percentage = ((100 * value / present_count) | round(2)) if present_count else 0 %}<td class="percentage-column" data-order="{{ percentage }}" data-search="{{ percentage }}">{% if percentage != 0 %}<div class="percentage-value"><span>{{ percent_label(percentage) }}</span><small>{{ value | count }}</small></div><div class="percentage-track track-{{ track }}" aria-hidden="true"><div style="width: {{ percentage }}%"></div></div>{% endif %}</td>{% endfor %}
210
+ <td class="date-formats-cell" tabindex="0" aria-describedby="{{ column.id }}-date-formats-tooltip">
211
+ {% set variant_label = column.date_profile.format_count ~ (' variant' if column.date_profile.format_count == 1 else ' variants') %}
212
+ <span class="date-format-count">{{ variant_label }}</span>
213
+ <div id="{{ column.id }}-date-formats-tooltip" class="date-formats-tooltip" role="tooltip">
214
+ <div class="examples-tooltip-title">{{ variant_label }}</div>
215
+ <div class="examples-tooltip-list">
216
+ {% for item in column.date_profile.breakdown %}<div class="date-breakdown-{{ item.category }}"><span>{{ item.label }}</span><small>{{ item.count | count }} ({{ item.percent }}%)</small></div>{% endfor %}
217
+ </div>
218
+ </div>
219
+ </td>
220
+ </tr>
221
+ {% endfor %}
222
+ </tbody>
223
+ </table>
224
+ </div>
225
+ <p class="metadata filter-result" data-table-status="date-table" role="status" hidden></p>
226
+ </details>
227
+ {% endif %}
228
+
229
+ {% if string_columns %}
230
+ <details id="strings" class="report-details secondary-details string-details">
231
+ <summary>String analysis <span class="section-count">({{ string_columns | length }})</span></summary>
232
+ <div class="table-actions details-actions"><button type="button" class="btn btn-sm btn-outline-secondary table-reset" data-reset-table="string-table" hidden>Reset</button></div>
233
+ <div class="table-responsive secondary-table-wrap" tabindex="0" role="region" aria-label="String analysis">
234
+ <table id="string-table" class="table table-sm string-table">
235
+ <thead><tr><th scope="col">Column</th><th scope="col">Class</th><th scope="col">Fixed</th><th scope="col">Min</th><th scope="col">Max</th><th scope="col">Mean</th><th scope="col">Median</th><th scope="col">Distinct</th><th scope="col">Examples</th></tr></thead>
236
+ <tbody>
237
+ {% for column in string_columns %}
238
+ {% set fixed = column.string_profile.fixed_length is not none %}
239
+ <tr>
240
+ <th scope="row" data-order="{{ column.name }}" data-search="#{{ column.position }} {{ column.name or '(unnamed)' }}"><span class="column-index">{{ column.position }}</span><span class="column-name">{{ column.name or '(unnamed)' }}</span></th>
241
+ <td data-order="{{ column.string_profile.status }}" data-search="{{ column.string_profile.status }}"><span class="type-badge string-status string-{{ column.string_profile.status }}">{{ column.string_profile.status | replace('_', ' ') }}</span></td>
242
+ <td data-order="{{ column.string_profile.fixed_length if fixed else -1 }}" data-search="{{ column.string_profile.fixed_length if fixed else '' }}">{% if fixed %}{{ column.string_profile.fixed_length | count }}{% endif %}</td>
243
+ <td data-order="{{ -1 if fixed else column.string_profile.minimum_length }}" data-search="{{ '' if fixed else column.string_profile.minimum_length }}">{% if not fixed %}{{ column.string_profile.minimum_length | count }}{% endif %}</td>
244
+ <td data-order="{{ -1 if fixed else column.string_profile.maximum_length }}" data-search="{{ '' if fixed else column.string_profile.maximum_length }}">{% if not fixed %}{{ column.string_profile.maximum_length | count }}{% endif %}</td>
245
+ <td data-order="{{ -1 if fixed else column.string_profile.mean_length }}" data-search="{{ '' if fixed else column.string_profile.mean_length }}">{% if not fixed %}{{ column.string_profile.mean_length | number }}{% endif %}</td>
246
+ <td data-order="{{ -1 if fixed else column.string_profile.median_length }}" data-search="{{ '' if fixed else column.string_profile.median_length }}">{% if not fixed %}{{ column.string_profile.median_length | number }}{% endif %}</td>
247
+ <td data-order="{{ column.distinct_count }}" data-search="{{ column.distinct_count }}">{{ column.distinct_count | count }}</td>
248
+ <td class="examples length-examples" tabindex="0"{% if column.string_profile.length_distribution %} aria-describedby="{{ column.id }}-lengths-tooltip"{% endif %}>
249
+ {% set representative_lengths = column.string_profile.length_distribution | sort(attribute='count', reverse=true) %}
250
+ {% for item in representative_lengths[:3] %}{% if item.examples %}<span class="example-value">{{ item.examples[0].value }}</span>{% endif %}{% endfor %}
251
+ {% if representative_lengths | length > 3 %}<span class="example-more" aria-hidden="true">+{{ (representative_lengths | length) - 3 }}</span>{% endif %}
252
+ {% if column.string_profile.length_distribution %}
253
+ <div id="{{ column.id }}-lengths-tooltip" class="examples-tooltip length-examples-tooltip" role="tooltip">
254
+ <div class="examples-tooltip-title">Lengths <span>{{ column.string_profile.distinct_length_count }}</span></div>
255
+ <div class="examples-tooltip-list">
256
+ {% for item in representative_lengths %}<div class="length-tooltip-row"><div class="length-tooltip-copy"><strong>{{ item.length }} character{{ '' if item.length == 1 else 's' }}</strong>{% if item.examples %}<span>{% for example in item.examples[:3] %}{{ example.value }}{% if not loop.last %} / {% endif %}{% endfor %}</span>{% endif %}</div><div class="length-tooltip-frequency"><div class="percentage-value"><span>{{ item.percent }}%</span><small>{{ item.count | count }}</small></div><div class="percentage-track track-concept" aria-hidden="true"><div style="width: {{ item.percent }}%"></div></div></div></div>{% endfor %}
257
+ </div>
258
+ </div>
259
+ {% endif %}
260
+ </td>
261
+ </tr>
262
+ {% endfor %}
263
+ </tbody>
264
+ </table>
265
+ </div>
266
+ <p class="metadata filter-result" data-table-status="string-table" role="status" hidden></p>
267
+ </details>
268
+ {% endif %}
269
+ </section>
270
+
271
+ <section class="report-section">
272
+ <details id="sample" class="report-details secondary-details sample-details">
273
+ <summary>Data sample</summary>
274
+ <div class="table-actions details-actions"><span class="metadata">{{ report.preview | length }} of {{ report.summary.row_count | count }} rows</span>{% if report.preview %}<button type="button" class="btn btn-sm btn-outline-secondary table-reset" data-reset-table="sample-table" hidden>Reset</button>{% endif %}</div>
275
+ {% if report.preview %}
276
+ <div class="table-responsive sample-scroll" tabindex="0" role="region" aria-label="Raw data sample">
277
+ <table id="sample-table" class="table table-sm table-striped sample-table">
278
+ <thead><tr><th scope="col" class="row-number" data-value-type="integer">Row</th>{% for column in report.columns %}<th scope="col" data-value-type="{{ column.inferred_type }}">{{ column.name or '(unnamed)' }}</th>{% endfor %}</tr></thead>
279
+ <tbody>{% for row in report.preview %}<tr><th scope="row" class="row-number" data-order="{{ row.row_number }}" data-search="{{ row.row_number }}">{{ row.row_number }}</th>{% for value in row.values %}<td data-order="{{ value }}" data-search="{{ value }}">{{ value }}</td>{% endfor %}</tr>{% endfor %}</tbody>
280
+ </table>
281
+ </div>
282
+ <p class="metadata filter-result" data-table-status="sample-table" role="status" hidden></p>
283
+ {% else %}<p class="empty-state">{{ 'No data records.' if report.summary.row_count == 0 else 'Data preview disabled.' }}</p>{% endif %}
284
+ </details>
285
+ </section>
286
+
287
+ <section class="report-section methodology">
288
+ <details id="settings" class="report-details methodology-details">
289
+ <summary>Analysis settings</summary>
290
+ <dl class="settings-grid">
291
+ <div><dt>Scope</dt><dd>All {{ report.summary.row_count | count }} records</dd></div>
292
+ <div><dt>Missing-value markers</dt><dd>{% for marker in report.config.missing_values %}<code>{{ marker | tojson }}</code> {% else %}None{% endfor %}</dd></div>
293
+ <div><dt>Missing-value comparison</dt><dd>Whitespace trimmed; case-sensitive</dd></div>
294
+ <div><dt>Value normalization</dt><dd>Trim: {{ 'enabled' if report.config.normalization.trim else 'disabled' }}; collapse internal horizontal whitespace: {{ 'enabled' if report.config.normalization.collapse_internal_whitespace else 'disabled' }}. Raw preview values are preserved.</dd></div>
295
+ <div><dt>Duplicate comparison</dt><dd>Exact raw values across every column</dd></div>
296
+ <div><dt>Type inference</dt><dd>At least {{ (report.config.type_inference.minimum_confidence * 100) | round(1) }}% agreement across present values; configured strict date formats, dot-decimal numbers and true/false. Leading-zero identifiers remain text.</dd></div>
297
+ <div><dt>Date detection</dt><dd>Orders: {{ report.config.date_detection.orders | join(', ') }}; separators: {% for separator in report.config.date_detection.separators %}<code>{{ separator | tojson }}</code> {% endfor %}; ambiguous order: {{ report.config.date_detection.ambiguous_order or 'inferred only from unambiguous column evidence' }}</dd></div>
298
+ <div><dt>Enum candidates</dt><dd>Text columns with at most {{ report.config.enum_detection.maximum_distinct_values }} values when the dataset has at least {{ report.config.enum_detection.minimum_row_count | count }} rows</dd></div>
299
+ <div><dt>String lengths</dt><dd>Very short through {{ report.config.string_analysis.very_short_max_length }}; short through {{ report.config.string_analysis.short_max_length }}; medium through {{ report.config.string_analysis.medium_max_length }}; long through {{ report.config.string_analysis.long_max_length }}; otherwise very long. Up to {{ report.config.string_analysis.examples_per_length }} examples per length are retained when the maximum is {{ report.config.string_analysis.length_distribution_max_length }} or less.</dd></div>
300
+ <div><dt>Value representation</dt><dd>Complete through {{ report.config.value_examples.full_distribution_max_distinct }} distinct values; otherwise a reproducible sample of {{ report.config.value_examples.candidate_sample_size }}</dd></div>
301
+ <div><dt>Row numbering</dt><dd>Data records start at 1, excluding the header. Quoted multiline values count as one record.</dd></div>
302
+ <div><dt>Preview selection</dt><dd>First {{ report.config.preview_rows }} records; raw values preserved</dd></div>
303
+ <div><dt>JSON format</dt><dd>{{ report.format_version }} - revision {{ report.format_revision }} (experimental)</dd></div>
304
+ </dl>
305
+ <p class="metadata source-hash">Source SHA-256: <code>{{ report.source.sha256 }}</code></p>
306
+ </details>
307
+ </section>
308
+ </main>
309
+ <footer class="footer"><div class="report-container footer-inner"><span>Tabalyst {{ tabalyst_version }}</span><span><a href="https://github.com/loribel-labs/tabalyst">GitHub repository</a> &middot; Gregory Borelli - Catalyseur Num&#233;rique</span></div></footer>
310
+ </div>
311
+ </div>
312
+ <script src="https://cdn.datatables.net/3.0.4/js/dataTables.min.js" integrity="sha384-i3qKUispup6yO0B16Bvnk2AmVi8O60Qww2omjtD9NHi1ns1H4jiz2EhYBjLew/9C" crossorigin="anonymous"></script>
313
+ <script src="https://cdn.datatables.net/3.0.4/js/dataTables.bootstrap5.min.js" integrity="sha384-4d8X9sr6Gnv9AgIQn6bv3lmQxj5fD+9bVAun0/XMmdy7oPRvT0adfiUUiiYpi4Ck" crossorigin="anonymous"></script>
314
+ <script src="https://cdn.datatables.net/columncontrol/2.0.2/js/dataTables.columnControl.min.js" integrity="sha384-s+uWQ72aUBdD8WMvuzuM/dgwuDT+sZo4mNvlZ9AAH6iZlX9hpJX6t4R7jbK26LZN" crossorigin="anonymous"></script>
315
+ <script src="https://cdn.datatables.net/columncontrol/2.0.2/js/columnControl.bootstrap5.min.js" integrity="sha384-1QCMHcxcmlVHzTGWujfsVkg2KuWc4Mtd4lly3TIbzGhIwP0Bw3OQuJ5Po7oPN5sn" crossorigin="anonymous"></script>
316
+ <script>{{ report_js | safe }}</script>
317
+ </body>
318
+ </html>
@@ -0,0 +1,208 @@
1
+ Metadata-Version: 2.4
2
+ Name: tabalyst
3
+ Version: 0.1.0
4
+ Summary: Open-source data profiling engine for structured JSON and customizable HTML reports.
5
+ Author: Gregory Borelli
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/loribel-labs/tabalyst
8
+ Project-URL: Repository, https://github.com/loribel-labs/tabalyst.git
9
+ Project-URL: Issues, https://github.com/loribel-labs/tabalyst/issues
10
+ Keywords: csv,data-analysis,data-profiling,data-quality,eda,html-report,json
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
21
+ Requires-Python: >=3.11
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: jinja2>=3.1
25
+ Requires-Dist: pandas>=2.2
26
+ Requires-Dist: pydantic>=2.10
27
+ Requires-Dist: typer>=0.12
28
+ Provides-Extra: dev
29
+ Requires-Dist: build>=1.2; extra == "dev"
30
+ Requires-Dist: pytest>=8; extra == "dev"
31
+ Requires-Dist: pytest-cov>=6; extra == "dev"
32
+ Requires-Dist: ruff>=0.11; extra == "dev"
33
+ Dynamic: license-file
34
+
35
+ # Tabalyst
36
+
37
+ Tabalyst is a local-first CSV profiling library and command-line tool. It reads a
38
+ CSV file once, produces a structured JSON profile, and renders the same result as
39
+ an interactive HTML report.
40
+
41
+ Version `0.1.0` is the first packaged public API. The analysis format is still
42
+ young, so larger schema changes are reserved for future minor releases.
43
+
44
+ ## Requirements and installation
45
+
46
+ Tabalyst supports Python 3.11, 3.12, 3.13, and 3.14.
47
+
48
+ ```console
49
+ pip install tabalyst
50
+ ```
51
+
52
+ For local development from a clone:
53
+
54
+ ```console
55
+ python -m venv .venv
56
+ python -m pip install -e ".[dev]"
57
+ ```
58
+
59
+ ## Command line
60
+
61
+ Pass the CSV source and the complete HTML report path as positional arguments:
62
+
63
+ ```console
64
+ tabalyst data.csv reports/client-a.html
65
+ ```
66
+
67
+ The report path must end in `.html`. Parent directories are created
68
+ automatically, and existing artifacts are replaced. A successful run creates:
69
+
70
+ ```text
71
+ reports/
72
+ |-- client-a.html
73
+ |-- client-a.json
74
+ `-- executions.json
75
+ ```
76
+
77
+ The JSON profile always shares the HTML filename stem. `executions.json` keeps a
78
+ cumulative history for successful analyses in the same report directory, so
79
+ several named reports can coexist there.
80
+
81
+ Common options are:
82
+
83
+ ```console
84
+ tabalyst data.csv reports/client-a.html --separator ";" --encoding cp1252
85
+ tabalyst data.csv reports/client-a.html --config tabalyst.json
86
+ tabalyst --help
87
+ tabalyst --version
88
+ ```
89
+
90
+ `python -m tabalyst` accepts the same arguments.
91
+
92
+ ## Python API
93
+
94
+ ```python
95
+ import tabalyst
96
+
97
+ result = tabalyst.analyze(
98
+ "data.csv",
99
+ "reports/client-a.html",
100
+ separator=";",
101
+ encoding="cp1252",
102
+ config_path="tabalyst.json",
103
+ )
104
+
105
+ print(tabalyst.__version__)
106
+ print(result["summary"]["row_count"])
107
+ ```
108
+
109
+ `analyze()` returns a JSON-serializable dictionary whose logical content matches
110
+ the adjacent JSON file. The HTML is rendered from that canonical JSON profile.
111
+ Expected failures derive from `tabalyst.TabalystError`; callers may distinguish
112
+ `InputError`, `ConfigurationError`, and `ReportError`.
113
+
114
+ ## Configuration
115
+
116
+ Configuration files are strict JSON. Unknown names and invalid values are errors.
117
+ The existing analysis settings remain available; the smallest useful file is:
118
+
119
+ ```json
120
+ {
121
+ "csv": {
122
+ "delimiter": ";",
123
+ "encoding": "cp1252"
124
+ }
125
+ }
126
+ ```
127
+
128
+ Resolution order is:
129
+
130
+ 1. an explicit `separator` or `encoding` argument;
131
+ 2. the file passed with `config_path` or `--config`;
132
+ 3. Tabalyst defaults: comma and `utf-8-sig`.
133
+
134
+ `utf-8-sig` accepts ordinary UTF-8 and UTF-8 with a byte-order mark. See the
135
+ [configuration reference](https://github.com/loribel-labs/tabalyst/blob/main/docs/configuration.md)
136
+ for analysis, normalization,
137
+ date, type-inference, sampling, and enum settings.
138
+
139
+ ## Public examples
140
+
141
+ The repository contains two reproducible examples:
142
+
143
+ - `basic`: five rows covering common CSV values and one duplicate row.
144
+ - `insurance-customers`: 3,000 synthetic customer and contract records across
145
+ 34 columns. Names and contact details are fictional, and email addresses use
146
+ reserved `.example` domains.
147
+
148
+ Each dataset under `examples/input/` has a matching generated report under
149
+ `examples/output/`. Regenerate both with:
150
+
151
+ ```console
152
+ tabalyst examples/input/basic.csv examples/output/basic/report.html --config examples/config.json
153
+ tabalyst examples/input/insurance-customers.csv examples/output/insurance-customers/report.html --config examples/config.json
154
+ ```
155
+
156
+ See the complete layout and maintenance notes in the
157
+ [examples README](https://github.com/loribel-labs/tabalyst/blob/main/examples/README.md).
158
+
159
+ ## What the report contains
160
+
161
+ - Dataset dimensions, missing cells, duplicate rows, and quality observations.
162
+ - Physical and semantic type inference with explicit confidence and error rates.
163
+ - Numeric, date, string-length, normalization, and value-distribution profiles.
164
+ - A bounded raw-data preview while all records are analyzed.
165
+ - Sortable and filterable HTML tables.
166
+
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.
172
+
173
+ ## Compatibility with alpha commands
174
+
175
+ The earlier commands remain available during the `0.1.x` transition:
176
+
177
+ ```console
178
+ tabalyst analyze data.csv -o reports/client-a.html
179
+ tabalyst render reports/client-a.json -o reports/regenerated.html
180
+ ```
181
+
182
+ The alpha `analyze` form also retains automatic `tabalyst.json` discovery,
183
+ multiple `--config` overrides, and `--preview-rows`. New integrations should use
184
+ the direct command or `tabalyst.analyze()`.
185
+
186
+ ## Development and packaging
187
+
188
+ ```console
189
+ python -m pytest
190
+ python -m ruff check .
191
+ python -m build
192
+ ```
193
+
194
+ The build creates a wheel and source distribution under `dist/`. Release steps,
195
+ including the clean-wheel smoke test and PyPI Trusted Publishing setup, are in
196
+ [RELEASING.md](https://github.com/loribel-labs/tabalyst/blob/main/RELEASING.md).
197
+
198
+ Report format details and internal boundaries are documented in
199
+ [docs/architecture.md](https://github.com/loribel-labs/tabalyst/blob/main/docs/architecture.md).
200
+ Please report defects through the
201
+ [GitHub issue tracker](https://github.com/loribel-labs/tabalyst/issues).
202
+
203
+ ## License
204
+
205
+ Tabalyst is released under the
206
+ [MIT License](https://github.com/loribel-labs/tabalyst/blob/main/LICENSE).
207
+
208
+ Created by Gregory Borelli - Catalyseur Numérique.
@@ -0,0 +1,21 @@
1
+ tabalyst/__init__.py,sha256=Mnd_rUa264Lrjm4EjejB19z48CLIKZTqbRs5P0x7bs8,670
2
+ tabalyst/__main__.py,sha256=so367Wyp5v19V29BVRNOKiQVRey-5PP0jXfMChAFpTY,147
3
+ tabalyst/_version.py,sha256=DOg96CtpeaSyuQjWnv8ONJHbVMgvhVE5ujOAyOpbd74,845
4
+ tabalyst/analysis.py,sha256=C3P7FURHSlLDPrNr3uVrwZgbPs2xPXbaWUND9HTRPtU,27324
5
+ tabalyst/cli.py,sha256=vv1OBb4HGou_q5YMpsCsrlgZXWxYqF2be22kF-1vuUU,7386
6
+ tabalyst/config.py,sha256=OyDXo-CqLGT77K4--d626gQGounZVVloYZuRzcP6CuA,7443
7
+ tabalyst/errors.py,sha256=AVUSmMsg6VO8HvAuscCA2y_O-3ObMdLoN-B9tKLmUrA,472
8
+ tabalyst/execution_log.py,sha256=qzUmFzOPL4SpljAjuTurDSrNkxa2qB3kFYy5qfnWdxE,3246
9
+ tabalyst/ingestion.py,sha256=UOzQknmPT1J4z179Nf4qMEd0ciT8BX3EP5h6s-R0bH4,2623
10
+ tabalyst/models.py,sha256=hEFpRbu4xntwWYlob0Hh3-JlW7VjCnAdoRKK8M41SVg,4221
11
+ tabalyst/reporting.py,sha256=7hRkDclYfiqpEPB0GS7INpj5nqkoHsPlMqTTOHlmKpQ,2087
12
+ tabalyst/service.py,sha256=Yunm5I9toeRTKgioeTUB1JfMBxkKewW98NIIuyk2Y9I,4784
13
+ tabalyst/static/report.js,sha256=fVqdaDHSgu0ylGIrFA-aj8Gen6UoBA1pQk-uTuSf7To,21681
14
+ tabalyst/static/theme.css,sha256=Y2FhE2J-hACxKC5EvXrM-fgIdJFaTmh0LWorFyYBqsY,34141
15
+ tabalyst/templates/report.html,sha256=bfFLgw8IbBAYNDybQ970YqSMxvAr7_2yS8dCzAW3h3M,30023
16
+ tabalyst-0.1.0.dist-info/licenses/LICENSE,sha256=GwXpC_s4juQ19LaCaZIub0SpN7Lerga7l8bFTkdC61g,1065
17
+ tabalyst-0.1.0.dist-info/METADATA,sha256=F9oHZyC08NR2FjIlKhWEfbXWVXcetMMRd1XnzZ8Cqqk,6841
18
+ tabalyst-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
19
+ tabalyst-0.1.0.dist-info/entry_points.txt,sha256=CJDjeWPjqMlkzESvHE6PQ4c6FMKG-JGmgLUa5RsBExU,47
20
+ tabalyst-0.1.0.dist-info/top_level.txt,sha256=Ny0-NR54GcG6Jlcf1V-UaT9jh8-jwyvDocc39DJ6aLo,9
21
+ tabalyst-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tabalyst = tabalyst.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 chergreg
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ tabalyst