dataframe-toolkit 0.13.0__tar.gz → 0.15.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/PKG-INFO +81 -2
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/README.md +80 -1
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/pyproject.toml +1 -1
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/pyproject.toml.orig +1 -1
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/print_cmd.py +8 -2
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/query_cmd.py +78 -1
- dataframe_toolkit-0.15.0/src/dftk/common/docformats.py +246 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/common/io.py +46 -5
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/skills/dftk/SKILL.md +7 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/__init__.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/cli.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/__init__.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/annotate_cmd.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/base.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/binx_cmd.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/chi_cmd.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/clean_cmd.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/cleannames_cmd.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/concat_cmd.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/corr_cmd.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/dataset_cmd.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/describe_cmd.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/eval_cmd.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/explode_cmd.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/fit_cmd.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/func_cmd.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/help.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/hist_cmd.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/implode_cmd.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/info_cmd.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/interp_cmd.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/line_cmd.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/melt_cmd.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/merge_cmd.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/pivot_cmd.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/randvar_cmd.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/sample_cmd.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/scale_cmd.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/scat_cmd.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/segid_cmd.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/skill_cmd.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/split_cmd.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/stat_cmd.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/test_cmd.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/transpose_cmd.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/commands/wstat_cmd.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/common/__init__.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/common/copula.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/common/evalfuncs.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/common/plot.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/common/seed.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/common/seq.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/common/stats.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/io.py +0 -0
- {dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/skills/dftk/references/snakemake.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: dataframe-toolkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.15.0
|
|
4
4
|
Summary: A command-line toolkit for DataFrame analysis and manipulation
|
|
5
5
|
Keywords: dataframe,statistics,data analysis,CLI,TSV,pandas
|
|
6
6
|
Author: Gunnar Pálsson
|
|
@@ -166,7 +166,7 @@ publication-quality output.
|
|
|
166
166
|
| `sample` | Random row sampling (with or without replacement, by count or fraction) |
|
|
167
167
|
| `split` | Split a dataframe into one file per group |
|
|
168
168
|
| `annotate` | Read and write provenance metadata (genome, source, …) in parquet files |
|
|
169
|
-
| `print` | Read any dftk input (TSV, parquet, stdin) and write TSV
|
|
169
|
+
| `print` | Read any dftk input (TSV, parquet, stdin) and write TSV, JSON, Markdown or LaTeX |
|
|
170
170
|
| `clean` | Remove leftover temp parquet pipe files from interrupted pipelines |
|
|
171
171
|
| `help` | List all subcommands or show full help for one: `dftk help stat` |
|
|
172
172
|
|
|
@@ -193,6 +193,49 @@ dftk dataset fertility -o \
|
|
|
193
193
|
|
|
194
194
|
For your own export, `dftk cleannames data.tsv --backend duckdb -o data.parquet` also keeps integer columns with empty cells as integers.
|
|
195
195
|
|
|
196
|
+
### JSON files
|
|
197
|
+
|
|
198
|
+
DuckDB reads JSON itself, so `query --sql` takes a JSON file in `FROM` (a file of records, `[{...}, {...}]`, or JSON Lines, one object per line; `'logs/*.jsonl'` reads several). When the SQL doesn't use the `data` table, dftk reads no input. In TSV a nested object or list is shown as one line of JSON (`["x","y"]`), which later commands can't compute on, so flatten it in the same query: `pos.*` makes a column of each field, `len()` counts a list and `unnest()` gives one row per item:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
printf '%s\n' '{"id": 1, "pos": {"chr": "1", "start": 100}, "tags": ["x", "y"]}' \
|
|
202
|
+
'{"id": 2, "pos": {"chr": "2", "start": 200}, "tags": []}' \
|
|
203
|
+
> records.jsonl
|
|
204
|
+
dftk query --sql \
|
|
205
|
+
"SELECT * EXCLUDE (pos, tags), pos.*, len(tags) AS ntags FROM 'records.jsonl'"
|
|
206
|
+
# id chr start ntags
|
|
207
|
+
# 1 1 100 2
|
|
208
|
+
# 2 2 200 0
|
|
209
|
+
dftk query --sql "SELECT id, unnest(tags) AS tag FROM 'records.jsonl'" -o \
|
|
210
|
+
| dftk pivot -i tag -v id -f count
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
A field that is a string in some records and a list in others (a variant catalog's `ReferenceRegion`, say) gets DuckDB type JSON; TSV shows its strings without quotes and its lists as JSON. `json_type()` tells them apart, so a list can become one row per item:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
printf '%s\n' '{"locus": "AFF2", "region": "chrX:1-60"}' \
|
|
217
|
+
'{"locus": "ATXN7", "region": ["chr3:1-30", "chr3:30-42"]}' \
|
|
218
|
+
> catalog.jsonl
|
|
219
|
+
dftk query --sql "
|
|
220
|
+
SELECT locus,
|
|
221
|
+
unnest(CASE WHEN json_type(region) = 'ARRAY' THEN region->>'\$[*]'
|
|
222
|
+
ELSE [region->>'\$'] END) AS region
|
|
223
|
+
FROM 'catalog.jsonl'"
|
|
224
|
+
# locus region
|
|
225
|
+
# AFF2 chrX:1-60
|
|
226
|
+
# ATXN7 chr3:1-30
|
|
227
|
+
# ATXN7 chr3:30-42
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
To get JSON back, write `-o FILE.json` or `.jsonl`, or `--format json` for stdout (see [Output formats](#output-formats)). Nested values and mixed fields are written as JSON again; a key missing from a record is written as `null`:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
dftk query --sql "SELECT * FROM 'catalog.jsonl' WHERE locus LIKE 'ATXN%'" --format json
|
|
234
|
+
# [
|
|
235
|
+
# {"locus": "ATXN7", "region": ["chr3:1-30", "chr3:30-42"]}
|
|
236
|
+
# ]
|
|
237
|
+
```
|
|
238
|
+
|
|
196
239
|
### Large tables: hexbin instead of points
|
|
197
240
|
|
|
198
241
|
With tens of thousands of points a scatter plot is a solid blob. `--hexbin` counts points per hexagon instead (`--hexlog` for a log colour scale), and `--fit lowess` adds a smooth curve:
|
|
@@ -390,6 +433,8 @@ All commands accept:
|
|
|
390
433
|
a previous command's bare `-o` (that temp file is then read and deleted)
|
|
391
434
|
- A pipe as the filename, e.g. `<(grep -v '^#' data.tsv)` or `<(dftk … -o)`,
|
|
392
435
|
read the same way as stdin
|
|
436
|
+
- JSON through `query --sql "SELECT … FROM 'f.jsonl'"`, which needs no
|
|
437
|
+
other input (see [JSON files](#json-files))
|
|
393
438
|
|
|
394
439
|
```bash
|
|
395
440
|
dftk dataset tips -o | dftk stat -c tip -g day # parquet path on stdin
|
|
@@ -401,6 +446,28 @@ Older scripts give `...` as the file to read a parquet path from stdin; that
|
|
|
401
446
|
still works. In zsh with Oh My Zsh an unquoted `...` becomes `../..`, so
|
|
402
447
|
leave it out (or use `-`) in scripts you share.
|
|
403
448
|
|
|
449
|
+
### Output formats
|
|
450
|
+
|
|
451
|
+
Without `-o`, output is TSV on stdout. `-o` alone, or `-o FILE.parquet`, writes parquet and prints the path for the next command; `-o FILE` writes TSV, except for these extensions:
|
|
452
|
+
|
|
453
|
+
| `-o` | Writes |
|
|
454
|
+
|------|--------|
|
|
455
|
+
| `FILE.json` | JSON array of records |
|
|
456
|
+
| `FILE.jsonl` | JSON Lines, one record per line |
|
|
457
|
+
| `FILE.md` | Markdown table (numeric columns right-aligned) |
|
|
458
|
+
| `FILE.tex` | bare LaTeX `tabular` with booktabs rules |
|
|
459
|
+
|
|
460
|
+
`--format tsv|json|jsonl|md|tex` writes the same formats to stdout, or overrides the extension. `--digits N` applies to all of them. The `.tex` file is only the tabular, with special characters (`_ & % $ # { } ~ ^ \ | < >`) escaped; it needs `\usepackage{booktabs}`, and goes inside your own `table` environment with `\input{tips_by_day.tex}`. Add `\usepackage[T1]{fontenc}` (pdfLaTeX's default font has no straight `"`, which JSON lists contain):
|
|
461
|
+
|
|
462
|
+
```bash
|
|
463
|
+
dftk dataset tips -o | dftk pivot -i day -v tip -f mean count --digits 3 --format md
|
|
464
|
+
# | day | mean_tip | count_tip |
|
|
465
|
+
# | --- | ---: | ---: |
|
|
466
|
+
# | Thur | 2.77 | 62 |
|
|
467
|
+
# ...
|
|
468
|
+
dftk dataset tips -o | dftk pivot -i day -v tip -f mean count --digits 3 -o tips_by_day.tex
|
|
469
|
+
```
|
|
470
|
+
|
|
404
471
|
### Comment lines and preambles
|
|
405
472
|
|
|
406
473
|
dftk has no comment or skip-rows option: `#` is ordinary data, so headers
|
|
@@ -427,6 +494,18 @@ dftk print data.txt --noheader --move V1:chr V2:begin V3:end -o named.tsv
|
|
|
427
494
|
dftk eval data.txt --noheader -f "len = V3 - V2" --removeheader # no header out
|
|
428
495
|
```
|
|
429
496
|
|
|
497
|
+
In `merge`, `--noheader` applies to both files, so both sides are named
|
|
498
|
+
`V1 V2 …`. The key appears once and the right side's other columns get the
|
|
499
|
+
`_r` suffix. `--move` runs after the merge, so it renames the merged names:
|
|
500
|
+
|
|
501
|
+
```bash
|
|
502
|
+
dftk merge -l left.txt -r right.txt -k V1 --noheader
|
|
503
|
+
# V1 V2 V3 V2_r V3_r
|
|
504
|
+
dftk merge -l left.txt -r right.txt -k V1 --noheader \
|
|
505
|
+
--move V1:id V2:n V3:s V2_r:n2 V3_r:s2
|
|
506
|
+
# id n s n2 s2
|
|
507
|
+
```
|
|
508
|
+
|
|
430
509
|
Parquet always has column names, so `--removeheader` with `-o` and
|
|
431
510
|
`--noheader` on parquet input do nothing (dftk warns).
|
|
432
511
|
|
|
@@ -136,7 +136,7 @@ publication-quality output.
|
|
|
136
136
|
| `sample` | Random row sampling (with or without replacement, by count or fraction) |
|
|
137
137
|
| `split` | Split a dataframe into one file per group |
|
|
138
138
|
| `annotate` | Read and write provenance metadata (genome, source, …) in parquet files |
|
|
139
|
-
| `print` | Read any dftk input (TSV, parquet, stdin) and write TSV
|
|
139
|
+
| `print` | Read any dftk input (TSV, parquet, stdin) and write TSV, JSON, Markdown or LaTeX |
|
|
140
140
|
| `clean` | Remove leftover temp parquet pipe files from interrupted pipelines |
|
|
141
141
|
| `help` | List all subcommands or show full help for one: `dftk help stat` |
|
|
142
142
|
|
|
@@ -163,6 +163,49 @@ dftk dataset fertility -o \
|
|
|
163
163
|
|
|
164
164
|
For your own export, `dftk cleannames data.tsv --backend duckdb -o data.parquet` also keeps integer columns with empty cells as integers.
|
|
165
165
|
|
|
166
|
+
### JSON files
|
|
167
|
+
|
|
168
|
+
DuckDB reads JSON itself, so `query --sql` takes a JSON file in `FROM` (a file of records, `[{...}, {...}]`, or JSON Lines, one object per line; `'logs/*.jsonl'` reads several). When the SQL doesn't use the `data` table, dftk reads no input. In TSV a nested object or list is shown as one line of JSON (`["x","y"]`), which later commands can't compute on, so flatten it in the same query: `pos.*` makes a column of each field, `len()` counts a list and `unnest()` gives one row per item:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
printf '%s\n' '{"id": 1, "pos": {"chr": "1", "start": 100}, "tags": ["x", "y"]}' \
|
|
172
|
+
'{"id": 2, "pos": {"chr": "2", "start": 200}, "tags": []}' \
|
|
173
|
+
> records.jsonl
|
|
174
|
+
dftk query --sql \
|
|
175
|
+
"SELECT * EXCLUDE (pos, tags), pos.*, len(tags) AS ntags FROM 'records.jsonl'"
|
|
176
|
+
# id chr start ntags
|
|
177
|
+
# 1 1 100 2
|
|
178
|
+
# 2 2 200 0
|
|
179
|
+
dftk query --sql "SELECT id, unnest(tags) AS tag FROM 'records.jsonl'" -o \
|
|
180
|
+
| dftk pivot -i tag -v id -f count
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
A field that is a string in some records and a list in others (a variant catalog's `ReferenceRegion`, say) gets DuckDB type JSON; TSV shows its strings without quotes and its lists as JSON. `json_type()` tells them apart, so a list can become one row per item:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
printf '%s\n' '{"locus": "AFF2", "region": "chrX:1-60"}' \
|
|
187
|
+
'{"locus": "ATXN7", "region": ["chr3:1-30", "chr3:30-42"]}' \
|
|
188
|
+
> catalog.jsonl
|
|
189
|
+
dftk query --sql "
|
|
190
|
+
SELECT locus,
|
|
191
|
+
unnest(CASE WHEN json_type(region) = 'ARRAY' THEN region->>'\$[*]'
|
|
192
|
+
ELSE [region->>'\$'] END) AS region
|
|
193
|
+
FROM 'catalog.jsonl'"
|
|
194
|
+
# locus region
|
|
195
|
+
# AFF2 chrX:1-60
|
|
196
|
+
# ATXN7 chr3:1-30
|
|
197
|
+
# ATXN7 chr3:30-42
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
To get JSON back, write `-o FILE.json` or `.jsonl`, or `--format json` for stdout (see [Output formats](#output-formats)). Nested values and mixed fields are written as JSON again; a key missing from a record is written as `null`:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
dftk query --sql "SELECT * FROM 'catalog.jsonl' WHERE locus LIKE 'ATXN%'" --format json
|
|
204
|
+
# [
|
|
205
|
+
# {"locus": "ATXN7", "region": ["chr3:1-30", "chr3:30-42"]}
|
|
206
|
+
# ]
|
|
207
|
+
```
|
|
208
|
+
|
|
166
209
|
### Large tables: hexbin instead of points
|
|
167
210
|
|
|
168
211
|
With tens of thousands of points a scatter plot is a solid blob. `--hexbin` counts points per hexagon instead (`--hexlog` for a log colour scale), and `--fit lowess` adds a smooth curve:
|
|
@@ -360,6 +403,8 @@ All commands accept:
|
|
|
360
403
|
a previous command's bare `-o` (that temp file is then read and deleted)
|
|
361
404
|
- A pipe as the filename, e.g. `<(grep -v '^#' data.tsv)` or `<(dftk … -o)`,
|
|
362
405
|
read the same way as stdin
|
|
406
|
+
- JSON through `query --sql "SELECT … FROM 'f.jsonl'"`, which needs no
|
|
407
|
+
other input (see [JSON files](#json-files))
|
|
363
408
|
|
|
364
409
|
```bash
|
|
365
410
|
dftk dataset tips -o | dftk stat -c tip -g day # parquet path on stdin
|
|
@@ -371,6 +416,28 @@ Older scripts give `...` as the file to read a parquet path from stdin; that
|
|
|
371
416
|
still works. In zsh with Oh My Zsh an unquoted `...` becomes `../..`, so
|
|
372
417
|
leave it out (or use `-`) in scripts you share.
|
|
373
418
|
|
|
419
|
+
### Output formats
|
|
420
|
+
|
|
421
|
+
Without `-o`, output is TSV on stdout. `-o` alone, or `-o FILE.parquet`, writes parquet and prints the path for the next command; `-o FILE` writes TSV, except for these extensions:
|
|
422
|
+
|
|
423
|
+
| `-o` | Writes |
|
|
424
|
+
|------|--------|
|
|
425
|
+
| `FILE.json` | JSON array of records |
|
|
426
|
+
| `FILE.jsonl` | JSON Lines, one record per line |
|
|
427
|
+
| `FILE.md` | Markdown table (numeric columns right-aligned) |
|
|
428
|
+
| `FILE.tex` | bare LaTeX `tabular` with booktabs rules |
|
|
429
|
+
|
|
430
|
+
`--format tsv|json|jsonl|md|tex` writes the same formats to stdout, or overrides the extension. `--digits N` applies to all of them. The `.tex` file is only the tabular, with special characters (`_ & % $ # { } ~ ^ \ | < >`) escaped; it needs `\usepackage{booktabs}`, and goes inside your own `table` environment with `\input{tips_by_day.tex}`. Add `\usepackage[T1]{fontenc}` (pdfLaTeX's default font has no straight `"`, which JSON lists contain):
|
|
431
|
+
|
|
432
|
+
```bash
|
|
433
|
+
dftk dataset tips -o | dftk pivot -i day -v tip -f mean count --digits 3 --format md
|
|
434
|
+
# | day | mean_tip | count_tip |
|
|
435
|
+
# | --- | ---: | ---: |
|
|
436
|
+
# | Thur | 2.77 | 62 |
|
|
437
|
+
# ...
|
|
438
|
+
dftk dataset tips -o | dftk pivot -i day -v tip -f mean count --digits 3 -o tips_by_day.tex
|
|
439
|
+
```
|
|
440
|
+
|
|
374
441
|
### Comment lines and preambles
|
|
375
442
|
|
|
376
443
|
dftk has no comment or skip-rows option: `#` is ordinary data, so headers
|
|
@@ -397,6 +464,18 @@ dftk print data.txt --noheader --move V1:chr V2:begin V3:end -o named.tsv
|
|
|
397
464
|
dftk eval data.txt --noheader -f "len = V3 - V2" --removeheader # no header out
|
|
398
465
|
```
|
|
399
466
|
|
|
467
|
+
In `merge`, `--noheader` applies to both files, so both sides are named
|
|
468
|
+
`V1 V2 …`. The key appears once and the right side's other columns get the
|
|
469
|
+
`_r` suffix. `--move` runs after the merge, so it renames the merged names:
|
|
470
|
+
|
|
471
|
+
```bash
|
|
472
|
+
dftk merge -l left.txt -r right.txt -k V1 --noheader
|
|
473
|
+
# V1 V2 V3 V2_r V3_r
|
|
474
|
+
dftk merge -l left.txt -r right.txt -k V1 --noheader \
|
|
475
|
+
--move V1:id V2:n V3:s V2_r:n2 V3_r:s2
|
|
476
|
+
# id n s n2 s2
|
|
477
|
+
```
|
|
478
|
+
|
|
400
479
|
Parquet always has column names, so `--removeheader` with `-o` and
|
|
401
480
|
`--noheader` on parquet input do nothing (dftk warns).
|
|
402
481
|
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
dftk.commands.print_cmd — the ``dftk print`` subcommand.
|
|
3
3
|
|
|
4
4
|
Reads any dftk-compatible input (TSV file, TSV on stdin, or a parquet pipe
|
|
5
|
-
path on stdin) and writes TSV to stdout
|
|
5
|
+
path on stdin) and writes TSV to stdout, or JSON, a Markdown table or a
|
|
6
|
+
LaTeX tabular with --format json|jsonl|md|tex. Acts as the terminal stage of a
|
|
6
7
|
dftk pipeline:
|
|
7
8
|
|
|
8
9
|
dftk stat -g grp -s val -o \\
|
|
@@ -12,6 +13,11 @@ dftk pipeline:
|
|
|
12
13
|
With --noheader and --move it names the columns of a headerless file:
|
|
13
14
|
|
|
14
15
|
dftk print data.txt --noheader --move V1:chr V2:begin V3:end
|
|
16
|
+
|
|
17
|
+
A table for a document:
|
|
18
|
+
|
|
19
|
+
dftk dataset tips -o | dftk pivot -i day -v tip -f mean -o \\
|
|
20
|
+
| dftk print --digits 3 --format md
|
|
15
21
|
"""
|
|
16
22
|
|
|
17
23
|
import argparse
|
|
@@ -24,7 +30,7 @@ class PrintCommand(BaseCommand):
|
|
|
24
30
|
"""Read any dftk input and write TSV to stdout."""
|
|
25
31
|
|
|
26
32
|
name = "print"
|
|
27
|
-
help = "
|
|
33
|
+
help = "Write any input (TSV or parquet pipe) as TSV, JSON, Markdown or LaTeX"
|
|
28
34
|
|
|
29
35
|
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
|
|
30
36
|
# The standard output options (--move, --select, --removeheader, …)
|
|
@@ -26,12 +26,14 @@ sql mode (--sql)
|
|
|
26
26
|
"""
|
|
27
27
|
|
|
28
28
|
import argparse
|
|
29
|
+
import json
|
|
29
30
|
import logging
|
|
30
31
|
import os
|
|
31
32
|
|
|
32
33
|
import pandas as pd
|
|
33
34
|
|
|
34
35
|
from dftk.commands.base import BaseCommand
|
|
36
|
+
from dftk.common import docformats
|
|
35
37
|
from dftk.common.io import io
|
|
36
38
|
|
|
37
39
|
try:
|
|
@@ -85,6 +87,9 @@ def _sql_query(args: argparse.Namespace, sql: str, table: str) -> pd.DataFrame:
|
|
|
85
87
|
if direct_parquet:
|
|
86
88
|
rel = conn.read_parquet(filename)
|
|
87
89
|
conn.register(table, rel)
|
|
90
|
+
elif filename is None and not _uses_table(sql, table):
|
|
91
|
+
# SQL reads its own files (FROM 'f.jsonl'): don't wait for stdin.
|
|
92
|
+
pass
|
|
88
93
|
else:
|
|
89
94
|
# stdin, TSV files, or parquet-pipe: read into pandas first.
|
|
90
95
|
df = io.read(args)
|
|
@@ -96,7 +101,42 @@ def _sql_query(args: argparse.Namespace, sql: str, table: str) -> pd.DataFrame:
|
|
|
96
101
|
df = result.df()
|
|
97
102
|
except Exception as exc:
|
|
98
103
|
raise ValueError(f"DuckDB SQL failed: {exc}") from exc
|
|
99
|
-
|
|
104
|
+
df = _hugeint_to_int64(df, description)
|
|
105
|
+
docformats.mark_json_columns(
|
|
106
|
+
df, [d[0] for d in description or [] if str(d[1]) == "JSON"]
|
|
107
|
+
)
|
|
108
|
+
return df
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _uses_table(sql: str, table: str) -> bool:
|
|
112
|
+
"""Return True unless DuckDB's parser says *sql* never names *table*.
|
|
113
|
+
|
|
114
|
+
Uses json_serialize_sql, which only parses (duckdb.get_table_names also
|
|
115
|
+
binds, and fails on valid SQL such as SELECT pos.*). On a parse error
|
|
116
|
+
the input is read as before and the query itself reports the error.
|
|
117
|
+
"""
|
|
118
|
+
try:
|
|
119
|
+
tree = json.loads(
|
|
120
|
+
duckdb.execute("SELECT json_serialize_sql(?)", [sql]).fetchone()[0]
|
|
121
|
+
)
|
|
122
|
+
except Exception: # an old duckdb without json_serialize_sql
|
|
123
|
+
return True
|
|
124
|
+
if tree.get("error"):
|
|
125
|
+
return True
|
|
126
|
+
names: set[str] = set()
|
|
127
|
+
|
|
128
|
+
def walk(node) -> None:
|
|
129
|
+
if isinstance(node, dict):
|
|
130
|
+
if node.get("type") == "BASE_TABLE":
|
|
131
|
+
names.add(node.get("table_name", "").lower())
|
|
132
|
+
for value in node.values():
|
|
133
|
+
walk(value)
|
|
134
|
+
elif isinstance(node, list):
|
|
135
|
+
for value in node:
|
|
136
|
+
walk(value)
|
|
137
|
+
|
|
138
|
+
walk(tree)
|
|
139
|
+
return table.lower() in names
|
|
100
140
|
|
|
101
141
|
|
|
102
142
|
def _hugeint_to_int64(df: pd.DataFrame, description) -> pd.DataFrame:
|
|
@@ -197,6 +237,41 @@ CTEs:
|
|
|
197
237
|
)
|
|
198
238
|
SELECT * FROM ranked WHERE rank <= 3"
|
|
199
239
|
|
|
240
|
+
JSON FILES
|
|
241
|
+
----------
|
|
242
|
+
DuckDB reads JSON itself: name the file in FROM, in single quotes. A file
|
|
243
|
+
of records ([{...}, {...}]) and JSON Lines (one object per line) both
|
|
244
|
+
work, and 'logs/*.jsonl' reads several files as one table. When the SQL
|
|
245
|
+
doesn't use the data table, dftk reads no input, so nothing is piped in:
|
|
246
|
+
|
|
247
|
+
printf '%s\\n' '{"id": 1, "pos": {"chr": "1", "start": 100}, "tags": ["x", "y"]}' \\
|
|
248
|
+
'{"id": 2, "pos": {"chr": "2", "start": 200}, "tags": []}' \\
|
|
249
|
+
> records.jsonl
|
|
250
|
+
dftk query --sql "SELECT * FROM 'records.jsonl'"
|
|
251
|
+
|
|
252
|
+
Nested objects and lists come out as one text cell each, which later
|
|
253
|
+
commands can't compute on. Flatten them in the same query: pos.* makes a
|
|
254
|
+
column of each field, len() counts a list, unnest() gives a row per item:
|
|
255
|
+
|
|
256
|
+
dftk query --sql \\
|
|
257
|
+
"SELECT * EXCLUDE (pos, tags), pos.*, len(tags) AS ntags
|
|
258
|
+
FROM 'records.jsonl'"
|
|
259
|
+
dftk query --sql "SELECT id, unnest(tags) AS tag FROM 'records.jsonl'"
|
|
260
|
+
dftk query --sql \\
|
|
261
|
+
"SELECT * EXCLUDE (pos, tags), pos.* FROM 'records.jsonl'" -o \\
|
|
262
|
+
| dftk stat -c start
|
|
263
|
+
|
|
264
|
+
In TSV output a list or object that is left nested is shown as one line
|
|
265
|
+
of JSON (["x","y"]). A field that is a string in some records and a list
|
|
266
|
+
in others gets DuckDB type JSON; its plain strings are shown without
|
|
267
|
+
quotes. To get JSON back, give -o FILE.json (or .jsonl) or --format json;
|
|
268
|
+
nested values, and such mixed fields, are written as JSON again, and a key
|
|
269
|
+
missing from a record is written as null:
|
|
270
|
+
|
|
271
|
+
dftk query --sql "SELECT * FROM 'records.jsonl' WHERE pos.chr = '1'" \\
|
|
272
|
+
--format json
|
|
273
|
+
dftk query --sql "SELECT id, tags FROM 'records.jsonl'" -o ids.jsonl
|
|
274
|
+
|
|
200
275
|
OUT-OF-CORE LARGE FILES
|
|
201
276
|
------------------------
|
|
202
277
|
When the input is a named .parquet file, DuckDB queries it natively without
|
|
@@ -254,6 +329,8 @@ class QueryCommand(BaseCommand):
|
|
|
254
329
|
"Full DuckDB SQL query. The input is registered as a virtual "
|
|
255
330
|
"table named 'data' (see --table). Named .parquet inputs are "
|
|
256
331
|
"queried natively without full load into memory. "
|
|
332
|
+
"SQL that only reads its own files (FROM 'f.jsonl') "
|
|
333
|
+
"needs no input. "
|
|
257
334
|
"Cannot be combined with -q. Requires duckdb."
|
|
258
335
|
),
|
|
259
336
|
)
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dftk.common.docformats — JSON, Markdown and LaTeX output for io.printdf.
|
|
3
|
+
|
|
4
|
+
Nested values (lists, structs, maps) come from DuckDB as numpy arrays and
|
|
5
|
+
dicts. TSV, Markdown and LaTeX show them as one line of JSON text; JSON
|
|
6
|
+
output writes them as JSON.
|
|
7
|
+
|
|
8
|
+
A DuckDB column of type JSON (from a JSON file whose field is a string in one
|
|
9
|
+
record and a list in another) reaches pandas as JSON text, e.g.
|
|
10
|
+
'"chrX:1-2"' and '["chr3:1-2","chr3:2-3"]'. query_cmd lists such columns in
|
|
11
|
+
the parquet metadata key JSON_COLUMNS_KEY, so they survive -o pipes; the text
|
|
12
|
+
view drops the quotes around a plain string and JSON output parses them.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import contextlib
|
|
16
|
+
import decimal
|
|
17
|
+
import json
|
|
18
|
+
import math
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
import pandas as pd
|
|
22
|
+
|
|
23
|
+
JSON_COLUMNS_KEY = "dftk_json_columns"
|
|
24
|
+
|
|
25
|
+
# Output format by file extension; --format names the same formats
|
|
26
|
+
EXTENSIONS = {".json": "json", ".jsonl": "jsonl", ".md": "md", ".tex": "tex"}
|
|
27
|
+
FORMATS = ("tsv", "json", "jsonl", "md", "tex")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def json_columns(df: pd.DataFrame) -> list[str]:
|
|
31
|
+
"""Columns of *df* that hold DuckDB JSON text (see module docstring)."""
|
|
32
|
+
names = df.attrs.get("_parquet_meta", {}).get(JSON_COLUMNS_KEY, "")
|
|
33
|
+
return [c for c in names.split(",") if c in df.columns]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def mark_json_columns(df: pd.DataFrame, columns: list[str]) -> None:
|
|
37
|
+
"""Record *columns* as JSON text in df.attrs (carried into parquet)."""
|
|
38
|
+
meta = dict(df.attrs.get("_parquet_meta", {}))
|
|
39
|
+
if columns:
|
|
40
|
+
meta[JSON_COLUMNS_KEY] = ",".join(columns)
|
|
41
|
+
else:
|
|
42
|
+
meta.pop(JSON_COLUMNS_KEY, None)
|
|
43
|
+
df.attrs["_parquet_meta"] = meta
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def rename_json_columns(df: pd.DataFrame, mapping: dict[str, str]) -> None:
|
|
47
|
+
"""Follow a --move rename in the JSON column list."""
|
|
48
|
+
names = df.attrs.get("_parquet_meta", {}).get(JSON_COLUMNS_KEY)
|
|
49
|
+
if names:
|
|
50
|
+
mark_json_columns(df, [mapping.get(c, c) for c in names.split(",")])
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _plain(value):
|
|
54
|
+
"""json.dumps fallback for numpy, Decimal and other non-JSON values."""
|
|
55
|
+
if isinstance(value, np.ndarray): # MaskedArray.tolist() gives None
|
|
56
|
+
return value.tolist()
|
|
57
|
+
if isinstance(value, np.generic):
|
|
58
|
+
return value.item()
|
|
59
|
+
if isinstance(value, decimal.Decimal):
|
|
60
|
+
return float(value)
|
|
61
|
+
return str(value)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _is_nested(value) -> bool:
|
|
65
|
+
return isinstance(value, (np.ndarray, list, tuple, dict))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _dumps(value) -> str:
|
|
69
|
+
return json.dumps(value, default=_plain, ensure_ascii=False, separators=(",", ":"))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def nested_as_text(df: pd.DataFrame) -> pd.DataFrame:
|
|
73
|
+
"""Return *df* with nested values and JSON-column text as one-line text."""
|
|
74
|
+
jcols = set(json_columns(df))
|
|
75
|
+
out = None
|
|
76
|
+
for col in df.columns:
|
|
77
|
+
dtype = df[col].dtype
|
|
78
|
+
if not (
|
|
79
|
+
pd.api.types.is_object_dtype(dtype) or pd.api.types.is_string_dtype(dtype)
|
|
80
|
+
):
|
|
81
|
+
continue
|
|
82
|
+
if col in jcols:
|
|
83
|
+
new = df[col].map(_json_text_cell)
|
|
84
|
+
elif df[col].map(_is_nested).any():
|
|
85
|
+
new = df[col].map(lambda v: _dumps(v) if _is_nested(v) else v)
|
|
86
|
+
else:
|
|
87
|
+
continue
|
|
88
|
+
if out is None:
|
|
89
|
+
out = df.copy()
|
|
90
|
+
out[col] = new
|
|
91
|
+
return df if out is None else out
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _json_text_cell(value):
|
|
95
|
+
"""'"chrX:1-2"' → 'chrX:1-2'; a list or object stays JSON text."""
|
|
96
|
+
if not isinstance(value, str):
|
|
97
|
+
return value
|
|
98
|
+
try:
|
|
99
|
+
parsed = json.loads(value)
|
|
100
|
+
except ValueError:
|
|
101
|
+
return value
|
|
102
|
+
return parsed if isinstance(parsed, str) else value
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _json_value(value, digits: int | None, is_json_text: bool):
|
|
106
|
+
"""A cell as a JSON-ready Python value."""
|
|
107
|
+
if is_json_text and isinstance(value, str):
|
|
108
|
+
try:
|
|
109
|
+
return json.loads(value)
|
|
110
|
+
except ValueError:
|
|
111
|
+
return value
|
|
112
|
+
if _is_nested(value):
|
|
113
|
+
return json.loads(_dumps(value))
|
|
114
|
+
if value is None or value is pd.NA or value is pd.NaT:
|
|
115
|
+
return None
|
|
116
|
+
if isinstance(value, np.generic):
|
|
117
|
+
value = value.item()
|
|
118
|
+
if isinstance(value, float):
|
|
119
|
+
if math.isnan(value) or math.isinf(value):
|
|
120
|
+
return None
|
|
121
|
+
if digits is not None:
|
|
122
|
+
return float(f"{value:.{digits}g}")
|
|
123
|
+
return value
|
|
124
|
+
if isinstance(value, (int, bool, str)):
|
|
125
|
+
return value
|
|
126
|
+
return _plain(value)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def write_json(df: pd.DataFrame, dest, lines: bool, digits: int | None) -> None:
|
|
130
|
+
"""Write *df* as an array of records, or one record per line (*lines*).
|
|
131
|
+
|
|
132
|
+
A missing value is written as null; the key is kept.
|
|
133
|
+
"""
|
|
134
|
+
jcols = set(json_columns(df))
|
|
135
|
+
cols = list(df.columns)
|
|
136
|
+
records = (
|
|
137
|
+
{
|
|
138
|
+
col: _json_value(value, digits, col in jcols)
|
|
139
|
+
for col, value in zip(cols, row, strict=True)
|
|
140
|
+
}
|
|
141
|
+
for row in df.itertuples(index=False, name=None)
|
|
142
|
+
)
|
|
143
|
+
with _open(dest) as fh:
|
|
144
|
+
if lines:
|
|
145
|
+
for record in records:
|
|
146
|
+
fh.write(json.dumps(record, ensure_ascii=False) + "\n")
|
|
147
|
+
else:
|
|
148
|
+
items = [json.dumps(r, ensure_ascii=False) for r in records]
|
|
149
|
+
fh.write("[\n" + ",\n".join(" " + s for s in items) + "\n]\n")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _cell_text(value, float_format: str | None) -> str:
|
|
153
|
+
if value is None or value is pd.NA or value is pd.NaT:
|
|
154
|
+
return ""
|
|
155
|
+
if isinstance(value, (float, np.floating)):
|
|
156
|
+
if math.isnan(value):
|
|
157
|
+
return ""
|
|
158
|
+
return float_format % value if float_format else str(float(value))
|
|
159
|
+
return str(value)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _is_numeric(series: pd.Series) -> bool:
|
|
163
|
+
return pd.api.types.is_numeric_dtype(series) and not pd.api.types.is_bool_dtype(
|
|
164
|
+
series
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _table(df: pd.DataFrame, float_format: str | None, escape):
|
|
169
|
+
"""Escaped header and rows of *df* (nested values as JSON text)."""
|
|
170
|
+
df = nested_as_text(df)
|
|
171
|
+
header = [escape(str(c)) for c in df.columns]
|
|
172
|
+
rows = [
|
|
173
|
+
[escape(_cell_text(v, float_format)) for v in row]
|
|
174
|
+
for row in df.itertuples(index=False, name=None)
|
|
175
|
+
]
|
|
176
|
+
numeric = [_is_numeric(df[c]) for c in df.columns]
|
|
177
|
+
return header, rows, numeric
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _escape_markdown(text: str) -> str:
|
|
181
|
+
return text.replace("\\", "\\\\").replace("|", "\\|").replace("\n", " ")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def write_markdown(
|
|
185
|
+
df: pd.DataFrame, dest, float_format: str | None, header: bool
|
|
186
|
+
) -> None:
|
|
187
|
+
"""Write *df* as a pipe table; numeric columns are right-aligned.
|
|
188
|
+
|
|
189
|
+
A Markdown table always has a header row; with --removeheader it is empty.
|
|
190
|
+
"""
|
|
191
|
+
names, rows, numeric = _table(df, float_format, _escape_markdown)
|
|
192
|
+
if not header:
|
|
193
|
+
names = [""] * len(names)
|
|
194
|
+
rule = ["---:" if num else "---" for num in numeric]
|
|
195
|
+
with _open(dest) as fh:
|
|
196
|
+
for cells in [names, rule, *rows]:
|
|
197
|
+
fh.write("| " + " | ".join(cells) + " |\n")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
_LATEX_SPECIAL = {
|
|
201
|
+
"\\": r"\textbackslash{}",
|
|
202
|
+
"&": r"\&",
|
|
203
|
+
"%": r"\%",
|
|
204
|
+
"$": r"\$",
|
|
205
|
+
"#": r"\#",
|
|
206
|
+
"_": r"\_",
|
|
207
|
+
"{": r"\{",
|
|
208
|
+
"}": r"\}",
|
|
209
|
+
"~": r"\textasciitilde{}",
|
|
210
|
+
"^": r"\textasciicircum{}",
|
|
211
|
+
# The default OT1 font prints these as an em dash and as inverted ! and ?
|
|
212
|
+
"|": r"\textbar{}",
|
|
213
|
+
"<": r"\textless{}",
|
|
214
|
+
">": r"\textgreater{}",
|
|
215
|
+
"\n": " ",
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _escape_latex(text: str) -> str:
|
|
220
|
+
return "".join(_LATEX_SPECIAL.get(ch, ch) for ch in text)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def write_latex(df: pd.DataFrame, dest, float_format: str | None, header: bool) -> None:
|
|
224
|
+
"""Write *df* as a bare booktabs tabular (needs \\usepackage{booktabs}).
|
|
225
|
+
|
|
226
|
+
Numeric columns are right-aligned (r), others left (l). Wrap it in your
|
|
227
|
+
own table environment with \\input{file.tex}. A " (in JSON lists, say)
|
|
228
|
+
and ~ ^ print as intended with \\usepackage[T1]{fontenc}; the default
|
|
229
|
+
OT1 font has no straight double quote.
|
|
230
|
+
"""
|
|
231
|
+
names, rows, numeric = _table(df, float_format, _escape_latex)
|
|
232
|
+
spec = "".join("r" if num else "l" for num in numeric)
|
|
233
|
+
with _open(dest) as fh:
|
|
234
|
+
fh.write(f"\\begin{{tabular}}{{{spec}}}\n\\toprule\n")
|
|
235
|
+
if header:
|
|
236
|
+
fh.write(" & ".join(names) + " \\\\\n\\midrule\n")
|
|
237
|
+
for cells in rows:
|
|
238
|
+
fh.write(" & ".join(cells) + " \\\\\n")
|
|
239
|
+
fh.write("\\bottomrule\n\\end{tabular}\n")
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _open(dest):
|
|
243
|
+
"""Open *dest* for writing if it is a path; use it as is if a stream."""
|
|
244
|
+
if isinstance(dest, str):
|
|
245
|
+
return open(dest, "w", encoding="utf-8", newline="")
|
|
246
|
+
return contextlib.nullcontext(dest)
|
|
@@ -10,6 +10,10 @@ Ported from df/src/df/dfio.py with the following changes:
|
|
|
10
10
|
-o FILE.parquet → named parquet written to FILE, path printed to stdout
|
|
11
11
|
(NOT auto-deleted — reusable later in the same pipeline)
|
|
12
12
|
-o FILE → TSV written to FILE, nothing on stdout
|
|
13
|
+
-o FILE.json / .jsonl / .md / .tex
|
|
14
|
+
→ JSON, JSON Lines, Markdown or LaTeX table written to
|
|
15
|
+
FILE (common/docformats.py); --format sends the same
|
|
16
|
+
formats to stdout
|
|
13
17
|
|
|
14
18
|
• Parquet pipes: when stdin (no DATAFILE, or -) or a pipe given as
|
|
15
19
|
DATAFILE (<(...)) holds just the path of an existing .parquet file, that
|
|
@@ -18,7 +22,7 @@ Ported from df/src/df/dfio.py with the following changes:
|
|
|
18
22
|
they can be referenced again downstream. The older ``...`` DATAFILE
|
|
19
23
|
marker still works and requires a parquet path.
|
|
20
24
|
|
|
21
|
-
• Removed --sqlout
|
|
25
|
+
• Removed --sqlout. --latex became -o FILE.tex / --format tex.
|
|
22
26
|
• Removed --experimental (unused).
|
|
23
27
|
• parser_printdf kept as an alias for parser_output for compatibility.
|
|
24
28
|
"""
|
|
@@ -36,6 +40,8 @@ import tempfile
|
|
|
36
40
|
import numpy as np
|
|
37
41
|
import pandas as pd
|
|
38
42
|
|
|
43
|
+
from dftk.common import docformats
|
|
44
|
+
|
|
39
45
|
try:
|
|
40
46
|
import duckdb
|
|
41
47
|
except ImportError:
|
|
@@ -529,12 +535,27 @@ class io:
|
|
|
529
535
|
"path to stdout (auto-deleted by the next reader). "
|
|
530
536
|
"Use -o FILE.parquet to write a named parquet and print its path to "
|
|
531
537
|
"stdout (not auto-deleted — reusable later in the same pipeline). "
|
|
532
|
-
"Use -o FILE (no .parquet) to write TSV to FILE with no stdout output
|
|
538
|
+
"Use -o FILE (no .parquet) to write TSV to FILE with no stdout output; "
|
|
539
|
+
"FILE.json, .jsonl, .md or .tex writes JSON, JSON Lines, a "
|
|
540
|
+
"Markdown table or a LaTeX tabular instead."
|
|
541
|
+
),
|
|
542
|
+
)
|
|
543
|
+
g.add_argument(
|
|
544
|
+
"--format",
|
|
545
|
+
choices=docformats.FORMATS,
|
|
546
|
+
default=None,
|
|
547
|
+
help=(
|
|
548
|
+
"Output format for stdout or -o FILE, instead of the format "
|
|
549
|
+
"the file extension gives: tsv, json (array of records), "
|
|
550
|
+
"jsonl (one record per line), md (Markdown table), tex "
|
|
551
|
+
"(bare booktabs tabular: \\usepackage{booktabs} and [T1]{fontenc}, "
|
|
552
|
+
"then \\input the file). Not with parquet output."
|
|
533
553
|
),
|
|
534
554
|
)
|
|
535
555
|
g.add_argument(
|
|
536
556
|
"--digits",
|
|
537
|
-
help="Significant floating-point digits in TSV
|
|
557
|
+
help="Significant floating-point digits in TSV, JSON, Markdown and "
|
|
558
|
+
"LaTeX output",
|
|
538
559
|
type=int,
|
|
539
560
|
default=None,
|
|
540
561
|
)
|
|
@@ -688,6 +709,7 @@ class io:
|
|
|
688
709
|
try:
|
|
689
710
|
old, new = item.split(":")
|
|
690
711
|
df = df.rename(columns={old: new})
|
|
712
|
+
docformats.rename_json_columns(df, {old: new})
|
|
691
713
|
except Exception:
|
|
692
714
|
logging.warning(f"--move: failed to rename from {item!r}")
|
|
693
715
|
|
|
@@ -836,12 +858,22 @@ class io:
|
|
|
836
858
|
|
|
837
859
|
# Header: written unless --removeheader (--noheader is about the input)
|
|
838
860
|
header = not getattr(args, "removeheader", False)
|
|
839
|
-
|
|
861
|
+
to_parquet = output == "" or str(output).endswith(".parquet")
|
|
862
|
+
if not header and to_parquet:
|
|
840
863
|
logging.warning(
|
|
841
864
|
"--removeheader has no effect on parquet output (-o); parquet "
|
|
842
865
|
"always keeps column names"
|
|
843
866
|
)
|
|
844
867
|
|
|
868
|
+
fmt: str | None = getattr(args, "format", None)
|
|
869
|
+
if fmt is not None and to_parquet:
|
|
870
|
+
raise ValueError(
|
|
871
|
+
f"--format {fmt} can't be combined with parquet output (-o, "
|
|
872
|
+
"-o FILE.parquet): give -o FILE or leave -o out"
|
|
873
|
+
)
|
|
874
|
+
if fmt is None and output:
|
|
875
|
+
fmt = docformats.EXTENSIONS.get(os.path.splitext(output)[1].lower())
|
|
876
|
+
|
|
845
877
|
if output == "":
|
|
846
878
|
# Pipe mode — write temp parquet, print path
|
|
847
879
|
os.makedirs(DFTK_TMPDIR, exist_ok=True)
|
|
@@ -857,13 +889,22 @@ class io:
|
|
|
857
889
|
_write_parquet(df, output, meta=meta or None)
|
|
858
890
|
print(output)
|
|
859
891
|
|
|
892
|
+
elif fmt in ("json", "jsonl"):
|
|
893
|
+
dest = output if output is not None else sys.stdout
|
|
894
|
+
docformats.write_json(df, dest, lines=fmt == "jsonl", digits=digits)
|
|
895
|
+
|
|
896
|
+
elif fmt in ("md", "tex"):
|
|
897
|
+
dest = output if output is not None else sys.stdout
|
|
898
|
+
write = docformats.write_markdown if fmt == "md" else docformats.write_latex
|
|
899
|
+
write(df, dest, float_format=float_format, header=header)
|
|
900
|
+
|
|
860
901
|
else:
|
|
861
902
|
# TSV — stdout or named file
|
|
862
903
|
if digits is not None:
|
|
863
904
|
_warn_digits_exponent(df, digits)
|
|
864
905
|
dest = output if output is not None else sys.stdout
|
|
865
906
|
_write_tsv(
|
|
866
|
-
df,
|
|
907
|
+
docformats.nested_as_text(df),
|
|
867
908
|
dest,
|
|
868
909
|
sep="\t",
|
|
869
910
|
index=False,
|
|
@@ -87,6 +87,12 @@ columns), `--drop COL…`, `--move OLD:NEW…` (rename), `--postquery EXPR`,
|
|
|
87
87
|
`--dropna`, `--na_rep VAL`, `--removeheader`, and
|
|
88
88
|
`--meta KEY=VALUE` (provenance, see below).
|
|
89
89
|
|
|
90
|
+
Output format: TSV unless `-o FILE` ends in `.json` (array of records),
|
|
91
|
+
`.jsonl`, `.md` (Markdown table) or `.tex` (bare booktabs tabular, for
|
|
92
|
+
`\input` in a LaTeX table); `--format tsv|json|jsonl|md|tex` does the same
|
|
93
|
+
on stdout. Lists and nested values are one line of JSON in TSV and stay
|
|
94
|
+
nested in JSON output.
|
|
95
|
+
|
|
90
96
|
## Choosing a command
|
|
91
97
|
|
|
92
98
|
Look at a table first:
|
|
@@ -104,6 +110,7 @@ Transform:
|
|
|
104
110
|
| New column from a formula: `z = x / y`, `where(...)`, string, DNA and statistical functions | `eval -f`, `eval -c` (constant), `eval -s` (string) |
|
|
105
111
|
| Fill empty cells: from another column, a group mean, the row above (`ffill`, per group with `groupby`) or by interpolation; sort first, fills follow row order | `eval -f "x = x.fillna(y)"`, `"x = x.groupby(g).ffill()"` (see `eval -h`) |
|
|
106
112
|
| Filter rows (pandas expression) or run any DuckDB SQL (`FROM data`) | `query -q` / `query --sql` |
|
|
113
|
+
| Read JSON or JSON Lines (no input needed); flatten nested fields with `pos.*`, lists with `unnest()` | `query --sql "SELECT * EXCLUDE (pos), pos.* FROM 'f.jsonl'"` |
|
|
107
114
|
| Join two tables, including anti-joins | `merge DATAFILE -r OTHER -k KEY [-t left] [--only left]` |
|
|
108
115
|
| Stack tables row-wise | `concat A B C [--sourcecol src]` |
|
|
109
116
|
| Wide to long / long to wide plus aggregation | `melt` / `pivot -i ROWS -g COLS -v VALS -f FUNCS` |
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dataframe_toolkit-0.13.0 → dataframe_toolkit-0.15.0}/src/dftk/skills/dftk/references/snakemake.md
RENAMED
|
File without changes
|