csvq-cli 0.1.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.
- csvq_cli-0.1.0/LICENSE +21 -0
- csvq_cli-0.1.0/PKG-INFO +79 -0
- csvq_cli-0.1.0/README.md +59 -0
- csvq_cli-0.1.0/csvq/__init__.py +2 -0
- csvq_cli-0.1.0/csvq/cli.py +396 -0
- csvq_cli-0.1.0/csvq_cli.egg-info/PKG-INFO +79 -0
- csvq_cli-0.1.0/csvq_cli.egg-info/SOURCES.txt +11 -0
- csvq_cli-0.1.0/csvq_cli.egg-info/dependency_links.txt +1 -0
- csvq_cli-0.1.0/csvq_cli.egg-info/entry_points.txt +2 -0
- csvq_cli-0.1.0/csvq_cli.egg-info/requires.txt +1 -0
- csvq_cli-0.1.0/csvq_cli.egg-info/top_level.txt +1 -0
- csvq_cli-0.1.0/pyproject.toml +28 -0
- csvq_cli-0.1.0/setup.cfg +4 -0
csvq_cli-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marcus
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
csvq_cli-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: csvq-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CSV/TSV query, filter, transform, and convert from the command line
|
|
5
|
+
Author-email: Marcus <marcus.builds.things@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/marcusbuildsthings-droid/csvq
|
|
8
|
+
Keywords: csv,tsv,query,filter,cli,data,tabular
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Utilities
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: click>=8.0
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# csvq-cli
|
|
22
|
+
|
|
23
|
+
CSV/TSV query, filter, transform, and convert from the command line.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install csvq-cli
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Commands
|
|
32
|
+
|
|
33
|
+
| Command | Description |
|
|
34
|
+
|---------|-------------|
|
|
35
|
+
| `csvq head` | Show headers and preview rows |
|
|
36
|
+
| `csvq count` | Count rows |
|
|
37
|
+
| `csvq select` | Select specific columns |
|
|
38
|
+
| `csvq filter` | Filter rows (==, !=, >, <, contains, matches, etc.) |
|
|
39
|
+
| `csvq sort` | Sort by column |
|
|
40
|
+
| `csvq unique` | Unique values with counts |
|
|
41
|
+
| `csvq stats` | Numeric stats (min, max, mean, median, sum) |
|
|
42
|
+
| `csvq grep` | Search rows matching regex |
|
|
43
|
+
| `csvq sample` | Random sample of rows |
|
|
44
|
+
| `csvq merge` | Merge multiple CSV files |
|
|
45
|
+
| `csvq to-json` | Convert to JSON |
|
|
46
|
+
| `csvq to-tsv` | Convert to TSV |
|
|
47
|
+
| `csvq to-md` | Convert to Markdown table |
|
|
48
|
+
|
|
49
|
+
## Examples
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Preview a file
|
|
53
|
+
csvq head data.csv
|
|
54
|
+
|
|
55
|
+
# Filter and convert
|
|
56
|
+
csvq filter data.csv status == active --json
|
|
57
|
+
|
|
58
|
+
# Stats on a column
|
|
59
|
+
csvq stats sales.csv revenue
|
|
60
|
+
|
|
61
|
+
# Grep across all columns
|
|
62
|
+
csvq grep data.csv "error|warning"
|
|
63
|
+
|
|
64
|
+
# Select columns and sort
|
|
65
|
+
csvq select data.csv name email | csvq sort - name
|
|
66
|
+
|
|
67
|
+
# Pipe from stdin
|
|
68
|
+
cat data.csv | csvq count
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
All commands support `--json` output. Auto-detects delimiter (CSV, TSV, semicolon, pipe).
|
|
72
|
+
|
|
73
|
+
## For AI Agents
|
|
74
|
+
|
|
75
|
+
See [SKILL.md](SKILL.md) for agent-optimized documentation.
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
MIT
|
csvq_cli-0.1.0/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# csvq-cli
|
|
2
|
+
|
|
3
|
+
CSV/TSV query, filter, transform, and convert from the command line.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install csvq-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
| Command | Description |
|
|
14
|
+
|---------|-------------|
|
|
15
|
+
| `csvq head` | Show headers and preview rows |
|
|
16
|
+
| `csvq count` | Count rows |
|
|
17
|
+
| `csvq select` | Select specific columns |
|
|
18
|
+
| `csvq filter` | Filter rows (==, !=, >, <, contains, matches, etc.) |
|
|
19
|
+
| `csvq sort` | Sort by column |
|
|
20
|
+
| `csvq unique` | Unique values with counts |
|
|
21
|
+
| `csvq stats` | Numeric stats (min, max, mean, median, sum) |
|
|
22
|
+
| `csvq grep` | Search rows matching regex |
|
|
23
|
+
| `csvq sample` | Random sample of rows |
|
|
24
|
+
| `csvq merge` | Merge multiple CSV files |
|
|
25
|
+
| `csvq to-json` | Convert to JSON |
|
|
26
|
+
| `csvq to-tsv` | Convert to TSV |
|
|
27
|
+
| `csvq to-md` | Convert to Markdown table |
|
|
28
|
+
|
|
29
|
+
## Examples
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Preview a file
|
|
33
|
+
csvq head data.csv
|
|
34
|
+
|
|
35
|
+
# Filter and convert
|
|
36
|
+
csvq filter data.csv status == active --json
|
|
37
|
+
|
|
38
|
+
# Stats on a column
|
|
39
|
+
csvq stats sales.csv revenue
|
|
40
|
+
|
|
41
|
+
# Grep across all columns
|
|
42
|
+
csvq grep data.csv "error|warning"
|
|
43
|
+
|
|
44
|
+
# Select columns and sort
|
|
45
|
+
csvq select data.csv name email | csvq sort - name
|
|
46
|
+
|
|
47
|
+
# Pipe from stdin
|
|
48
|
+
cat data.csv | csvq count
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
All commands support `--json` output. Auto-detects delimiter (CSV, TSV, semicolon, pipe).
|
|
52
|
+
|
|
53
|
+
## For AI Agents
|
|
54
|
+
|
|
55
|
+
See [SKILL.md](SKILL.md) for agent-optimized documentation.
|
|
56
|
+
|
|
57
|
+
## License
|
|
58
|
+
|
|
59
|
+
MIT
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"""csvq-cli — CSV/TSV query, filter, transform, and convert."""
|
|
2
|
+
import csv
|
|
3
|
+
import io
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
import re
|
|
7
|
+
import operator
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from collections import Counter
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
|
|
13
|
+
# ── helpers ──────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
def _detect_dialect(path_or_text, is_file=True):
|
|
16
|
+
"""Detect CSV dialect (delimiter, quoting)."""
|
|
17
|
+
if is_file:
|
|
18
|
+
with open(path_or_text, newline="") as f:
|
|
19
|
+
sample = f.read(8192)
|
|
20
|
+
else:
|
|
21
|
+
sample = path_or_text[:8192]
|
|
22
|
+
try:
|
|
23
|
+
dialect = csv.Sniffer().sniff(sample, delimiters=",\t;|")
|
|
24
|
+
return dialect
|
|
25
|
+
except csv.Error:
|
|
26
|
+
return csv.excel
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _read_rows(file, delimiter, has_header=True):
|
|
30
|
+
"""Read CSV into (headers, rows). Returns (None, rows) if no header."""
|
|
31
|
+
if file == "-":
|
|
32
|
+
text = sys.stdin.read()
|
|
33
|
+
dialect = _detect_dialect(text, is_file=False)
|
|
34
|
+
else:
|
|
35
|
+
dialect = _detect_dialect(file, is_file=True)
|
|
36
|
+
with open(file) as f:
|
|
37
|
+
text = f.read()
|
|
38
|
+
|
|
39
|
+
if delimiter:
|
|
40
|
+
dialect.delimiter = delimiter
|
|
41
|
+
|
|
42
|
+
reader = csv.reader(io.StringIO(text), dialect)
|
|
43
|
+
all_rows = list(reader)
|
|
44
|
+
if not all_rows:
|
|
45
|
+
return ([], [])
|
|
46
|
+
if has_header:
|
|
47
|
+
return (all_rows[0], all_rows[1:])
|
|
48
|
+
return (None, all_rows)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _output(headers, rows, fmt, file=None):
|
|
52
|
+
"""Output rows in requested format."""
|
|
53
|
+
if fmt == "json":
|
|
54
|
+
if headers:
|
|
55
|
+
data = [dict(zip(headers, r)) for r in rows]
|
|
56
|
+
else:
|
|
57
|
+
data = rows
|
|
58
|
+
out = json.dumps(data, indent=2, ensure_ascii=False)
|
|
59
|
+
elif fmt == "tsv":
|
|
60
|
+
buf = io.StringIO()
|
|
61
|
+
w = csv.writer(buf, delimiter="\t", lineterminator="\n")
|
|
62
|
+
if headers:
|
|
63
|
+
w.writerow(headers)
|
|
64
|
+
w.writerows(rows)
|
|
65
|
+
out = buf.getvalue()
|
|
66
|
+
elif fmt == "markdown":
|
|
67
|
+
if not headers:
|
|
68
|
+
headers = [f"col{i}" for i in range(len(rows[0]))] if rows else []
|
|
69
|
+
lines = ["| " + " | ".join(headers) + " |"]
|
|
70
|
+
lines.append("| " + " | ".join(["---"] * len(headers)) + " |")
|
|
71
|
+
for r in rows:
|
|
72
|
+
lines.append("| " + " | ".join(r) + " |")
|
|
73
|
+
out = "\n".join(lines) + "\n"
|
|
74
|
+
else: # csv
|
|
75
|
+
buf = io.StringIO()
|
|
76
|
+
w = csv.writer(buf, lineterminator="\n")
|
|
77
|
+
if headers:
|
|
78
|
+
w.writerow(headers)
|
|
79
|
+
w.writerows(rows)
|
|
80
|
+
out = buf.getvalue()
|
|
81
|
+
|
|
82
|
+
if file:
|
|
83
|
+
Path(file).write_text(out)
|
|
84
|
+
click.echo(f"Written to {file}")
|
|
85
|
+
else:
|
|
86
|
+
click.echo(out, nl=False)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
OPS = {
|
|
90
|
+
"==": operator.eq, "!=": operator.ne,
|
|
91
|
+
">": operator.gt, ">=": operator.ge,
|
|
92
|
+
"<": operator.lt, "<=": operator.le,
|
|
93
|
+
"contains": lambda a, b: b in a,
|
|
94
|
+
"startswith": lambda a, b: a.startswith(b),
|
|
95
|
+
"endswith": lambda a, b: a.endswith(b),
|
|
96
|
+
"matches": lambda a, b: bool(re.search(b, a)),
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _coerce(val):
|
|
101
|
+
"""Try numeric coercion for comparisons."""
|
|
102
|
+
try:
|
|
103
|
+
return float(val)
|
|
104
|
+
except (ValueError, TypeError):
|
|
105
|
+
return val
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ── CLI ──────────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
@click.group()
|
|
111
|
+
@click.version_option()
|
|
112
|
+
def cli():
|
|
113
|
+
"""CSV/TSV query, filter, transform, and convert."""
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@cli.command()
|
|
117
|
+
@click.argument("file", default="-")
|
|
118
|
+
@click.option("-d", "--delimiter", help="Force delimiter")
|
|
119
|
+
@click.option("--json", "fmt", flag_value="json", help="JSON output")
|
|
120
|
+
def head(file, delimiter, fmt):
|
|
121
|
+
"""Show headers and first rows."""
|
|
122
|
+
headers, rows = _read_rows(file, delimiter)
|
|
123
|
+
preview = rows[:10]
|
|
124
|
+
if fmt == "json":
|
|
125
|
+
click.echo(json.dumps({"headers": headers, "rows": len(rows), "preview": [dict(zip(headers, r)) for r in preview]}, indent=2))
|
|
126
|
+
else:
|
|
127
|
+
if headers:
|
|
128
|
+
click.echo(f"Columns ({len(headers)}): {', '.join(headers)}")
|
|
129
|
+
click.echo(f"Rows: {len(rows)}")
|
|
130
|
+
click.echo()
|
|
131
|
+
for r in preview:
|
|
132
|
+
click.echo(" | ".join(r))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@cli.command()
|
|
136
|
+
@click.argument("file", default="-")
|
|
137
|
+
@click.option("-d", "--delimiter", help="Force delimiter")
|
|
138
|
+
@click.option("--json", "fmt", flag_value="json", help="JSON output")
|
|
139
|
+
def count(file, delimiter, fmt):
|
|
140
|
+
"""Count rows."""
|
|
141
|
+
headers, rows = _read_rows(file, delimiter)
|
|
142
|
+
n = len(rows)
|
|
143
|
+
if fmt == "json":
|
|
144
|
+
click.echo(json.dumps({"count": n}))
|
|
145
|
+
else:
|
|
146
|
+
click.echo(n)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@cli.command()
|
|
150
|
+
@click.argument("file", default="-")
|
|
151
|
+
@click.argument("columns", nargs=-1, required=True)
|
|
152
|
+
@click.option("-d", "--delimiter", help="Force delimiter")
|
|
153
|
+
@click.option("-o", "--output", "outfile", help="Output file")
|
|
154
|
+
@click.option("--json", "fmt", flag_value="json", help="JSON output")
|
|
155
|
+
@click.option("--tsv", "fmt", flag_value="tsv", help="TSV output")
|
|
156
|
+
@click.option("--md", "fmt", flag_value="markdown", help="Markdown output")
|
|
157
|
+
def select(file, columns, delimiter, outfile, fmt):
|
|
158
|
+
"""Select specific columns by name or index."""
|
|
159
|
+
headers, rows = _read_rows(file, delimiter)
|
|
160
|
+
if not headers:
|
|
161
|
+
click.echo("Error: no headers found", err=True)
|
|
162
|
+
sys.exit(1)
|
|
163
|
+
|
|
164
|
+
indices = []
|
|
165
|
+
out_headers = []
|
|
166
|
+
for c in columns:
|
|
167
|
+
if c.isdigit():
|
|
168
|
+
idx = int(c)
|
|
169
|
+
indices.append(idx)
|
|
170
|
+
out_headers.append(headers[idx] if idx < len(headers) else f"col{idx}")
|
|
171
|
+
elif c in headers:
|
|
172
|
+
indices.append(headers.index(c))
|
|
173
|
+
out_headers.append(c)
|
|
174
|
+
else:
|
|
175
|
+
click.echo(f"Error: column '{c}' not found. Available: {', '.join(headers)}", err=True)
|
|
176
|
+
sys.exit(1)
|
|
177
|
+
|
|
178
|
+
out_rows = [[r[i] if i < len(r) else "" for i in indices] for r in rows]
|
|
179
|
+
_output(out_headers, out_rows, fmt or "csv", outfile)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@cli.command()
|
|
183
|
+
@click.argument("file", default="-")
|
|
184
|
+
@click.argument("column")
|
|
185
|
+
@click.argument("op", type=click.Choice(list(OPS.keys())))
|
|
186
|
+
@click.argument("value")
|
|
187
|
+
@click.option("-d", "--delimiter", help="Force delimiter")
|
|
188
|
+
@click.option("-o", "--output", "outfile", help="Output file")
|
|
189
|
+
@click.option("--json", "fmt", flag_value="json", help="JSON output")
|
|
190
|
+
@click.option("--tsv", "fmt", flag_value="tsv", help="TSV output")
|
|
191
|
+
@click.option("--md", "fmt", flag_value="markdown", help="Markdown output")
|
|
192
|
+
def filter(file, column, op, value, delimiter, outfile, fmt):
|
|
193
|
+
"""Filter rows where COLUMN OP VALUE."""
|
|
194
|
+
headers, rows = _read_rows(file, delimiter)
|
|
195
|
+
if column.isdigit():
|
|
196
|
+
idx = int(column)
|
|
197
|
+
elif column in headers:
|
|
198
|
+
idx = headers.index(column)
|
|
199
|
+
else:
|
|
200
|
+
click.echo(f"Error: column '{column}' not found", err=True)
|
|
201
|
+
sys.exit(1)
|
|
202
|
+
|
|
203
|
+
op_fn = OPS[op]
|
|
204
|
+
out = []
|
|
205
|
+
for r in rows:
|
|
206
|
+
cell = r[idx] if idx < len(r) else ""
|
|
207
|
+
a, b = _coerce(cell), _coerce(value)
|
|
208
|
+
try:
|
|
209
|
+
if op_fn(a, b):
|
|
210
|
+
out.append(r)
|
|
211
|
+
except TypeError:
|
|
212
|
+
if op_fn(str(a), str(b)):
|
|
213
|
+
out.append(r)
|
|
214
|
+
_output(headers, out, fmt or "csv", outfile)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@cli.command()
|
|
218
|
+
@click.argument("file", default="-")
|
|
219
|
+
@click.argument("column")
|
|
220
|
+
@click.option("--desc", is_flag=True, help="Descending order")
|
|
221
|
+
@click.option("-n", "--numeric", is_flag=True, help="Numeric sort")
|
|
222
|
+
@click.option("-d", "--delimiter", help="Force delimiter")
|
|
223
|
+
@click.option("-o", "--output", "outfile", help="Output file")
|
|
224
|
+
@click.option("--json", "fmt", flag_value="json", help="JSON output")
|
|
225
|
+
@click.option("--tsv", "fmt", flag_value="tsv", help="TSV output")
|
|
226
|
+
def sort(file, column, desc, numeric, delimiter, outfile, fmt):
|
|
227
|
+
"""Sort rows by column."""
|
|
228
|
+
headers, rows = _read_rows(file, delimiter)
|
|
229
|
+
if column.isdigit():
|
|
230
|
+
idx = int(column)
|
|
231
|
+
elif column in headers:
|
|
232
|
+
idx = headers.index(column)
|
|
233
|
+
else:
|
|
234
|
+
click.echo(f"Error: column '{column}' not found", err=True)
|
|
235
|
+
sys.exit(1)
|
|
236
|
+
|
|
237
|
+
def key_fn(r):
|
|
238
|
+
v = r[idx] if idx < len(r) else ""
|
|
239
|
+
if numeric:
|
|
240
|
+
try:
|
|
241
|
+
return float(v)
|
|
242
|
+
except ValueError:
|
|
243
|
+
return float("inf")
|
|
244
|
+
return v
|
|
245
|
+
|
|
246
|
+
rows.sort(key=key_fn, reverse=desc)
|
|
247
|
+
_output(headers, rows, fmt or "csv", outfile)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@cli.command()
|
|
251
|
+
@click.argument("file", default="-")
|
|
252
|
+
@click.argument("column")
|
|
253
|
+
@click.option("-d", "--delimiter", help="Force delimiter")
|
|
254
|
+
@click.option("--json", "fmt", flag_value="json", help="JSON output")
|
|
255
|
+
def unique(file, column, delimiter, fmt):
|
|
256
|
+
"""Show unique values in a column."""
|
|
257
|
+
headers, rows = _read_rows(file, delimiter)
|
|
258
|
+
if column.isdigit():
|
|
259
|
+
idx = int(column)
|
|
260
|
+
elif column in headers:
|
|
261
|
+
idx = headers.index(column)
|
|
262
|
+
else:
|
|
263
|
+
click.echo(f"Error: column '{column}' not found", err=True)
|
|
264
|
+
sys.exit(1)
|
|
265
|
+
|
|
266
|
+
counts = Counter(r[idx] if idx < len(r) else "" for r in rows)
|
|
267
|
+
if fmt == "json":
|
|
268
|
+
click.echo(json.dumps({"column": column, "unique": len(counts), "values": dict(counts.most_common())}, indent=2))
|
|
269
|
+
else:
|
|
270
|
+
for val, cnt in counts.most_common():
|
|
271
|
+
click.echo(f"{cnt:>6} {val}")
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@cli.command()
|
|
275
|
+
@click.argument("file", default="-")
|
|
276
|
+
@click.argument("column")
|
|
277
|
+
@click.option("-d", "--delimiter", help="Force delimiter")
|
|
278
|
+
@click.option("--json", "fmt", flag_value="json", help="JSON output")
|
|
279
|
+
def stats(file, column, delimiter, fmt):
|
|
280
|
+
"""Basic stats for a numeric column (min, max, mean, sum, count)."""
|
|
281
|
+
headers, rows = _read_rows(file, delimiter)
|
|
282
|
+
if column.isdigit():
|
|
283
|
+
idx = int(column)
|
|
284
|
+
elif column in headers:
|
|
285
|
+
idx = headers.index(column)
|
|
286
|
+
else:
|
|
287
|
+
click.echo(f"Error: column '{column}' not found", err=True)
|
|
288
|
+
sys.exit(1)
|
|
289
|
+
|
|
290
|
+
vals = []
|
|
291
|
+
for r in rows:
|
|
292
|
+
try:
|
|
293
|
+
vals.append(float(r[idx]))
|
|
294
|
+
except (ValueError, IndexError):
|
|
295
|
+
pass
|
|
296
|
+
|
|
297
|
+
if not vals:
|
|
298
|
+
click.echo("No numeric values found", err=True)
|
|
299
|
+
sys.exit(1)
|
|
300
|
+
|
|
301
|
+
result = {
|
|
302
|
+
"column": column,
|
|
303
|
+
"count": len(vals),
|
|
304
|
+
"min": min(vals),
|
|
305
|
+
"max": max(vals),
|
|
306
|
+
"sum": sum(vals),
|
|
307
|
+
"mean": round(sum(vals) / len(vals), 4),
|
|
308
|
+
}
|
|
309
|
+
vals_sorted = sorted(vals)
|
|
310
|
+
n = len(vals_sorted)
|
|
311
|
+
result["median"] = vals_sorted[n // 2] if n % 2 else (vals_sorted[n // 2 - 1] + vals_sorted[n // 2]) / 2
|
|
312
|
+
|
|
313
|
+
if fmt == "json":
|
|
314
|
+
click.echo(json.dumps(result, indent=2))
|
|
315
|
+
else:
|
|
316
|
+
for k, v in result.items():
|
|
317
|
+
click.echo(f"{k:>8}: {v}")
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
@cli.command("to-json")
|
|
321
|
+
@click.argument("file", default="-")
|
|
322
|
+
@click.option("-d", "--delimiter", help="Force delimiter")
|
|
323
|
+
@click.option("-o", "--output", "outfile", help="Output file")
|
|
324
|
+
def to_json(file, delimiter, outfile):
|
|
325
|
+
"""Convert CSV to JSON."""
|
|
326
|
+
headers, rows = _read_rows(file, delimiter)
|
|
327
|
+
_output(headers, rows, "json", outfile)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
@cli.command("to-tsv")
|
|
331
|
+
@click.argument("file", default="-")
|
|
332
|
+
@click.option("-d", "--delimiter", help="Force delimiter")
|
|
333
|
+
@click.option("-o", "--output", "outfile", help="Output file")
|
|
334
|
+
def to_tsv(file, delimiter, outfile):
|
|
335
|
+
"""Convert CSV to TSV."""
|
|
336
|
+
headers, rows = _read_rows(file, delimiter)
|
|
337
|
+
_output(headers, rows, "tsv", outfile)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
@cli.command("to-md")
|
|
341
|
+
@click.argument("file", default="-")
|
|
342
|
+
@click.option("-d", "--delimiter", help="Force delimiter")
|
|
343
|
+
@click.option("-o", "--output", "outfile", help="Output file")
|
|
344
|
+
def to_md(file, delimiter, outfile):
|
|
345
|
+
"""Convert CSV to Markdown table."""
|
|
346
|
+
headers, rows = _read_rows(file, delimiter)
|
|
347
|
+
_output(headers, rows, "markdown", outfile)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
@cli.command()
|
|
351
|
+
@click.argument("file", default="-")
|
|
352
|
+
@click.argument("pattern")
|
|
353
|
+
@click.option("-d", "--delimiter", help="Force delimiter")
|
|
354
|
+
@click.option("-o", "--output", "outfile", help="Output file")
|
|
355
|
+
@click.option("--json", "fmt", flag_value="json", help="JSON output")
|
|
356
|
+
def grep(file, pattern, delimiter, outfile, fmt):
|
|
357
|
+
"""Search rows matching pattern (any column)."""
|
|
358
|
+
headers, rows = _read_rows(file, delimiter)
|
|
359
|
+
regex = re.compile(pattern, re.IGNORECASE)
|
|
360
|
+
matched = [r for r in rows if any(regex.search(cell) for cell in r)]
|
|
361
|
+
_output(headers, matched, fmt or "csv", outfile)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
@cli.command()
|
|
365
|
+
@click.argument("files", nargs=-1, required=True)
|
|
366
|
+
@click.option("-d", "--delimiter", help="Force delimiter")
|
|
367
|
+
@click.option("-o", "--output", "outfile", help="Output file")
|
|
368
|
+
@click.option("--json", "fmt", flag_value="json", help="JSON output")
|
|
369
|
+
def merge(files, delimiter, outfile, fmt):
|
|
370
|
+
"""Merge multiple CSV files (same columns)."""
|
|
371
|
+
all_headers = None
|
|
372
|
+
all_rows = []
|
|
373
|
+
for f in files:
|
|
374
|
+
h, r = _read_rows(f, delimiter)
|
|
375
|
+
if all_headers is None:
|
|
376
|
+
all_headers = h
|
|
377
|
+
all_rows.extend(r)
|
|
378
|
+
_output(all_headers, all_rows, fmt or "csv", outfile)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
@cli.command()
|
|
382
|
+
@click.argument("file", default="-")
|
|
383
|
+
@click.option("-n", "--rows", "nrows", default=10, help="Number of rows")
|
|
384
|
+
@click.option("-d", "--delimiter", help="Force delimiter")
|
|
385
|
+
@click.option("-o", "--output", "outfile", help="Output file")
|
|
386
|
+
@click.option("--json", "fmt", flag_value="json", help="JSON output")
|
|
387
|
+
def sample(file, nrows, delimiter, outfile, fmt):
|
|
388
|
+
"""Random sample of rows."""
|
|
389
|
+
import random
|
|
390
|
+
headers, rows = _read_rows(file, delimiter)
|
|
391
|
+
sampled = random.sample(rows, min(nrows, len(rows)))
|
|
392
|
+
_output(headers, sampled, fmt or "csv", outfile)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
if __name__ == "__main__":
|
|
396
|
+
cli()
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: csvq-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CSV/TSV query, filter, transform, and convert from the command line
|
|
5
|
+
Author-email: Marcus <marcus.builds.things@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/marcusbuildsthings-droid/csvq
|
|
8
|
+
Keywords: csv,tsv,query,filter,cli,data,tabular
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Utilities
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: click>=8.0
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# csvq-cli
|
|
22
|
+
|
|
23
|
+
CSV/TSV query, filter, transform, and convert from the command line.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install csvq-cli
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Commands
|
|
32
|
+
|
|
33
|
+
| Command | Description |
|
|
34
|
+
|---------|-------------|
|
|
35
|
+
| `csvq head` | Show headers and preview rows |
|
|
36
|
+
| `csvq count` | Count rows |
|
|
37
|
+
| `csvq select` | Select specific columns |
|
|
38
|
+
| `csvq filter` | Filter rows (==, !=, >, <, contains, matches, etc.) |
|
|
39
|
+
| `csvq sort` | Sort by column |
|
|
40
|
+
| `csvq unique` | Unique values with counts |
|
|
41
|
+
| `csvq stats` | Numeric stats (min, max, mean, median, sum) |
|
|
42
|
+
| `csvq grep` | Search rows matching regex |
|
|
43
|
+
| `csvq sample` | Random sample of rows |
|
|
44
|
+
| `csvq merge` | Merge multiple CSV files |
|
|
45
|
+
| `csvq to-json` | Convert to JSON |
|
|
46
|
+
| `csvq to-tsv` | Convert to TSV |
|
|
47
|
+
| `csvq to-md` | Convert to Markdown table |
|
|
48
|
+
|
|
49
|
+
## Examples
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Preview a file
|
|
53
|
+
csvq head data.csv
|
|
54
|
+
|
|
55
|
+
# Filter and convert
|
|
56
|
+
csvq filter data.csv status == active --json
|
|
57
|
+
|
|
58
|
+
# Stats on a column
|
|
59
|
+
csvq stats sales.csv revenue
|
|
60
|
+
|
|
61
|
+
# Grep across all columns
|
|
62
|
+
csvq grep data.csv "error|warning"
|
|
63
|
+
|
|
64
|
+
# Select columns and sort
|
|
65
|
+
csvq select data.csv name email | csvq sort - name
|
|
66
|
+
|
|
67
|
+
# Pipe from stdin
|
|
68
|
+
cat data.csv | csvq count
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
All commands support `--json` output. Auto-detects delimiter (CSV, TSV, semicolon, pipe).
|
|
72
|
+
|
|
73
|
+
## For AI Agents
|
|
74
|
+
|
|
75
|
+
See [SKILL.md](SKILL.md) for agent-optimized documentation.
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
csvq/__init__.py
|
|
5
|
+
csvq/cli.py
|
|
6
|
+
csvq_cli.egg-info/PKG-INFO
|
|
7
|
+
csvq_cli.egg-info/SOURCES.txt
|
|
8
|
+
csvq_cli.egg-info/dependency_links.txt
|
|
9
|
+
csvq_cli.egg-info/entry_points.txt
|
|
10
|
+
csvq_cli.egg-info/requires.txt
|
|
11
|
+
csvq_cli.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
click>=8.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
csvq
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "csvq-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "CSV/TSV query, filter, transform, and convert from the command line"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [{name = "Marcus", email = "marcus.builds.things@gmail.com"}]
|
|
13
|
+
keywords = ["csv", "tsv", "query", "filter", "cli", "data", "tabular"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Topic :: Utilities",
|
|
21
|
+
]
|
|
22
|
+
dependencies = ["click>=8.0"]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
csvq = "csvq.cli:cli"
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/marcusbuildsthings-droid/csvq"
|
csvq_cli-0.1.0/setup.cfg
ADDED