csvql-query 2.6.2__py3-none-win_amd64.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.
- csvql/__init__.py +146 -0
- csvql/_loader.py +90 -0
- csvql_query-2.6.2.dist-info/METADATA +104 -0
- csvql_query-2.6.2.dist-info/RECORD +6 -0
- csvql_query-2.6.2.dist-info/WHEEL +5 -0
- csvql_query-2.6.2.dist-info/top_level.txt +1 -0
csvql/__init__.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""
|
|
2
|
+
csvql — fast CSV querying from Python.
|
|
3
|
+
|
|
4
|
+
Usage::
|
|
5
|
+
|
|
6
|
+
import csvql
|
|
7
|
+
|
|
8
|
+
# Returns a list of dicts
|
|
9
|
+
rows = csvql.query("SELECT category, COUNT(*) FROM 'sales.csv' GROUP BY category")
|
|
10
|
+
# [{"category": "Electronics", "COUNT(*)": "42"}, ...]
|
|
11
|
+
|
|
12
|
+
# Returns raw CSV string (header + rows)
|
|
13
|
+
csv_text = csvql.query_csv("SELECT * FROM 'data.csv' WHERE amount > 100")
|
|
14
|
+
|
|
15
|
+
# Optional pandas integration (pandas not required)
|
|
16
|
+
df = csvql.query_df("SELECT region, SUM(revenue) FROM 'data.csv' GROUP BY region")
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import ctypes
|
|
20
|
+
import csv
|
|
21
|
+
import io
|
|
22
|
+
import json
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from ._loader import load
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def query(sql: str) -> list[dict[str, Any]]:
|
|
29
|
+
"""Run a SQL query and return results as a list of dicts.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
sql: SQL string, e.g. ``"SELECT * FROM 'data.csv' WHERE age > 30"``
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
List of row dicts, e.g. ``[{"name": "Alice", "age": "31"}, ...]``
|
|
36
|
+
All values are strings (CSV has no type information).
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
RuntimeError: if the query fails (bad SQL, file not found, etc.)
|
|
40
|
+
"""
|
|
41
|
+
lib = load()
|
|
42
|
+
out = ctypes.c_void_p(None)
|
|
43
|
+
rc = lib.csvql_query_json(sql.encode(), ctypes.byref(out))
|
|
44
|
+
addr = out.value
|
|
45
|
+
if rc != 0:
|
|
46
|
+
raw = ctypes.string_at(addr) if addr else b"unknown error"
|
|
47
|
+
if addr:
|
|
48
|
+
lib.csvql_free(addr)
|
|
49
|
+
raise RuntimeError(raw.decode(errors="replace"))
|
|
50
|
+
if not addr:
|
|
51
|
+
return []
|
|
52
|
+
raw = ctypes.string_at(addr)
|
|
53
|
+
lib.csvql_free(addr)
|
|
54
|
+
return json.loads(raw.decode())
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def query_csv(sql: str) -> str:
|
|
58
|
+
"""Run a SQL query and return results as a CSV string (header + rows).
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
sql: SQL string.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
CSV text with a header row, e.g. ``"name,age\\nAlice,31\\n"``
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
RuntimeError: if the query fails.
|
|
68
|
+
"""
|
|
69
|
+
lib = load()
|
|
70
|
+
out = ctypes.c_void_p(None)
|
|
71
|
+
rc = lib.csvql_query_csv(sql.encode(), ctypes.byref(out))
|
|
72
|
+
addr = out.value
|
|
73
|
+
if rc != 0:
|
|
74
|
+
raw = ctypes.string_at(addr) if addr else b"unknown error"
|
|
75
|
+
if addr:
|
|
76
|
+
lib.csvql_free(addr)
|
|
77
|
+
raise RuntimeError(raw.decode(errors="replace"))
|
|
78
|
+
if not addr:
|
|
79
|
+
return ""
|
|
80
|
+
raw = ctypes.string_at(addr)
|
|
81
|
+
lib.csvql_free(addr)
|
|
82
|
+
return raw.decode()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def query_df(sql: str):
|
|
86
|
+
"""Run a SQL query and return a pandas DataFrame.
|
|
87
|
+
|
|
88
|
+
Requires pandas to be installed. All columns will be ``object`` dtype
|
|
89
|
+
(strings) — cast as needed after calling this function.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
sql: SQL string.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
``pandas.DataFrame``
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
ImportError: if pandas is not installed.
|
|
99
|
+
RuntimeError: if the query fails.
|
|
100
|
+
"""
|
|
101
|
+
try:
|
|
102
|
+
import pandas as pd
|
|
103
|
+
except ImportError as e:
|
|
104
|
+
raise ImportError("pandas is required for query_df(). Install it with: pip install pandas") from e
|
|
105
|
+
|
|
106
|
+
csv_text = query_csv(sql)
|
|
107
|
+
return pd.read_csv(io.StringIO(csv_text))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def query_tuples(sql: str) -> tuple[list[str], list[tuple[str, ...]]]:
|
|
111
|
+
"""Run a SQL query and return (headers, rows) as plain tuples.
|
|
112
|
+
|
|
113
|
+
Useful when you want array-of-arrays access (``row[0]``) instead of
|
|
114
|
+
dicts, or when memory overhead of dicts matters.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
sql: SQL string.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
A ``(headers, rows)`` tuple where ``headers`` is a list of column
|
|
121
|
+
name strings and ``rows`` is a list of string tuples — one per row.
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
RuntimeError: if the query fails.
|
|
125
|
+
|
|
126
|
+
Example::
|
|
127
|
+
|
|
128
|
+
headers, rows = csvql.query_tuples("SELECT name, age FROM 'data.csv'")
|
|
129
|
+
# headers: ['name', 'age']
|
|
130
|
+
# rows: [('Alice', '30'), ('Bob', '25'), ...]
|
|
131
|
+
for row in rows:
|
|
132
|
+
print(row[0], row[1])
|
|
133
|
+
"""
|
|
134
|
+
csv_text = query_csv(sql)
|
|
135
|
+
if not csv_text:
|
|
136
|
+
return [], []
|
|
137
|
+
reader = csv.reader(io.StringIO(csv_text))
|
|
138
|
+
all_rows = list(reader)
|
|
139
|
+
if not all_rows:
|
|
140
|
+
return [], []
|
|
141
|
+
headers = all_rows[0]
|
|
142
|
+
data = [tuple(r) for r in all_rows[1:] if r]
|
|
143
|
+
return headers, data
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
__all__ = ["query", "query_csv", "query_df", "query_tuples"]
|
csvql/_loader.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Locates and loads libcsvql (.dll on Windows, .dylib on macOS, .so elsewhere).
|
|
3
|
+
|
|
4
|
+
Search order:
|
|
5
|
+
1. Same directory as this file (installed wheel — lib bundled alongside .py)
|
|
6
|
+
2. zig-out/ relative to the repo root (development build)
|
|
7
|
+
3. Directories listed in CSVQL_LIB_PATH environment variable
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import ctypes
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
_lib_cache: ctypes.CDLL | None = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _lib_name() -> str:
|
|
19
|
+
if sys.platform == "win32":
|
|
20
|
+
# Zig names the Windows shared library csvql.dll — no "lib" prefix.
|
|
21
|
+
return "csvql.dll"
|
|
22
|
+
if sys.platform == "darwin":
|
|
23
|
+
return "libcsvql.dylib"
|
|
24
|
+
return "libcsvql.so"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _candidate_dirs() -> list[Path]:
|
|
28
|
+
dirs: list[Path] = []
|
|
29
|
+
|
|
30
|
+
# 1. Next to this .py file (wheel install)
|
|
31
|
+
dirs.append(Path(__file__).parent)
|
|
32
|
+
|
|
33
|
+
# 2. zig-out/ — walk up from this file looking for build.zig.
|
|
34
|
+
# Zig installs a .so/.dylib into zig-out/lib, but puts a Windows DLL
|
|
35
|
+
# in zig-out/bin alongside the executables (only the import library
|
|
36
|
+
# csvql.lib lands in lib/). Check both so a development build is
|
|
37
|
+
# found on every platform.
|
|
38
|
+
here = Path(__file__).resolve()
|
|
39
|
+
for parent in here.parents:
|
|
40
|
+
if (parent / "build.zig").exists():
|
|
41
|
+
dirs.append(parent / "zig-out" / "lib")
|
|
42
|
+
dirs.append(parent / "zig-out" / "bin")
|
|
43
|
+
break
|
|
44
|
+
|
|
45
|
+
# 3. CSVQL_LIB_PATH env override
|
|
46
|
+
env_path = os.environ.get("CSVQL_LIB_PATH")
|
|
47
|
+
if env_path:
|
|
48
|
+
dirs.append(Path(env_path))
|
|
49
|
+
|
|
50
|
+
return dirs
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def load() -> ctypes.CDLL:
|
|
54
|
+
global _lib_cache
|
|
55
|
+
if _lib_cache is not None:
|
|
56
|
+
return _lib_cache
|
|
57
|
+
|
|
58
|
+
name = _lib_name()
|
|
59
|
+
for d in _candidate_dirs():
|
|
60
|
+
candidate = d / name
|
|
61
|
+
if candidate.exists():
|
|
62
|
+
lib = ctypes.CDLL(str(candidate))
|
|
63
|
+
_setup_signatures(lib)
|
|
64
|
+
_lib_cache = lib
|
|
65
|
+
return lib
|
|
66
|
+
|
|
67
|
+
searched = "\n ".join(str(d / name) for d in _candidate_dirs())
|
|
68
|
+
raise FileNotFoundError(
|
|
69
|
+
f"Could not find {name}. Searched:\n {searched}\n"
|
|
70
|
+
"Run `zig build lib -Doptimize=ReleaseFast` to build it, "
|
|
71
|
+
"or set CSVQL_LIB_PATH to its directory."
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _setup_signatures(lib: ctypes.CDLL) -> None:
|
|
76
|
+
"""Declare argument and return types for all exported functions."""
|
|
77
|
+
lib.csvql_query_json.argtypes = [
|
|
78
|
+
ctypes.c_char_p,
|
|
79
|
+
ctypes.POINTER(ctypes.c_void_p),
|
|
80
|
+
]
|
|
81
|
+
lib.csvql_query_json.restype = ctypes.c_int
|
|
82
|
+
|
|
83
|
+
lib.csvql_query_csv.argtypes = [
|
|
84
|
+
ctypes.c_char_p,
|
|
85
|
+
ctypes.POINTER(ctypes.c_void_p),
|
|
86
|
+
]
|
|
87
|
+
lib.csvql_query_csv.restype = ctypes.c_int
|
|
88
|
+
|
|
89
|
+
lib.csvql_free.argtypes = [ctypes.c_void_p]
|
|
90
|
+
lib.csvql_free.restype = None
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: csvql-query
|
|
3
|
+
Version: 2.6.2
|
|
4
|
+
Summary: Fast CSV querying from Python — powered by a Zig/SIMD engine
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/melihbirim/csvql
|
|
7
|
+
Project-URL: Repository, https://github.com/melihbirim/csvql
|
|
8
|
+
Keywords: csv,query,sql,fast,simd,agent,ai,mcp
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Database
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Provides-Extra: pandas
|
|
23
|
+
Requires-Dist: pandas>=1.5; extra == "pandas"
|
|
24
|
+
|
|
25
|
+
# csvql-query
|
|
26
|
+
|
|
27
|
+
[](https://github.com/melihbirim/csvql/actions/workflows/ci.yml)
|
|
28
|
+
[](https://github.com/melihbirim/csvql/blob/main/LICENSE.md)
|
|
29
|
+
[](https://pypi.org/project/csvql-query/)
|
|
30
|
+
|
|
31
|
+
**Query CSV files with SQL from Python — powered by a Zig/SIMD engine.**
|
|
32
|
+
|
|
33
|
+
Zero-copy mmap reads + SIMD parsing happen before Python ever sees the data.
|
|
34
|
+
Faster than DuckDB on typical workloads, no dependencies required.
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install csvql-query
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import csvql
|
|
46
|
+
|
|
47
|
+
# Returns a list of dicts (like csv.DictReader, but with SQL)
|
|
48
|
+
rows = csvql.query("SELECT name, salary FROM 'employees.csv' WHERE salary > 100000 ORDER BY salary DESC")
|
|
49
|
+
# [{'name': 'Alice', 'salary': '185000'}, ...]
|
|
50
|
+
|
|
51
|
+
# Raw CSV string
|
|
52
|
+
csv_str = csvql.query_csv("SELECT * FROM 'data.csv' LIMIT 10")
|
|
53
|
+
|
|
54
|
+
# pandas DataFrame (pandas must be installed)
|
|
55
|
+
df = csvql.query_df("SELECT category, COUNT(*) as n FROM 'sales.csv' GROUP BY category")
|
|
56
|
+
|
|
57
|
+
# (headers, rows) tuples — no dependencies
|
|
58
|
+
headers, rows = csvql.query_tuples("SELECT name, age FROM 'users.csv' WHERE age > 25")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## API
|
|
62
|
+
|
|
63
|
+
| Function | Returns | Description |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| `query(sql)` | `list[dict]` | Execute SQL, get list of dicts |
|
|
66
|
+
| `query_csv(sql)` | `str` | Execute SQL, get raw CSV string |
|
|
67
|
+
| `query_df(sql)` | `DataFrame` | Execute SQL, get pandas DataFrame |
|
|
68
|
+
| `query_tuples(sql)` | `(list[str], list[tuple])` | Execute SQL, get (headers, rows) |
|
|
69
|
+
|
|
70
|
+
## SQL Support
|
|
71
|
+
|
|
72
|
+
The SQL path is embedded in the query string (same as the CLI):
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
# Filtering, ordering, limiting
|
|
76
|
+
csvql.query("SELECT name, city FROM 'data.csv' WHERE age > 30 ORDER BY name LIMIT 5")
|
|
77
|
+
|
|
78
|
+
# Aggregation
|
|
79
|
+
csvql.query("SELECT department, AVG(salary) FROM 'emp.csv' GROUP BY department")
|
|
80
|
+
|
|
81
|
+
# Unix pipes — use '-' as the filename
|
|
82
|
+
import subprocess, sys
|
|
83
|
+
# or just pass stdin data via the engine directly
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Full SQL reference: [SIMPLE_QUERY_LANGUAGE.md](https://github.com/melihbirim/csvql/blob/main/SIMPLE_QUERY_LANGUAGE.md)
|
|
87
|
+
|
|
88
|
+
## Performance
|
|
89
|
+
|
|
90
|
+
- mmap + SIMD parsing — data is never copied into Python memory
|
|
91
|
+
- Parallel chunk processing on multi-core machines
|
|
92
|
+
- Typically 5–9x faster than DuckDB on 1M-row CSVs
|
|
93
|
+
|
|
94
|
+
## Requirements
|
|
95
|
+
|
|
96
|
+
- Python ≥ 3.10
|
|
97
|
+
- macOS (x86_64 / arm64) or Linux (x86_64)
|
|
98
|
+
- `pandas` optional — only needed for `query_df()`
|
|
99
|
+
|
|
100
|
+
## Links
|
|
101
|
+
|
|
102
|
+
- [GitHub](https://github.com/melihbirim/csvql)
|
|
103
|
+
- [CLI Installation](https://github.com/melihbirim/csvql#installation)
|
|
104
|
+
- [SQL Reference](https://github.com/melihbirim/csvql/blob/main/SIMPLE_QUERY_LANGUAGE.md)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
csvql/__init__.py,sha256=kHEihvCYuuJb-0RQIWi_4wp-5UVFi3dPMvO9LPR7Pck,4177
|
|
2
|
+
csvql/_loader.py,sha256=pWlOb6xEevlRNYqtQv8loQJpqtxhAM1nWBoZ2TCHgkE,2776
|
|
3
|
+
csvql_query-2.6.2.dist-info/METADATA,sha256=Z3IA185CWmYbBEWZx3XJ5K3dEiPxNyFqqtdqvcfla5Y,3770
|
|
4
|
+
csvql_query-2.6.2.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
5
|
+
csvql_query-2.6.2.dist-info/top_level.txt,sha256=iVE80iZxyPJ-SsWu0-vcYeu_8yAKnru0E7PaX6ZeoMM,6
|
|
6
|
+
csvql_query-2.6.2.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
csvql
|