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.
- tabalyst/__init__.py +20 -0
- tabalyst/__main__.py +6 -0
- tabalyst/_version.py +23 -0
- tabalyst/analysis.py +786 -0
- tabalyst/cli.py +208 -0
- tabalyst/config.py +214 -0
- tabalyst/errors.py +17 -0
- tabalyst/execution_log.py +102 -0
- tabalyst/ingestion.py +86 -0
- tabalyst/models.py +174 -0
- tabalyst/reporting.py +54 -0
- tabalyst/service.py +134 -0
- tabalyst/static/report.js +500 -0
- tabalyst/static/theme.css +411 -0
- tabalyst/templates/report.html +318 -0
- tabalyst-0.1.0.dist-info/METADATA +208 -0
- tabalyst-0.1.0.dist-info/RECORD +21 -0
- tabalyst-0.1.0.dist-info/WHEEL +5 -0
- tabalyst-0.1.0.dist-info/entry_points.txt +2 -0
- tabalyst-0.1.0.dist-info/licenses/LICENSE +21 -0
- tabalyst-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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">☼</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> · Gregory Borelli - Catalyseur Numé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,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
|