dataframe-toolkit 0.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,230 @@
1
+ Metadata-Version: 2.3
2
+ Name: dataframe-toolkit
3
+ Version: 0.4.1
4
+ Summary: A command-line toolkit for DataFrame analysis and manipulation
5
+ Keywords: dataframe,statistics,data analysis,CLI,TSV,pandas
6
+ Author: Gunnar Pálsson
7
+ Author-email: Gunnar Pálsson <hemingur@gmail.com>
8
+ License: MIT
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
13
+ Classifier: Topic :: Utilities
14
+ Classifier: Programming Language :: Python :: 3
15
+ Requires-Dist: pandas>=2.1
16
+ Requires-Dist: numpy>=1.24
17
+ Requires-Dist: pyarrow>=12.0
18
+ Requires-Dist: scipy>=1.10
19
+ Requires-Dist: statsmodels>=0.14
20
+ Requires-Dist: duckdb>=0.8
21
+ Requires-Dist: matplotlib>=3.7
22
+ Requires-Dist: seaborn>=0.12
23
+ Requires-Dist: tqdm>=4.65
24
+ Requires-Python: >=3.12
25
+ Project-URL: Homepage, https://github.com/hemingur/dataframe-toolkit
26
+ Project-URL: Repository, https://github.com/hemingur/dataframe-toolkit
27
+ Description-Content-Type: text/markdown
28
+
29
+ # dftk — DataFrame analysis and manipulation toolkit
30
+
31
+ `dftk` is a command-line toolkit for exploratory data analysis on TSV files.
32
+ Each subcommand reads tabular data, performs one operation, and writes TSV to
33
+ stdout, making it easy to chain commands in pipelines.
34
+
35
+ `dftk` was developed during computational genomics research as a fast,
36
+ composable alternative to writing one-off pandas scripts. It is designed
37
+ for analysts who live in the terminal and want a consistent, pipeable toolkit
38
+ for the full data analysis pipeline — from initial exploration through
39
+ statistical modelling and publication-quality figures.
40
+
41
+ ## Installation
42
+
43
+ ```bash
44
+ uv tool install dataframe-toolkit
45
+ dftk --help
46
+ ```
47
+
48
+ Or with `pip`:
49
+
50
+ ```bash
51
+ pip install dataframe-toolkit
52
+ ```
53
+
54
+ To install the latest `main` branch directly from GitHub:
55
+
56
+ ```bash
57
+ uv tool install git+https://github.com/hemingur/dataframe-toolkit.git
58
+ ```
59
+
60
+ To install from a local clone:
61
+
62
+ ```bash
63
+ git clone https://github.com/hemingur/dataframe-toolkit.git
64
+ cd dataframe-toolkit
65
+ uv tool install .
66
+ ```
67
+
68
+ ## Quick example
69
+
70
+ ```bash
71
+ dftk stat data.tsv -c height weight -g sex
72
+ dftk stat data.tsv -c value -g group -o \
73
+ | dftk pivot -i group -v value_mean -o \
74
+ | dftk print
75
+ ```
76
+
77
+ ## Subcommands
78
+
79
+ ### Data transformation
80
+
81
+ | Command | Description |
82
+ |-----------|--------------------------------------------------------------------------------------|
83
+ | `eval` | Add or modify columns: eval expressions, string/path functions, statistical ops |
84
+ | `query` | Filter rows with pandas query expressions or SQL (`--sql` via DuckDB) |
85
+ | `merge` | Join two tables on key columns (inner/left/right/outer) |
86
+ | `concat` | Concatenate two or more tables row-wise |
87
+ | `melt` | Reshape wide-to-long (`pd.melt`) |
88
+ | `pivot` | Reshape long-to-wide with per-cell aggregation |
89
+ | `func` | Column transforms: cumsum, group mean/sum/min/max/count/median/std, rank, qcut:N |
90
+ | `scale` | Normalise: z-score, min-shift, sum/max/mean scaling, Blom rank, regression residuals |
91
+ | `interp` | Interpolate values from a reference curve into a table (1-D lookup) |
92
+ | `binx` | Assign bin indices to a column based on explicit or generated edges |
93
+
94
+ ### Statistics
95
+
96
+ | Command | Description |
97
+ |------------|-----------------------------------------------------------------------------------------------------|
98
+ | `stat` | Descriptive statistics (count/mean/std/CI/SEM/skew/kurtosis) with grouping and bootstrap |
99
+ | `wstat` | Weighted descriptive statistics (wmean, wstd, weighted quantile CI) |
100
+ | `fit` | OLS/robust/weighted regression via R-style formulas; tidy table, `--summary`, `--anova` |
101
+ | `test` | P-values between column pairs: t-test, Mann-Whitney, Wilcoxon, KS, correlations, bootstrap; groups |
102
+ | `corr` | Pairwise column correlations (Pearson/Spearman/Kendall) with optional BCa bootstrap CI |
103
+ | `describe` | Quick column-level summary (dtype, n, n_unique, n_null, sample values) |
104
+ | `randvar` | Sample from a distribution and append as a new column (norm, alpha, beta, …) |
105
+
106
+ ### Plots
107
+
108
+ All plot commands write a PNG/PDF when `-f FILE` is given, or display
109
+ interactively otherwise. They support `--groupcol` for colour grouping,
110
+ `--subgraphcol` for subplot grids, and figure/font presets for
111
+ publication-quality output.
112
+
113
+ | Command | Description |
114
+ |---------|-------------------------------------------------------------------------------------------------|
115
+ | `scat` | Scatter plot; optional OLS/robust fit overlay, bubble size (`--sizecol`), colour (`--colorcol`) |
116
+ | `line` | Line plot; optional error bars (`--yerr`) or CI bands (`--yci lo,hi`) and fit overlay |
117
+ | `hist` | Histogram or KDE (`--kde`); normalisation, cumulative mode, mean±σ annotation (`--stats`) |
118
+
119
+ ### Utilities
120
+
121
+ | Command | Description |
122
+ |------------|--------------------------------------------------------------------------|
123
+ | `dataset` | Load a curated example dataset from seaborn, statsmodels, or pydataset |
124
+ | `sample` | Random row sampling (with or without replacement, by count or fraction) |
125
+ | `split` | Split a dataframe into one file per group |
126
+ | `annotate` | Read and write provenance metadata (genome, source, …) in parquet files |
127
+ | `print` | Read any dftk input (TSV, stdin, `...` parquet pipe) and write TSV |
128
+ | `clean` | Remove leftover temp parquet pipe files from interrupted pipelines |
129
+ | `help` | List all subcommands or show full help for one: `dftk help stat` |
130
+
131
+ ## Common patterns
132
+
133
+ ### Chaining commands
134
+
135
+ ```bash
136
+ # z-score within group, then fit a model
137
+ dftk scale data.tsv -c expr -g condition -o \
138
+ | dftk fit - -f "expr_z ~ time + batch" -g condition
139
+ ```
140
+
141
+ ### Group summary then plot
142
+
143
+ ```bash
144
+ dftk stat results.tsv -c value -g group -o \
145
+ | dftk line - -x group -y value_mean --yerr value_sem -f fig.png
146
+ ```
147
+
148
+ ### Wide-to-long then plot overlaid histograms
149
+
150
+ ```bash
151
+ dftk melt data.tsv -i sample -d gene -v expression \
152
+ | dftk hist - -x expression -g gene -k -f dist.png
153
+ ```
154
+
155
+ ### Interpolation (standard curve lookup)
156
+
157
+ ```bash
158
+ dftk interp samples.tsv --ref stdcurve.tsv \
159
+ -x fluorescence -v concentration -d conc_ng_ul
160
+ ```
161
+
162
+ ### Bootstrap confidence intervals
163
+
164
+ ```bash
165
+ dftk stat data.tsv -c value -g group --bootstrap 1000 --randomseed 42 -o \
166
+ | dftk pivot -i group -v value_mean -f mean -o \
167
+ | dftk stat - -c group_A group_B
168
+ ```
169
+
170
+ ## Input/output
171
+
172
+ All commands accept:
173
+
174
+ - A TSV filename as a positional argument
175
+ - `-` to read TSV from stdin
176
+ - `...` to receive a parquet path from stdin (written by a previous `-o` command)
177
+ - A `.parquet` filename to read a named parquet file directly
178
+
179
+ Standard output options (available on all tabular commands):
180
+
181
+ - `-o` / `--output` — write a temp parquet for the next piped command instead of TSV to stdout
182
+ - `--select col1 col2 …` — keep only these columns
183
+ - `--drop col1 col2 …` — remove these columns
184
+ - `--round N` — round numeric output
185
+ - `--postquery EXPR` — filter output rows after processing
186
+ - `--meta KEY=VALUE` — embed provenance metadata in parquet output (repeatable)
187
+
188
+ ### Provenance annotations
189
+
190
+ Metadata embedded with `--meta` is stored in the parquet file schema and propagates
191
+ automatically through the pipe: every subsequent `-o` write re-embeds it alongside
192
+ any new `--meta` values.
193
+
194
+ ```bash
195
+ # Tag a file at creation
196
+ dftk eval raw.tsv -f "z = x + y" -o results.parquet \
197
+ --meta genome=hg38 --meta source=gwas_2024
198
+
199
+ # Inspect annotations
200
+ dftk annotate results.parquet
201
+ # genome hg38
202
+ # source gwas_2024
203
+
204
+ # Add or update an annotation in-place
205
+ dftk annotate results.parquet --set step=qc_filtered
206
+
207
+ # Annotations survive piping
208
+ dftk eval results.parquet -f "z_scaled = z / 2" -o | dftk scale ... -c z -o scaled.parquet
209
+ dftk annotate scaled.parquet # genome and source still present
210
+ ```
211
+
212
+ ## Figure options (plot commands)
213
+
214
+ ```text
215
+ --size single|double|full|WxH figure size (single ≈ 3.5", double ≈ 7.2")
216
+ --fontsize screen|publication|presentation
217
+ -f FILE save to file (PNG/PDF/SVG); omit to display
218
+ --groupcol COL colour-code by this column
219
+ --subgraphcol COL split into subplot grid by this column
220
+ --ncols N columns in subplot grid (default: auto)
221
+ --legend TEXT legend label for ungrouped series
222
+ ```
223
+
224
+ ## Dependencies
225
+
226
+ - `pandas`, `numpy`, `scipy` — core data handling and statistics
227
+ - `statsmodels` — regression (`fit`, `scale --resid`, `wstat`)
228
+ - `duckdb` — SQL queries (`query --sql`)
229
+ - `matplotlib`, `seaborn` — plots
230
+ - `pyarrow` — parquet I/O backend
@@ -0,0 +1,42 @@
1
+ dftk/__init__.py,sha256=Gp46arZlP72ZfUKQ9K13fR4_3ZbdGezi1PPQVaEkcFU,138
2
+ dftk/cli.py,sha256=o6PR9H10cYXUnS8nce0sJR0V6RoNSgIZP4jbWc2u7U4,2239
3
+ dftk/commands/__init__.py,sha256=RhY0u6TCU1c4hzE6O1vFofvpjxuPuuA00LHa3k54jcM,3437
4
+ dftk/commands/annotate_cmd.py,sha256=TS_1Be2AqqYCAyJTV8XLbdTq9OizmLo2-3mha14X1ck,5212
5
+ dftk/commands/base.py,sha256=zH_OPSmkAniB0ii7I-UC5ePS__Bb9glIMzL97hLl5U8,2938
6
+ dftk/commands/binx_cmd.py,sha256=qk5rV-jv4dLo-FCKV5g4hVm09uwbyw1rC-rxFNQJID0,3897
7
+ dftk/commands/clean_cmd.py,sha256=OIDjkCCxXqQGb6sFwGNnukj9z-Z_X52Y0-UhvBLMoYo,1537
8
+ dftk/commands/concat_cmd.py,sha256=BWB005QDsg4u3_Y8gHKT2jm5wDm-fBRvNCJlCop_23I,4978
9
+ dftk/commands/corr_cmd.py,sha256=w4MH0RRgTp497LDCFSY6VCIAGZGTB6UNRh8fZJ-rIhY,9095
10
+ dftk/commands/dataset_cmd.py,sha256=CZ9Yt15TchCosE_ha6ydzRMwuCZVoT62qlL11iRp9I0,5411
11
+ dftk/commands/describe_cmd.py,sha256=tE6Vn3iISxSJAkgeN0jSQ1_duCwYA1IvfA1Zsmu9f6s,11839
12
+ dftk/commands/eval_cmd.py,sha256=pQy4oycLqIxk9GbfNruQd7AbZTvhVeOHrvOs59X0WKY,21896
13
+ dftk/commands/fit_cmd.py,sha256=39hQERm8-YISGIb7brJSytyRQnZ3-efJ12Yldpuok8A,8352
14
+ dftk/commands/func_cmd.py,sha256=Sbi2OeR3koxnzOJdHzjIZShmM021Xg-y4yJLjSG15rw,5809
15
+ dftk/commands/help.py,sha256=yEVgLmos4MRDYlCHJJpUYToGhbumHEGiuRrKfq_lXFU,2172
16
+ dftk/commands/hist_cmd.py,sha256=UCmfaSPMwX3c0ZllcDrw65andd0X1pj4eQnaa1xToEc,9490
17
+ dftk/commands/interp_cmd.py,sha256=bZYU7SUz2D0j0IB3-HE999FXqNbP3wazSmYYRYDQSko,8503
18
+ dftk/commands/line_cmd.py,sha256=TstXMOc5p99ShIdOexpg74RPl10LmiWTniGdDwLr_ww,9877
19
+ dftk/commands/melt_cmd.py,sha256=7aYKOKx4zMv52w5P75n_48OdHgUiQI2_ZF5J0_lBy-A,2019
20
+ dftk/commands/merge_cmd.py,sha256=WRtbWWkorF_Bt9jZHivh5Q-GhGAFO7un9w4DQL7WjlQ,11972
21
+ dftk/commands/pivot_cmd.py,sha256=A8J2pCgihJBt-EpczQiZ3TwJg3b7w57y78dMKE_3VJ8,14143
22
+ dftk/commands/print_cmd.py,sha256=SxXxIfkTwRvxVQ12kgyDrLc6nYp0AgfD6UdPw4dKXLI,1077
23
+ dftk/commands/query_cmd.py,sha256=oiQxzHQxv8rFtANcNtI6lk_-mxur_fxcn9KBpf8ontY,7428
24
+ dftk/commands/randvar_cmd.py,sha256=5dj81YbraOdK5xna5nKKAFJFSh0Ojd77xbSr1efP9Us,6358
25
+ dftk/commands/sample_cmd.py,sha256=9KQEwp7p_DFD6nhTRT5-29yS0lTtiQKFOzTWDgX0aRY,3666
26
+ dftk/commands/scale_cmd.py,sha256=M3u_o1uRvO6mxiRDLlo0KCDDpCHTa3s3hS17zGa7KdY,9668
27
+ dftk/commands/scat_cmd.py,sha256=1bc9TXcgvWvjVfKEaEWixVua_EggDldZVY4sTd4QaWk,8816
28
+ dftk/commands/split_cmd.py,sha256=uq7f0rgLYfi6ec7BO8M6SPOdwXM4pDqnwD17a9N7WxQ,3668
29
+ dftk/commands/stat_cmd.py,sha256=Ct15RVGJ24125cYPydx_y6K5pwhji0eca8CW6EnvaBo,8916
30
+ dftk/commands/test_cmd.py,sha256=rbBPA3J0HaLQFvh9qnvra2cBJ-D5r2WFFryuIHyZTfM,7550
31
+ dftk/commands/wstat_cmd.py,sha256=i_Vn2k60yHGTVf56NPlRF3yH4CT9o69OKJPaI9wHp7Q,4575
32
+ dftk/common/__init__.py,sha256=s9-EGsv9ITWJWnXcWB8k5eHxUvf8mN18qs8WEAyhZqo,340
33
+ dftk/common/io.py,sha256=eJNZHEi77e1nH4op803IS9HDF4kKJKt_4CDp60Vd9nA,24956
34
+ dftk/common/plot.py,sha256=FZ1sfXgmg3z_GLBLDi8hqKhDy4B9NVI-ehNYa0zvStw,17846
35
+ dftk/common/seed.py,sha256=GdOUIu4Y3Kv9dIazuciBrVoRb1OLJ4NGgTu7BE3dGwE,863
36
+ dftk/common/seq.py,sha256=iohaxp8-56CGWb_bsxSvt8AUk5QhHlb9vMuPgVZF_ow,4489
37
+ dftk/common/stats.py,sha256=5dVy7J_9CudHPJYk6lrisQ3JLuSikzhqDT5YW1o_j0k,6745
38
+ dftk/io.py,sha256=LpVW3AbvPPl1GOdFxAuK23_eNgIiLFbhtAF1Zo9Gogo,285
39
+ dataframe_toolkit-0.4.1.dist-info/WHEEL,sha256=r-Se0i_n47Mj8pdnVuq7W628oP9YAKMaTjp4l3lQcns,81
40
+ dataframe_toolkit-0.4.1.dist-info/entry_points.txt,sha256=UmzJ50KmmTmZ7YygucsEmenPTXWmRKJMYpimPyBodQc,40
41
+ dataframe_toolkit-0.4.1.dist-info/METADATA,sha256=FzGggE9lxY3hpMYrmnGSxWWSI2affgcOH1d2My5iFNo,9286
42
+ dataframe_toolkit-0.4.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.31
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ dftk = dftk.cli:main
3
+
dftk/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ # dftk — DataFrame analysis and manipulation toolkit
2
+ from importlib.metadata import version
3
+
4
+ __version__ = version("dataframe-toolkit")
dftk/cli.py ADDED
@@ -0,0 +1,72 @@
1
+ """
2
+ dftk.cli — entry point for the ``dftk`` command.
3
+
4
+ All subcommands are discovered through the COMMANDS list in
5
+ dftk/commands/__init__.py. This file contains no command-specific
6
+ logic and never needs to change when a new subcommand is added.
7
+ """
8
+
9
+ import argparse
10
+ import logging
11
+ import sys
12
+
13
+ from dftk import __version__
14
+ from dftk.commands import COMMANDS
15
+ from dftk.common.io import DFTK_TMPDIR
16
+
17
+ logger = logging.getLogger("dftk")
18
+ logging.basicConfig(format="%(asctime)s %(module)s %(message)s", level=logging.INFO)
19
+
20
+
21
+ def main() -> None:
22
+ parser = argparse.ArgumentParser(
23
+ prog="dftk",
24
+ description="DataFrame analysis and manipulation toolkit.",
25
+ formatter_class=argparse.RawDescriptionHelpFormatter,
26
+ epilog=(
27
+ "Pipe mode — use -o alone to pass data between commands:\n"
28
+ " dftk stat -g grp -s val -o \\\n"
29
+ " | dftk pivot -i grp -v val_mean -o \\\n"
30
+ " | dftk print\n\n"
31
+ f"Temp pipe files are stored in {DFTK_TMPDIR}\n"
32
+ "Run 'dftk clean' to remove any leftover pipe files."
33
+ ),
34
+ )
35
+
36
+ parser.add_argument("--version", action="version", version=f"dftk {__version__}")
37
+
38
+ subparsers = parser.add_subparsers(dest="command", metavar="COMMAND")
39
+ subparsers.required = True
40
+
41
+ for cmd in COMMANDS:
42
+ cmd_parser = subparsers.add_parser(cmd.name, help=cmd.help)
43
+ cmd.add_arguments(cmd_parser)
44
+ cmd_parser.set_defaults(command_instance=cmd)
45
+
46
+ args = parser.parse_args()
47
+
48
+ try:
49
+ args.command_instance.execute(args)
50
+ except FileNotFoundError as exc:
51
+ print(f"File error: {exc}", file=sys.stderr)
52
+ sys.exit(1)
53
+ except ValueError as exc:
54
+ print(f"Input error: {exc}", file=sys.stderr)
55
+ sys.exit(1)
56
+ except RuntimeError as exc:
57
+ print(f"Processing error: {exc}", file=sys.stderr)
58
+ sys.exit(1)
59
+ except Exception as exc:
60
+ import traceback
61
+
62
+ print(
63
+ f"Unexpected error ({type(exc).__name__}): {exc}\n"
64
+ "Please report this with the full traceback below.",
65
+ file=sys.stderr,
66
+ )
67
+ traceback.print_exc(file=sys.stderr)
68
+ sys.exit(1)
69
+
70
+
71
+ if __name__ == "__main__":
72
+ main()
@@ -0,0 +1,100 @@
1
+ # dftk.commands — explicit registry of all dftk subcommands.
2
+ #
3
+ # To add a new subcommand:
4
+ # 1. Create src/dftk/commands/<name>.py with a class that inherits
5
+ # from BaseCommand (see base.py for the contract and an example skeleton).
6
+ # 2. Import the class here and add an instance to command_list below.
7
+ # That's it — cli.py discovers commands solely through this COMMANDS list.
8
+
9
+ from dftk.commands.annotate_cmd import AnnotateCommand
10
+ from dftk.commands.binx_cmd import BinxCommand
11
+ from dftk.commands.clean_cmd import CleanCommand
12
+ from dftk.commands.concat_cmd import ConcatCommand
13
+ from dftk.commands.corr_cmd import CorrCommand
14
+ from dftk.commands.dataset_cmd import DatasetCommand
15
+ from dftk.commands.describe_cmd import DescribeCommand
16
+ from dftk.commands.eval_cmd import EvalCommand
17
+ from dftk.commands.fit_cmd import FitCommand
18
+ from dftk.commands.func_cmd import FuncCommand
19
+ from dftk.commands.help import HelpCommand
20
+ from dftk.commands.hist_cmd import HistCommand
21
+ from dftk.commands.interp_cmd import InterpCommand
22
+ from dftk.commands.line_cmd import LineCommand
23
+ from dftk.commands.melt_cmd import MeltCommand
24
+ from dftk.commands.merge_cmd import MergeCommand
25
+ from dftk.commands.pivot_cmd import PivotCommand
26
+ from dftk.commands.print_cmd import PrintCommand
27
+ from dftk.commands.query_cmd import QueryCommand
28
+ from dftk.commands.randvar_cmd import RandvarCommand
29
+ from dftk.commands.sample_cmd import SampleCommand
30
+ from dftk.commands.scale_cmd import ScaleCommand
31
+ from dftk.commands.scat_cmd import ScatCommand
32
+ from dftk.commands.split_cmd import SplitCommand
33
+ from dftk.commands.stat_cmd import StatCommand
34
+ from dftk.commands.test_cmd import TestCommand
35
+ from dftk.commands.wstat_cmd import WstatCommand
36
+
37
+ # Ported subcommands — uncomment as each one is added:
38
+ # from dftk.commands.segid import SegidCommand
39
+ # from dftk.commands.info import InfoCommand
40
+ # from dftk.commands.transpose import TransposeCommand
41
+ # from dftk.commands.color import ColorCommand
42
+ # from dftk.commands.wavelet import WaveletCommand
43
+ # from dftk.commands.fisher import FisherCommand # superseded by eval's
44
+ # # fisher_test/fisher_OR
45
+
46
+ COMMAND_GROUPS: list[tuple[str, list]] = [
47
+ (
48
+ "Data transformation",
49
+ [
50
+ EvalCommand(),
51
+ QueryCommand(),
52
+ MergeCommand(),
53
+ ConcatCommand(),
54
+ MeltCommand(),
55
+ PivotCommand(),
56
+ FuncCommand(),
57
+ ScaleCommand(),
58
+ InterpCommand(),
59
+ BinxCommand(),
60
+ ],
61
+ ),
62
+ (
63
+ "Statistics",
64
+ [
65
+ StatCommand(),
66
+ WstatCommand(),
67
+ FitCommand(),
68
+ TestCommand(),
69
+ CorrCommand(),
70
+ DescribeCommand(),
71
+ RandvarCommand(),
72
+ ],
73
+ ),
74
+ (
75
+ "Plots",
76
+ [
77
+ ScatCommand(),
78
+ LineCommand(),
79
+ HistCommand(),
80
+ ],
81
+ ),
82
+ (
83
+ "Utilities",
84
+ [
85
+ DatasetCommand(),
86
+ SampleCommand(),
87
+ SplitCommand(),
88
+ AnnotateCommand(),
89
+ PrintCommand(),
90
+ CleanCommand(),
91
+ ],
92
+ ),
93
+ ]
94
+
95
+ command_list = [cmd for _, cmds in COMMAND_GROUPS for cmd in cmds]
96
+
97
+ # HelpCommand receives the live list and groups so it can introspect any peer.
98
+ help_command = HelpCommand(command_list, COMMAND_GROUPS)
99
+
100
+ COMMANDS = command_list + [help_command]
@@ -0,0 +1,165 @@
1
+ """
2
+ dftk.commands.annotate_cmd — dftk annotate subcommand.
3
+
4
+ Read and write provenance metadata embedded in parquet files.
5
+
6
+ Parquet stores arbitrary key-value string pairs in the file-level schema
7
+ metadata. dftk uses this to carry provenance annotations (genome build,
8
+ source dataset, processing steps, etc.) through a pipeline.
9
+
10
+ Usage examples:
11
+
12
+ # List all annotations on a file
13
+ dftk annotate data.parquet
14
+
15
+ # Set / update annotations (modifies the file in-place)
16
+ dftk annotate data.parquet --set genome=hg38 --set source=gwas_2024
17
+
18
+ # Get a single annotation
19
+ dftk annotate data.parquet --get genome
20
+
21
+ # Remove an annotation
22
+ dftk annotate data.parquet --delete genome
23
+
24
+ # Embed annotations when writing parquet from any dftk command
25
+ dftk eval data.tsv -f "z = x + y" -o result.parquet \\
26
+ --meta genome=hg38 --meta source=my_pipeline
27
+
28
+ Annotations propagate through the parquet pipe: a file annotated at
29
+ creation is re-annotated on every subsequent -o write unless explicitly
30
+ overridden.
31
+ """
32
+
33
+ import argparse
34
+ import sys
35
+
36
+ import pyarrow.parquet as pq
37
+
38
+ from dftk.commands.base import BaseCommand
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # Helpers
42
+ # ---------------------------------------------------------------------------
43
+
44
+
45
+ def _read_meta(path: str) -> dict[str, str]:
46
+ """Return custom (non-pandas) schema metadata from *path*."""
47
+ schema = pq.read_schema(path)
48
+ raw = schema.metadata or {}
49
+ return {k.decode(): v.decode() for k, v in raw.items() if k != b"pandas"}
50
+
51
+
52
+ def _write_meta(path: str, meta: dict[str, str]) -> None:
53
+ """Overwrite the custom metadata in the parquet file at *path* in-place."""
54
+ table = pq.read_table(path)
55
+ existing = table.schema.metadata or {}
56
+ # Keep the pandas key intact; replace everything else with *meta*
57
+ pandas_meta = {k: v for k, v in existing.items() if k == b"pandas"}
58
+ new_meta = {
59
+ **pandas_meta,
60
+ **{k.encode(): v.encode() for k, v in meta.items()},
61
+ }
62
+ table = table.replace_schema_metadata(new_meta)
63
+ pq.write_table(table, path)
64
+
65
+
66
+ # ---------------------------------------------------------------------------
67
+ # Command
68
+ # ---------------------------------------------------------------------------
69
+
70
+
71
+ class AnnotateCommand(BaseCommand):
72
+ @property
73
+ def name(self) -> str:
74
+ return "annotate"
75
+
76
+ @property
77
+ def help(self) -> str:
78
+ return "Read and write provenance metadata in parquet files"
79
+
80
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
81
+ import argparse as ap
82
+
83
+ parser.formatter_class = ap.RawDescriptionHelpFormatter
84
+ parser.description = __doc__
85
+
86
+ parser.add_argument(
87
+ "PARQUET",
88
+ help="Parquet file to inspect or annotate.",
89
+ )
90
+ parser.add_argument(
91
+ "--set",
92
+ dest="set_meta",
93
+ help="Set a metadata annotation (KEY=VALUE). Repeatable.",
94
+ default=[],
95
+ action="append",
96
+ metavar="KEY=VALUE",
97
+ )
98
+ parser.add_argument(
99
+ "--get",
100
+ dest="get_key",
101
+ help="Print the value of a single annotation key.",
102
+ default=None,
103
+ metavar="KEY",
104
+ )
105
+ parser.add_argument(
106
+ "--delete",
107
+ dest="delete_keys",
108
+ help="Remove an annotation key. Repeatable.",
109
+ default=[],
110
+ action="append",
111
+ metavar="KEY",
112
+ )
113
+ parser.add_argument(
114
+ "--clear",
115
+ help="Remove ALL custom annotations from the file.",
116
+ action="store_true",
117
+ )
118
+
119
+ def execute(self, args: argparse.Namespace) -> None:
120
+ path: str = args.PARQUET
121
+
122
+ if not path.endswith(".parquet"):
123
+ print(
124
+ f"annotate: {path!r} does not appear to be a parquet file.",
125
+ file=sys.stderr,
126
+ )
127
+ sys.exit(1)
128
+
129
+ # ---- mutations (--set / --delete / --clear) — applied first -------
130
+
131
+ if args.clear or args.set_meta or args.delete_keys:
132
+ meta = {} if args.clear else _read_meta(path)
133
+
134
+ for item in args.set_meta:
135
+ try:
136
+ k, v = item.split("=", 1)
137
+ meta[k.strip()] = v.strip()
138
+ except ValueError:
139
+ print(
140
+ f"annotate --set: expected KEY=VALUE, got {item!r}",
141
+ file=sys.stderr,
142
+ )
143
+ sys.exit(1)
144
+
145
+ for key in args.delete_keys:
146
+ meta.pop(key, None)
147
+
148
+ _write_meta(path, meta)
149
+
150
+ # ---- read current state and respond --------------------------------
151
+
152
+ meta = _read_meta(path)
153
+
154
+ if args.get_key is not None:
155
+ value = meta.get(args.get_key)
156
+ if value is None:
157
+ print(
158
+ f"annotate --get: key {args.get_key!r} not found.", file=sys.stderr
159
+ )
160
+ sys.exit(1)
161
+ print(value)
162
+ else:
163
+ # Default: list all annotations as TSV (key sorted)
164
+ for k, v in sorted(meta.items()):
165
+ print(f"{k}\t{v}")