datalake-doco 0.1.27__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.
- datalake_doco-0.1.27/.gitignore +11 -0
- datalake_doco-0.1.27/PKG-INFO +420 -0
- datalake_doco-0.1.27/README.md +406 -0
- datalake_doco-0.1.27/pyproject.toml +42 -0
- datalake_doco-0.1.27/src/datalake_doco/__init__.py +0 -0
- datalake_doco-0.1.27/src/datalake_doco/cli.py +165 -0
- datalake_doco-0.1.27/src/datalake_doco/config.py +31 -0
- datalake_doco-0.1.27/src/datalake_doco/freshness.py +50 -0
- datalake_doco-0.1.27/src/datalake_doco/frontend/app.js +651 -0
- datalake_doco-0.1.27/src/datalake_doco/frontend/index.html +26 -0
- datalake_doco-0.1.27/src/datalake_doco/frontend/style.css +401 -0
- datalake_doco-0.1.27/src/datalake_doco/markdown.py +291 -0
- datalake_doco-0.1.27/src/datalake_doco/models.py +94 -0
- datalake_doco-0.1.27/src/datalake_doco/profiler.py +167 -0
- datalake_doco-0.1.27/src/datalake_doco/readers/__init__.py +20 -0
- datalake_doco-0.1.27/src/datalake_doco/readers/delta.py +196 -0
- datalake_doco-0.1.27/src/datalake_doco/scanner.py +56 -0
- datalake_doco-0.1.27/src/datalake_doco/timeseries.py +145 -0
- datalake_doco-0.1.27/src/datalake_doco/writer.py +76 -0
- datalake_doco-0.1.27/tests/conftest.py +45 -0
- datalake_doco-0.1.27/tests/test_cli.py +289 -0
- datalake_doco-0.1.27/tests/test_freshness.py +83 -0
- datalake_doco-0.1.27/tests/test_markdown.py +271 -0
- datalake_doco-0.1.27/tests/test_profiler.py +267 -0
- datalake_doco-0.1.27/tests/test_reader.py +295 -0
- datalake_doco-0.1.27/tests/test_scanner.py +79 -0
- datalake_doco-0.1.27/tests/test_timeseries.py +122 -0
- datalake_doco-0.1.27/tests/test_type_mapping.py +50 -0
- datalake_doco-0.1.27/tests/test_writer.py +196 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: datalake-doco
|
|
3
|
+
Version: 0.1.27
|
|
4
|
+
Summary: Static documentation generator for datalakes
|
|
5
|
+
Project-URL: Homepage, https://github.com/leprechaun/static-datalake-doco
|
|
6
|
+
Project-URL: Repository, https://github.com/leprechaun/static-datalake-doco
|
|
7
|
+
Project-URL: Issues, https://github.com/leprechaun/static-datalake-doco/issues
|
|
8
|
+
Requires-Python: >=3.13
|
|
9
|
+
Requires-Dist: deltalake>=0.19
|
|
10
|
+
Requires-Dist: fsspec>=2024.2
|
|
11
|
+
Requires-Dist: polars>=1.0
|
|
12
|
+
Requires-Dist: s3fs>=2024.2
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# datalake-doco
|
|
16
|
+
|
|
17
|
+
[](https://qlty.sh/gh/leprechaun/projects/static-datalake-doco)
|
|
18
|
+
[](https://qlty.sh/gh/leprechaun/projects/static-datalake-doco)
|
|
19
|
+
|
|
20
|
+
## Purpose
|
|
21
|
+
|
|
22
|
+
<img width="300" alt="Image" src="https://github.com/user-attachments/assets/553ee789-b0fe-45be-925b-7e920024cba3" />
|
|
23
|
+
<img width="300" alt="Image" src="https://github.com/user-attachments/assets/2c53cfb7-a966-45b2-8b7b-69ef3cb4d8dc" />
|
|
24
|
+
<img width="300" alt="Image" src="https://github.com/user-attachments/assets/11826c47-98db-4b6a-b3ac-3a97e4149f9a" />
|
|
25
|
+
<img width="300" alt="Image" src="https://github.com/user-attachments/assets/fcd5b2df-2e60-46c8-b8fb-6e2e9c22f07e" />
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
a.k.a. "Poor Man's Data Catalogue"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This tool will generate a data catalogue after scanning an S3 bucket. The catalogue is a set of OpenMetadata-looking JSON files, and a catalogue. There's also a static HTML page that can be used to view the catalogue as human being.
|
|
32
|
+
|
|
33
|
+
Why? Because it's easy, quick, and does not require spinning up any sort of machinery.
|
|
34
|
+
|
|
35
|
+
Each table's documentation includes the schema, row count, version history, statistics, and a time distribution for each date/timestamp column. Sample data is off by default; turn it on with `--sample-data-count`, and leave out individual tables with `--sample-data-exclude-tables` in case there are concerns with data sensitivity.
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
`datalake-doco generate` scans one or more filesystem or S3 roots for Delta Lake tables, profiles each table, and writes a static JSON catalog plus per-table JSON documents to an output directory. The output is self-contained and can be served by any static HTTP server.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Pipeline
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
URI(s)
|
|
47
|
+
└─ scanner.scan() → list[(table_uri, table_name)]
|
|
48
|
+
└─ writer.init_output_dir() + copy_frontend()
|
|
49
|
+
└─ per table:
|
|
50
|
+
└─ DeltaReader.read() → TableDoc
|
|
51
|
+
├─ profiler.profile() → column statistics + time distributions
|
|
52
|
+
├─ dt.history() → Delta transaction log
|
|
53
|
+
└─ freshness.freshness() → last write vs typical write interval
|
|
54
|
+
└─ writer.write_table_doc() + write_catalog() → tables/**/*.json, catalog.json
|
|
55
|
+
└─ writer.write_llm_docs() → llms.txt + tables/**/*.md
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## CLI
|
|
61
|
+
|
|
62
|
+
### `generate`
|
|
63
|
+
|
|
64
|
+
Scans URIs, profiles all discovered Delta tables, and writes the full output.
|
|
65
|
+
|
|
66
|
+
| Argument / Option | Type | Default | Description |
|
|
67
|
+
|---|---|---|---|
|
|
68
|
+
| `uris` | `list[str]` (positional, 1+) | required | Roots to scan (local path or `s3://...`) |
|
|
69
|
+
| `--output` / `-o` | `Path` | `./datalake-docs` | Output directory |
|
|
70
|
+
| `--sample-data-count` | `int` | `0` | Sample rows per table (`0` = no sample data) |
|
|
71
|
+
| `--sample-data-exclude-tables` | `str` | `""` | Comma-separated `fullyQualifiedName`s to exclude from sample data |
|
|
72
|
+
| `--history-limit` | `int` | `100` | Max history entries read per table (`0` = all). Each entry is a separate commit file fetched from storage, so busy tables with thousands of commits make many requests |
|
|
73
|
+
| `--base-url` | `str` | `None` | Public URL of the output directory. Makes links in `llms.txt` and the table Markdown absolute (`None` = relative links) |
|
|
74
|
+
|
|
75
|
+
**Environment:**
|
|
76
|
+
- `AWS_ENDPOINT_URL_S3` — if set, used as the S3 endpoint for both scanning and reading. Enables MinIO and other S3-compatible stores.
|
|
77
|
+
|
|
78
|
+
**Behaviour:**
|
|
79
|
+
- Tables from all URIs are merged into one catalog.
|
|
80
|
+
- Tables that fail to profile are skipped, with an `[error]` line printed in place of `done`; they do not appear in the catalog.
|
|
81
|
+
- Tables are profiled sequentially; each progress line is printed as soon as profiling starts and completed in place once it finishes.
|
|
82
|
+
- `--sample-data-exclude-tables` is matched against `fullyQualifiedName` (dots, not slashes). Excluded tables are still profiled and appear in the catalog — only sample rows are omitted.
|
|
83
|
+
- The `tables/` subdirectory in the output is deleted and recreated on each run.
|
|
84
|
+
|
|
85
|
+
### `publish`
|
|
86
|
+
|
|
87
|
+
Copies only the frontend assets (`index.html`, `app.js`, `style.css`) to `--output`. Does not re-profile tables. Use this to update the UI without re-scanning the datalake.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Scanner
|
|
92
|
+
|
|
93
|
+
**`scanner.scan(uri, endpoint_url=None) → list[tuple[str, str]]`**
|
|
94
|
+
|
|
95
|
+
Returns `(table_uri, table_name)` pairs for every Delta table found under `uri`.
|
|
96
|
+
|
|
97
|
+
**Detection:** A directory is a Delta table if and only if it contains a child named `_delta_log`. Recursion stops at that point — subdirectories of a Delta table are not visited.
|
|
98
|
+
|
|
99
|
+
**`table_uri`:** Absolute path or full URI of the table root (no trailing slash).
|
|
100
|
+
|
|
101
|
+
**`table_name`:** Path of `table_uri` relative to the scan root, with the root prefix and leading slash stripped. If the table is the root itself, falls back to the final path segment.
|
|
102
|
+
|
|
103
|
+
Examples (root = `/data`):
|
|
104
|
+
|
|
105
|
+
| Table path | `table_name` |
|
|
106
|
+
|---|---|
|
|
107
|
+
| `/data/events` | `events` |
|
|
108
|
+
| `/data/bronze/events` | `bronze/events` |
|
|
109
|
+
| `/data/mydb/bronze/events` | `mydb/bronze/events` |
|
|
110
|
+
|
|
111
|
+
Trailing slashes in `uri` are stripped before processing.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## DeltaReader
|
|
116
|
+
|
|
117
|
+
**`DeltaReader().read(uri, name, sample_count=0, storage_options=None, history_limit=None) → TableDoc`**
|
|
118
|
+
|
|
119
|
+
### Name → identity fields
|
|
120
|
+
|
|
121
|
+
`name` uses `/` as the segment separator (matches scanner output).
|
|
122
|
+
|
|
123
|
+
| `name` input | `TableDoc.name` | `TableDoc.fullyQualifiedName` |
|
|
124
|
+
|---|---|---|
|
|
125
|
+
| `events` | `events` | `events` |
|
|
126
|
+
| `bronze/events` | `events` | `bronze.events` |
|
|
127
|
+
| `mydb/bronze/events` | `events` | `mydb.bronze.events` |
|
|
128
|
+
|
|
129
|
+
Rule: `name` = last `/`-segment; `fullyQualifiedName` = `name` with `/` replaced by `.`.
|
|
130
|
+
|
|
131
|
+
### Description
|
|
132
|
+
|
|
133
|
+
`description` ← the Delta table's metadata description (`DeltaTable.metadata().description`). `None` if absent or empty.
|
|
134
|
+
|
|
135
|
+
### Column schema
|
|
136
|
+
|
|
137
|
+
Each column in the polars schema produces one `Column`:
|
|
138
|
+
|
|
139
|
+
- `name` — column name
|
|
140
|
+
- `dataType` — mapped from polars dtype (see type mapping below)
|
|
141
|
+
- `nullable` — from the Delta schema field, **not** inferred from data
|
|
142
|
+
- `ordinalPosition` — 1-indexed position in the schema
|
|
143
|
+
- `precision`, `scale` — set only for `DECIMAL`; `None` otherwise
|
|
144
|
+
- `partitioned` — whether the column is one of the Delta table's partition columns (also listed in `TableDoc.partitionColumns`)
|
|
145
|
+
- `description` — the Delta field metadata's `description` key, or Spark's `comment` when there is none
|
|
146
|
+
- `tags` — every other field metadata key, such as `unit`; keys written by Delta and Spark themselves (`delta.*`, `__*`) are left out
|
|
147
|
+
|
|
148
|
+
The table page and the Schema tab show the description and tags in a Description column, e.g. "Heart rate (unit: bpm)". The column is omitted when no column is documented.
|
|
149
|
+
|
|
150
|
+
**Type mapping:**
|
|
151
|
+
|
|
152
|
+
| Polars dtype | `dataType` |
|
|
153
|
+
|---|---|
|
|
154
|
+
| `Int8`, `UInt8` | `TINYINT` |
|
|
155
|
+
| `Int16`, `UInt16` | `SMALLINT` |
|
|
156
|
+
| `Int32`, `UInt32` | `INT` |
|
|
157
|
+
| `Int64`, `UInt64` | `BIGINT` |
|
|
158
|
+
| `Float32` | `FLOAT` |
|
|
159
|
+
| `Float64` | `DOUBLE` |
|
|
160
|
+
| `Boolean` | `BOOLEAN` |
|
|
161
|
+
| `String`, `Utf8` | `STRING` |
|
|
162
|
+
| `Date` | `DATE` |
|
|
163
|
+
| `Binary` | `BINARY` |
|
|
164
|
+
| `Datetime(any)` | `TIMESTAMP` |
|
|
165
|
+
| `Decimal(p, s)` | `DECIMAL` (precision=p, scale=s) |
|
|
166
|
+
| `List`, `Array` | `ARRAY` |
|
|
167
|
+
| `Struct` | `STRUCT` |
|
|
168
|
+
| anything else | `str(dtype).upper()` |
|
|
169
|
+
|
|
170
|
+
### Table profile
|
|
171
|
+
|
|
172
|
+
- `rowCount` — total rows in the table (a streaming `pl.len()` over the scan)
|
|
173
|
+
- `columnCount` — number of columns in the schema
|
|
174
|
+
- `timestamp` — ISO 8601 UTC timestamp at the moment of profiling
|
|
175
|
+
- `columnProfile` — one `ColumnProfile` per column
|
|
176
|
+
|
|
177
|
+
### Sample data
|
|
178
|
+
|
|
179
|
+
Produced only when `sample_count > 0`.
|
|
180
|
+
|
|
181
|
+
- `columns` — column names in schema order
|
|
182
|
+
- `rows` — up to `sample_count` rows from the head of the table
|
|
183
|
+
- Cell values are coerced to JSON-safe types: `str`, `int`, `float`, `bool`, or `None`. Date/datetime values are serialised via `.isoformat()`; anything else via `str()`.
|
|
184
|
+
|
|
185
|
+
### Delta history
|
|
186
|
+
|
|
187
|
+
- Stored under `extension["deltaHistory"]` as a list of `HistoryEntry`.
|
|
188
|
+
- Limited to the `history_limit` most-recent entries when set (the CLI defaults to 100, `config.DEFAULT_HISTORY_LIMIT`); all entries otherwise. Freshness is computed from these entries, so a limit below about 100 shortens the window for the typical write interval.
|
|
189
|
+
- `timestamp` is converted from epoch-milliseconds to ISO 8601 UTC string; stored as `""` if absent.
|
|
190
|
+
- `parameters` ← `operationParameters` (empty dict if absent).
|
|
191
|
+
- `description` ← `userMetadata` (`None` if absent/empty).
|
|
192
|
+
- `engine_info` ← `engineInfo` (`None` if absent/empty).
|
|
193
|
+
- If the history list is empty, `extension` is `None`.
|
|
194
|
+
- `commitCount` (top level, not under `extension`) is the latest version + 1: every commit ever made, however many history entries were read and even after old log files are cleaned up.
|
|
195
|
+
|
|
196
|
+
### Freshness
|
|
197
|
+
|
|
198
|
+
`freshness` (also copied to the table's `catalog.json` entry) comes from the data-changing commits in the history (`config.DATA_OPERATIONS`: `WRITE`, `MERGE`, `DELETE`, `UPDATE`):
|
|
199
|
+
|
|
200
|
+
- `lastWrite`, `secondsSinceLastWrite` — the newest data commit, and its age at profiling time
|
|
201
|
+
- `typicalIntervalSeconds` — median of the last `config.FRESHNESS_GAPS` (49) gaps between data commits; `null` with only one commit. Gaps under `config.BURST_GAP_SECONDS` (60) are left out, so a burst of commits such as a backfill counts as one write. A table with fewer than `config.MIN_INTERVAL_GAPS` (3) longer gaps really does write that often, so all its gaps count
|
|
202
|
+
- `burstGapSeconds` — the burst threshold applied, or `null` when all gaps counted
|
|
203
|
+
- `ratio` — `secondsSinceLastWrite / typicalIntervalSeconds`
|
|
204
|
+
- `intervalGaps`, `dataCommits` — gaps used for the interval, and data commits in the history
|
|
205
|
+
- `null` when the history has no data commits.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Profiler
|
|
210
|
+
|
|
211
|
+
**`profiler.profile(lf: pl.LazyFrame, schema, now=None) → dict[str, dict]`**
|
|
212
|
+
|
|
213
|
+
Returns a dict keyed by column name. Field names are OpenMetadata-aligned. Every column's statistics are computed in a single streaming pass over `lf`; `now` (default: the current UTC time) is the profiling time used to mark partial periods in time distributions.
|
|
214
|
+
|
|
215
|
+
### Numeric columns
|
|
216
|
+
|
|
217
|
+
| Field | Description |
|
|
218
|
+
|---|---|
|
|
219
|
+
| `valuesCount` | Non-null row count |
|
|
220
|
+
| `nullCount` | Null row count |
|
|
221
|
+
| `mean` | Arithmetic mean |
|
|
222
|
+
| `stddev` | Standard deviation |
|
|
223
|
+
| `min` | Minimum value |
|
|
224
|
+
| `firstQuartile` | 25th percentile |
|
|
225
|
+
| `median` | 50th percentile |
|
|
226
|
+
| `thirdQuartile` | 75th percentile |
|
|
227
|
+
| `max` | Maximum value |
|
|
228
|
+
|
|
229
|
+
`uniqueCount`, `top`, and `freq` are **absent** (not present as `None`).
|
|
230
|
+
|
|
231
|
+
### Other columns (string, boolean, etc.)
|
|
232
|
+
|
|
233
|
+
| Field | Description |
|
|
234
|
+
|---|---|
|
|
235
|
+
| `valuesCount` | Non-null row count |
|
|
236
|
+
| `nullCount` | Null row count |
|
|
237
|
+
| `uniqueCount` | Number of distinct values (including null) |
|
|
238
|
+
| `top` | Most frequent non-null value as a string, or `null` if all values are null |
|
|
239
|
+
| `freq` | Occurrence count of `top`, or `null` if all values are null |
|
|
240
|
+
|
|
241
|
+
`mean`, `stddev`, `min`, `firstQuartile`, `median`, `thirdQuartile`, and `max` are **absent**.
|
|
242
|
+
|
|
243
|
+
### Temporal columns (date, timestamp, duration, time)
|
|
244
|
+
|
|
245
|
+
`valuesCount`, `nullCount`, and `min`/`max` as ISO strings. Date and timestamp columns also get a `distribution`, built from per-hour (timestamp) or per-day (date) row counts gathered in the same single pass as the other statistics:
|
|
246
|
+
|
|
247
|
+
| Field | Description |
|
|
248
|
+
|---|---|
|
|
249
|
+
| `grain` | `monthly` (every value is midnight on the 1st), `daily` (every value is midnight) or `sub-daily` |
|
|
250
|
+
| `bucketing` | Time zone tz-aware timestamps were converted to before bucketing (`config.BUCKET_TIMEZONE`, default `UTC`), or `naive`/`date` for values bucketed as stored |
|
|
251
|
+
| `year`, `month`, `day` | Row counts per period that has rows |
|
|
252
|
+
| `weekday`, `hour` | Row counts per weekday and hour of day, every slot listed; `hour` only for sub-daily timestamps |
|
|
253
|
+
| `daysWithData`, `missingDays`, `largestGap` | Days with rows, days without rows between the first and last, and the longest run of those |
|
|
254
|
+
| `gaps` | Runs of missing days (or months, for monthly columns), largest first |
|
|
255
|
+
| `rowsPerDay` | min, p5, median, p95 and max rows per day with data, excluding the partial day; not for monthly columns |
|
|
256
|
+
| `partialDay`, `partialMonth` | The day and month still in progress at profiling time |
|
|
257
|
+
| `sameAs` | Set when the column's per-day counts repeat another column's: `derived` for a date matching a timestamp, `identical` otherwise |
|
|
258
|
+
|
|
259
|
+
### Float safety
|
|
260
|
+
|
|
261
|
+
Any stat that is `NaN` or `±Inf` is replaced with `null`. Values are rounded to 6 decimal places.
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## Writer
|
|
266
|
+
|
|
267
|
+
`generate` writes the output incrementally, so a long run leaves a usable catalogue behind as it goes:
|
|
268
|
+
|
|
269
|
+
1. `init_output_dir` creates `output_dir` if it does not exist, and deletes and recreates `output_dir/tables/`.
|
|
270
|
+
2. `copy_frontend` copies the assets from the package's `frontend/` directory into `output_dir`, and an empty `catalog.json` is written.
|
|
271
|
+
3. As each table finishes, `write_table_doc` writes `output_dir/tables/{fullyQualifiedName.replace(".", "/")}.json` (creating intermediate directories as needed) and `catalog.json` is rewritten with the tables so far.
|
|
272
|
+
4. `write_llm_docs` writes `llms.txt` and one `tables/{path}.md` per table once every table is profiled.
|
|
273
|
+
|
|
274
|
+
`write_output(catalog, table_docs, output_dir, base_url=None)` does all of that in one call, for already-profiled tables.
|
|
275
|
+
|
|
276
|
+
**JSON serialisation:** Dataclasses are recursively converted to plain dicts. `float` values that are `NaN` or `±Inf` are replaced with `null`. Output is UTF-8, indented with 2 spaces.
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## Output schema
|
|
281
|
+
|
|
282
|
+
### `catalog.json`
|
|
283
|
+
|
|
284
|
+
```json
|
|
285
|
+
{
|
|
286
|
+
"generatedAt": "<ISO 8601 UTC>",
|
|
287
|
+
"sources": ["<uri>"],
|
|
288
|
+
"tables": [
|
|
289
|
+
{
|
|
290
|
+
"fullyQualifiedName": "mydb.bronze.events",
|
|
291
|
+
"name": "events",
|
|
292
|
+
"uri": "<absolute table URI>",
|
|
293
|
+
"docPath": "tables/mydb/bronze/events.json",
|
|
294
|
+
"updatedAt": "<ISO 8601 UTC>",
|
|
295
|
+
"rowCount": 1000000,
|
|
296
|
+
"columnCount": 12,
|
|
297
|
+
"description": "Raw clickstream events",
|
|
298
|
+
"freshness": {"lastWrite": "<ISO 8601 UTC>", "secondsSinceLastWrite": 900, "typicalIntervalSeconds": 3600, "intervalGaps": 49, "burstGapSeconds": 60, "dataCommits": 812, "ratio": 0.25}
|
|
299
|
+
}
|
|
300
|
+
]
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### `tables/{path}.json`
|
|
305
|
+
|
|
306
|
+
```json
|
|
307
|
+
{
|
|
308
|
+
"name": "events",
|
|
309
|
+
"fullyQualifiedName": "mydb.bronze.events",
|
|
310
|
+
"uri": "<absolute table URI>",
|
|
311
|
+
"updatedAt": "<ISO 8601 UTC>",
|
|
312
|
+
"description": "Raw clickstream events",
|
|
313
|
+
"columns": [
|
|
314
|
+
{
|
|
315
|
+
"name": "id",
|
|
316
|
+
"dataType": "BIGINT",
|
|
317
|
+
"nullable": true,
|
|
318
|
+
"ordinalPosition": 1,
|
|
319
|
+
"precision": null,
|
|
320
|
+
"scale": null,
|
|
321
|
+
"partitioned": false,
|
|
322
|
+
"description": "Event id",
|
|
323
|
+
"tags": {"unit": "count"}
|
|
324
|
+
}
|
|
325
|
+
],
|
|
326
|
+
"tableProfile": {
|
|
327
|
+
"timestamp": "<ISO 8601 UTC>",
|
|
328
|
+
"rowCount": 1000000,
|
|
329
|
+
"columnCount": 12,
|
|
330
|
+
"columnProfile": [
|
|
331
|
+
{
|
|
332
|
+
"name": "id",
|
|
333
|
+
"timestamp": "<ISO 8601 UTC>",
|
|
334
|
+
"valuesCount": 1000000.0,
|
|
335
|
+
"nullCount": 0.0,
|
|
336
|
+
"mean": 500000.5,
|
|
337
|
+
"stddev": 288675.1,
|
|
338
|
+
"min": 1.0,
|
|
339
|
+
"firstQuartile": 250000.75,
|
|
340
|
+
"median": 500000.5,
|
|
341
|
+
"thirdQuartile": 750000.25,
|
|
342
|
+
"max": 1000000.0,
|
|
343
|
+
"uniqueCount": null,
|
|
344
|
+
"top": null,
|
|
345
|
+
"freq": null,
|
|
346
|
+
"distribution": null
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
"name": "ts",
|
|
350
|
+
"timestamp": "<ISO 8601 UTC>",
|
|
351
|
+
"valuesCount": 1000000.0,
|
|
352
|
+
"nullCount": 0.0,
|
|
353
|
+
"min": "2026-09-01T02:04:50+00:00",
|
|
354
|
+
"max": "2026-11-06T13:24:03+00:00",
|
|
355
|
+
"distribution": {
|
|
356
|
+
"grain": "sub-daily",
|
|
357
|
+
"bucketing": "UTC",
|
|
358
|
+
"partialDay": "2026-11-06",
|
|
359
|
+
"partialMonth": "2026-11",
|
|
360
|
+
"daysWithData": 66,
|
|
361
|
+
"missingDays": 1,
|
|
362
|
+
"largestGap": {"start": "2026-10-04", "end": "2026-10-04", "days": 1},
|
|
363
|
+
"gaps": [{"start": "2026-10-04", "end": "2026-10-04", "count": 1}],
|
|
364
|
+
"rowsPerDay": {"min": 9800, "p5": 14100.5, "median": 15200, "p95": 16050.25, "max": 17000},
|
|
365
|
+
"year": {"2026": 1000000},
|
|
366
|
+
"month": {"2026-09": 450000, "2026-10": 460000, "2026-11": 90000},
|
|
367
|
+
"day": {"2026-09-01": 9800, "...": 0},
|
|
368
|
+
"weekday": {"Mon": 142000, "Tue": 143000, "Wed": 144000, "Thu": 143500, "Fri": 142500, "Sat": 142000, "Sun": 143000},
|
|
369
|
+
"hour": {"00": 41000, "...": 0, "23": 41500}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
]
|
|
373
|
+
},
|
|
374
|
+
"sampleData": {
|
|
375
|
+
"columns": ["id", "name"],
|
|
376
|
+
"rows": [[1, "alice"], [2, "bob"]]
|
|
377
|
+
},
|
|
378
|
+
"extension": {
|
|
379
|
+
"deltaHistory": [
|
|
380
|
+
{
|
|
381
|
+
"version": 5,
|
|
382
|
+
"timestamp": "<ISO 8601 UTC>",
|
|
383
|
+
"operation": "WRITE",
|
|
384
|
+
"parameters": {"mode": "Append"},
|
|
385
|
+
"description": null,
|
|
386
|
+
"engine_info": "Delta-Rust/0.19.0"
|
|
387
|
+
}
|
|
388
|
+
]
|
|
389
|
+
},
|
|
390
|
+
"partitionColumns": [],
|
|
391
|
+
"commitCount": 84142,
|
|
392
|
+
"freshness": {"lastWrite": "<ISO 8601 UTC>", "secondsSinceLastWrite": 900, "typicalIntervalSeconds": 3600, "intervalGaps": 49, "burstGapSeconds": 60, "dataCommits": 812, "ratio": 0.25}
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
`description` is `null` when the Delta table has no description. `sampleData` is `null` when `--sample-data-count` is `0` or the table is excluded. `extension` is `null` when the Delta table has no history. `freshness` is `null` when the history has no data-changing commits. `"..."` in the example above stands for the omitted per-day and per-hour entries.
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
### LLM docs (`llms.txt` + `tables/{path}.md`)
|
|
401
|
+
|
|
402
|
+
Written at the end of `generate`, next to the JSON, so the same static server exposes them. Point an agent with a fetch tool at `<server>/llms.txt` and it can follow the links to each table.
|
|
403
|
+
|
|
404
|
+
- `llms.txt` — one section per group (the fully qualified name minus the table name, e.g. `gadgetbridge.gold`), one linked line per table with its description, row and column counts, and the age of its last write compared to its typical write interval.
|
|
405
|
+
- `tables/{path}.md` — description, row/column counts, partition columns, location, total Delta commits, last write and typical write interval, schema, statistics (nulls, distinct, min, mean, max, most common), a time distribution per date/timestamp column (grain, bucketing time zone, rows per day, gaps, a year × month grid with the partial month marked, rows by weekday and hour; a column with the same per-day counts as another is collapsed to one line), sample rows, the last 5 history entries, and links to the other tables in the same group, to the table's JSON and back to `llms.txt`.
|
|
406
|
+
|
|
407
|
+
Links are relative to the file they appear in, unless `--base-url` is given. Use `--base-url` when the files will be pasted into a chat rather than fetched, so the links still resolve. If the docs are published where a cloud-hosted LLM can reach them, consider generating without `--sample-data-count`.
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
## Missing Features
|
|
412
|
+
|
|
413
|
+
- Non-Delta table formats (Parquet, Iceberg, Hudi, etc.)
|
|
414
|
+
|
|
415
|
+
## Out of scope
|
|
416
|
+
|
|
417
|
+
- Tables with zero columns
|
|
418
|
+
- Incremental / append-only catalog updates
|
|
419
|
+
- S3 credential configuration: only the endpoint (`AWS_ENDPOINT_URL_S3`) is passed explicitly; credentials come from the standard `AWS_*` environment variables, read by `fsspec`/`s3fs` when scanning and by `deltalake`/Polars when reading tables
|
|
420
|
+
- Column ordering guarantees beyond schema order
|