snkv 0.2.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.
- snkv-0.2.0/PKG-INFO +15 -0
- snkv-0.2.0/README.md +349 -0
- snkv-0.2.0/pyproject.toml +45 -0
- snkv-0.2.0/setup.cfg +4 -0
- snkv-0.2.0/setup.py +207 -0
- snkv-0.2.0/snkv/__init__.py +470 -0
- snkv-0.2.0/snkv/_snkv.pyi +128 -0
- snkv-0.2.0/snkv/py.typed +0 -0
- snkv-0.2.0/snkv.egg-info/PKG-INFO +15 -0
- snkv-0.2.0/snkv.egg-info/SOURCES.txt +24 -0
- snkv-0.2.0/snkv.egg-info/dependency_links.txt +1 -0
- snkv-0.2.0/snkv.egg-info/top_level.txt +1 -0
- snkv-0.2.0/snkv_module.c +1263 -0
- snkv-0.2.0/tests/test_acid.py +200 -0
- snkv-0.2.0/tests/test_autovacuum.py +165 -0
- snkv-0.2.0/tests/test_basic.py +435 -0
- snkv-0.2.0/tests/test_checkpoint.py +173 -0
- snkv-0.2.0/tests/test_column_family.py +241 -0
- snkv-0.2.0/tests/test_concurrent.py +154 -0
- snkv-0.2.0/tests/test_config.py +228 -0
- snkv-0.2.0/tests/test_crash_recovery.py +239 -0
- snkv-0.2.0/tests/test_json.py +260 -0
- snkv-0.2.0/tests/test_prefix.py +298 -0
- snkv-0.2.0/tests/test_prod.py +282 -0
- snkv-0.2.0/tests/test_stress.py +342 -0
- snkv-0.2.0/tests/test_wal.py +407 -0
snkv-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: snkv
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Python bindings for SNKV - crash-safe embedded key-value store
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Project-URL: Homepage, https://github.com/hash-anu/snkv
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: C
|
|
9
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
10
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
11
|
+
Classifier: Operating System :: MacOS
|
|
12
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
13
|
+
Classifier: Topic :: Database
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
+
Requires-Python: >=3.8
|
snkv-0.2.0/README.md
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
# SNKV Python Bindings
|
|
2
|
+
|
|
3
|
+
[](https://github.com/hash-anu/snkv/actions/workflows/c-cpp.yml)
|
|
4
|
+
[](https://github.com/hash-anu/snkv/blob/master/LICENSE)
|
|
5
|
+
|
|
6
|
+
Idiomatic Python 3.8+ bindings for [SNKV](https://github.com/hash-anu/snkv) — a lightweight,
|
|
7
|
+
ACID-compliant embedded key-value store built directly on SQLite's B-Tree engine.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Dict-style API** — `db["key"] = value`, `val = db["key"]`, `del db["key"]`, `"key" in db`
|
|
14
|
+
- **Context managers** — `with KVStore(...) as db` and `with db.create_column_family(...) as cf` for guaranteed cleanup
|
|
15
|
+
- **Prefix iterators** — efficient namespace scans with `db.prefix_iterator(b"user:")`
|
|
16
|
+
- **WAL checkpoint control** — PASSIVE / FULL / RESTART / TRUNCATE modes via `db.checkpoint()`
|
|
17
|
+
- **Auto-checkpoint** — set `wal_size_limit=N` to checkpoint automatically after every N WAL frames
|
|
18
|
+
- **Typed exceptions** — `NotFoundError`, `BusyError`, `LockedError`, `ReadOnlyError`, `CorruptError` all subclass `snkv.Error`
|
|
19
|
+
- **No Python dependencies** — pure CPython C extension; only requires a C compiler and `python3-dev`
|
|
20
|
+
- **247 tests** — full pytest suite covering ACID, WAL, crash recovery, concurrency, column families, JSON, and more
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
### Linux
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# System dependencies
|
|
30
|
+
sudo apt-get install -y build-essential python3-dev python3-pip
|
|
31
|
+
|
|
32
|
+
# Python build dependencies
|
|
33
|
+
pip3 install setuptools wheel pytest
|
|
34
|
+
|
|
35
|
+
# Build
|
|
36
|
+
cd python
|
|
37
|
+
python3 setup.py build_ext --inplace
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
#### macOS
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Compiler (skip if already installed)
|
|
44
|
+
xcode-select --install
|
|
45
|
+
|
|
46
|
+
# Python build dependencies
|
|
47
|
+
pip3 install setuptools wheel pytest
|
|
48
|
+
|
|
49
|
+
# Build
|
|
50
|
+
cd python
|
|
51
|
+
python3 setup.py build_ext --inplace
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
#### Windows — Native Python (recommended)
|
|
55
|
+
|
|
56
|
+
1. Install [Python 3.8+](https://python.org/downloads) — check **"Add Python to PATH"**
|
|
57
|
+
2. Install [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) — select **"Desktop development with C++"**
|
|
58
|
+
3. Open **"x64 Native Tools Command Prompt for VS 2022"** from the Start Menu (required for 64-bit Python; "Developer PowerShell for VS" defaults to 32-bit and will fail)
|
|
59
|
+
|
|
60
|
+
```cmd
|
|
61
|
+
:: Python build dependencies
|
|
62
|
+
pip install setuptools wheel pytest
|
|
63
|
+
|
|
64
|
+
:: Build
|
|
65
|
+
cd python
|
|
66
|
+
python setup.py build_ext --inplace
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
#### Windows — MSYS2 MinGW64 shell
|
|
70
|
+
|
|
71
|
+
Open the **MSYS2 MinGW64** shell (not plain MSYS2, not cmd.exe):
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# System + Python dependencies (one-time)
|
|
75
|
+
pacman -S --needed mingw-w64-x86_64-python \
|
|
76
|
+
mingw-w64-x86_64-python-pip \
|
|
77
|
+
mingw-w64-x86_64-python-setuptools \
|
|
78
|
+
mingw-w64-x86_64-python-pytest
|
|
79
|
+
|
|
80
|
+
# Build
|
|
81
|
+
cd python
|
|
82
|
+
python3 setup.py build_ext --inplace
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
> On all platforms, `setup.py` automatically locates `snkv.h` — no manual
|
|
86
|
+
> header step needed. On Linux/macOS it regenerates it via `make snkv.h`;
|
|
87
|
+
> on Windows it falls back to the pre-built `snkv.h` included in the repo.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Quick Start
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from snkv import KVStore
|
|
95
|
+
|
|
96
|
+
with KVStore("mydb.db") as db:
|
|
97
|
+
db["hello"] = "world"
|
|
98
|
+
print(db["hello"].decode()) # world
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## API Reference
|
|
104
|
+
|
|
105
|
+
### Opening a store
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from snkv import KVStore, JOURNAL_WAL, JOURNAL_DELETE, SYNC_NORMAL, SYNC_OFF, SYNC_FULL
|
|
109
|
+
|
|
110
|
+
with KVStore(
|
|
111
|
+
"mydb.db",
|
|
112
|
+
journal_mode=JOURNAL_WAL, # JOURNAL_WAL (default) or JOURNAL_DELETE
|
|
113
|
+
sync_level=SYNC_NORMAL, # SYNC_NORMAL (default), SYNC_OFF, SYNC_FULL
|
|
114
|
+
cache_size=2000, # pages (~8 MB default)
|
|
115
|
+
page_size=4096, # bytes; new databases only
|
|
116
|
+
busy_timeout=5000, # ms to retry on SQLITE_BUSY (default 0)
|
|
117
|
+
read_only=False, # open read-only
|
|
118
|
+
wal_size_limit=100, # auto-checkpoint every 100 WAL frames (0 = off)
|
|
119
|
+
) as db:
|
|
120
|
+
...
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### CRUD
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
# Write
|
|
127
|
+
db["key"] = b"value" # bytes or str keys/values are both accepted
|
|
128
|
+
db["key"] = "value" # str is UTF-8 encoded automatically
|
|
129
|
+
|
|
130
|
+
# Read
|
|
131
|
+
val = db["key"] # returns bytes; raises NotFoundError if missing
|
|
132
|
+
val = db.get("key") # returns bytes or None
|
|
133
|
+
val = db.get("key", b"def") # with default
|
|
134
|
+
|
|
135
|
+
# Check existence
|
|
136
|
+
exists = "key" in db
|
|
137
|
+
exists = db.exists(b"key")
|
|
138
|
+
|
|
139
|
+
# Delete
|
|
140
|
+
del db["key"]
|
|
141
|
+
db.delete(b"key") # same as del; no error if key absent
|
|
142
|
+
|
|
143
|
+
# Upsert
|
|
144
|
+
db.put(b"key", b"value") # identical to db["key"] = value
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Transactions
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
db.begin(write=True)
|
|
151
|
+
db["a"] = "1"
|
|
152
|
+
db["b"] = "2"
|
|
153
|
+
db.commit() # persist
|
|
154
|
+
|
|
155
|
+
db.begin(write=True)
|
|
156
|
+
db["c"] = "3"
|
|
157
|
+
db.rollback() # discard — "c" is never written
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Auto-commit is the default: each `db["key"] = value` outside an explicit transaction is
|
|
161
|
+
committed immediately.
|
|
162
|
+
|
|
163
|
+
### Column Families
|
|
164
|
+
|
|
165
|
+
Logical namespaces within a single database file. Always close `cf` before `db`.
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
# Create (first use)
|
|
169
|
+
with db.create_column_family("users") as cf:
|
|
170
|
+
cf[b"alice"] = b"admin"
|
|
171
|
+
cf[b"bob"] = b"viewer"
|
|
172
|
+
|
|
173
|
+
# Open (subsequent uses)
|
|
174
|
+
with db.open_column_family("users") as cf:
|
|
175
|
+
print(cf[b"alice"]) # b"admin"
|
|
176
|
+
|
|
177
|
+
# List all column families
|
|
178
|
+
names = db.list_column_families() # ["users", ...]
|
|
179
|
+
|
|
180
|
+
# Drop
|
|
181
|
+
db.drop_column_family("users")
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Iterators
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
# Full scan — yields (key, value) tuples in key order
|
|
188
|
+
for key, value in db.iterator():
|
|
189
|
+
print(key, value)
|
|
190
|
+
|
|
191
|
+
# Prefix scan
|
|
192
|
+
for key, value in db.prefix_iterator(b"user:"):
|
|
193
|
+
print(key, value)
|
|
194
|
+
|
|
195
|
+
# Manual control
|
|
196
|
+
it = db.iterator()
|
|
197
|
+
it.first()
|
|
198
|
+
while not it.eof:
|
|
199
|
+
print(it.key, it.value)
|
|
200
|
+
it.next()
|
|
201
|
+
it.close()
|
|
202
|
+
|
|
203
|
+
# As a context manager
|
|
204
|
+
with db.iterator() as it:
|
|
205
|
+
for key, value in it:
|
|
206
|
+
...
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### WAL Checkpoint
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
from snkv import CHECKPOINT_PASSIVE, CHECKPOINT_FULL, CHECKPOINT_RESTART, CHECKPOINT_TRUNCATE
|
|
213
|
+
|
|
214
|
+
# Returns (nLog, nCkpt) — WAL frames total / frames written to DB
|
|
215
|
+
nlog, nckpt = db.checkpoint(CHECKPOINT_PASSIVE) # copy frames without blocking
|
|
216
|
+
nlog, nckpt = db.checkpoint(CHECKPOINT_FULL) # wait for writers, flush all
|
|
217
|
+
nlog, nckpt = db.checkpoint(CHECKPOINT_RESTART) # like FULL, reset write position
|
|
218
|
+
nlog, nckpt = db.checkpoint(CHECKPOINT_TRUNCATE) # like RESTART, truncate WAL file
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Must be called outside an active write transaction. Use `wal_size_limit` to auto-checkpoint
|
|
222
|
+
instead.
|
|
223
|
+
|
|
224
|
+
### Maintenance
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
db.sync() # flush OS write buffers (fsync)
|
|
228
|
+
db.vacuum(100) # reclaim up to 100 unused pages incrementally
|
|
229
|
+
db.integrity_check() # raises CorruptError if database is corrupt
|
|
230
|
+
stats = db.stats() # dict: {"puts": N, "gets": N, "deletes": N, "iterations": N}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Error Hierarchy
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
snkv.Error (base)
|
|
239
|
+
├── snkv.NotFoundError (also KeyError — raised by db["missing"])
|
|
240
|
+
├── snkv.BusyError (SQLITE_BUSY — another writer holds the lock)
|
|
241
|
+
├── snkv.LockedError (SQLITE_LOCKED)
|
|
242
|
+
├── snkv.ReadOnlyError (write attempted on read-only store)
|
|
243
|
+
└── snkv.CorruptError (database file is corrupt)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
import snkv
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
val = db["missing_key"]
|
|
251
|
+
except snkv.NotFoundError:
|
|
252
|
+
val = b"default"
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
db["key"] = b"value"
|
|
256
|
+
except snkv.BusyError:
|
|
257
|
+
# retry after a delay
|
|
258
|
+
...
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Running Tests
|
|
264
|
+
|
|
265
|
+
**Linux / macOS**
|
|
266
|
+
```bash
|
|
267
|
+
cd python
|
|
268
|
+
python3 -m pytest tests/ -v
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
**Windows — Native Python (x64 Native Tools Command Prompt for VS 2022)**
|
|
272
|
+
```cmd
|
|
273
|
+
cd python
|
|
274
|
+
set PYTHONPATH=.
|
|
275
|
+
python -m pytest tests\ -v
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
**Windows — MSYS2 MinGW64 shell**
|
|
279
|
+
```bash
|
|
280
|
+
cd python
|
|
281
|
+
PYTHONPATH=. python3 -m pytest tests/ -v
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
All 247 tests should pass.
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Running Examples
|
|
289
|
+
|
|
290
|
+
**Linux / macOS**
|
|
291
|
+
```bash
|
|
292
|
+
cd python
|
|
293
|
+
PYTHONPATH=. python3 examples/basic.py # CRUD, binary data, in-memory store
|
|
294
|
+
PYTHONPATH=. python3 examples/transactions.py # begin/commit/rollback
|
|
295
|
+
PYTHONPATH=. python3 examples/column_families.py # logical namespaces
|
|
296
|
+
PYTHONPATH=. python3 examples/iterators.py # ordered scan, prefix scan
|
|
297
|
+
PYTHONPATH=. python3 examples/config.py # journal mode, sync, cache, WAL limit
|
|
298
|
+
PYTHONPATH=. python3 examples/checkpoint.py # manual + auto WAL checkpoint
|
|
299
|
+
PYTHONPATH=. python3 examples/session_store.py # real-world session store pattern
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**Windows — Native Python (x64 Native Tools Command Prompt for VS 2022)**
|
|
303
|
+
```cmd
|
|
304
|
+
cd python
|
|
305
|
+
set PYTHONPATH=.
|
|
306
|
+
python examples\basic.py
|
|
307
|
+
python examples\transactions.py
|
|
308
|
+
python examples\column_families.py
|
|
309
|
+
python examples\iterators.py
|
|
310
|
+
python examples\config.py
|
|
311
|
+
python examples\checkpoint.py
|
|
312
|
+
python examples\session_store.py
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
**Windows — MSYS2 MinGW64 shell**
|
|
316
|
+
```bash
|
|
317
|
+
cd python
|
|
318
|
+
PYTHONPATH=. python3 examples/basic.py
|
|
319
|
+
PYTHONPATH=. python3 examples/transactions.py
|
|
320
|
+
# ... same pattern for all examples
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## Thread Safety
|
|
326
|
+
|
|
327
|
+
Each thread must use its own `KVStore` instance. WAL mode serialises concurrent writers
|
|
328
|
+
at the SQLite level — a `BusyError` is raised (or retried up to `busy_timeout` ms) when
|
|
329
|
+
two writers collide. Multiple readers always make progress concurrently in WAL mode.
|
|
330
|
+
|
|
331
|
+
```python
|
|
332
|
+
import threading
|
|
333
|
+
from snkv import KVStore, JOURNAL_WAL
|
|
334
|
+
|
|
335
|
+
def worker(db_path, worker_id):
|
|
336
|
+
# Each thread opens its own connection
|
|
337
|
+
with KVStore(db_path, journal_mode=JOURNAL_WAL, busy_timeout=5000) as db:
|
|
338
|
+
db[f"key_{worker_id}".encode()] = b"value"
|
|
339
|
+
|
|
340
|
+
threads = [threading.Thread(target=worker, args=("mydb.db", i)) for i in range(4)]
|
|
341
|
+
for t in threads: t.start()
|
|
342
|
+
for t in threads: t.join()
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## License
|
|
348
|
+
|
|
349
|
+
Apache License 2.0 © 2025 Hash Anu
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "snkv"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
requires-python = ">=3.8"
|
|
9
|
+
description = "Python bindings for SNKV - crash-safe embedded key-value store"
|
|
10
|
+
license = {text = "Apache-2.0"}
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"Programming Language :: C",
|
|
14
|
+
"License :: OSI Approved :: Apache Software License",
|
|
15
|
+
"Operating System :: POSIX :: Linux",
|
|
16
|
+
"Operating System :: MacOS",
|
|
17
|
+
"Operating System :: Microsoft :: Windows",
|
|
18
|
+
"Topic :: Database",
|
|
19
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://github.com/hash-anu/snkv"
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.packages.find]
|
|
26
|
+
where = ["."]
|
|
27
|
+
include = ["snkv*"]
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.package-data]
|
|
30
|
+
snkv = ["py.typed", "_snkv.pyi"]
|
|
31
|
+
|
|
32
|
+
# ── cibuildwheel ────────────────────────────────────────────────────────────
|
|
33
|
+
[tool.cibuildwheel]
|
|
34
|
+
# Build CPython 3.8 – 3.13; skip 32-bit and musl builds
|
|
35
|
+
build = "cp38-* cp39-* cp310-* cp311-* cp312-* cp313-*"
|
|
36
|
+
skip = "*-win32 *-manylinux_i686 *-musllinux*"
|
|
37
|
+
|
|
38
|
+
[tool.cibuildwheel.linux]
|
|
39
|
+
archs = ["x86_64"]
|
|
40
|
+
|
|
41
|
+
[tool.cibuildwheel.macos]
|
|
42
|
+
archs = ["x86_64", "arm64"]
|
|
43
|
+
|
|
44
|
+
[tool.cibuildwheel.windows]
|
|
45
|
+
archs = ["AMD64"]
|
snkv-0.2.0/setup.cfg
ADDED
snkv-0.2.0/setup.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Build script for the snkv Python extension.
|
|
3
|
+
|
|
4
|
+
Before compiling the C extension this script:
|
|
5
|
+
1. Runs `make snkv.h` in the repo root to regenerate the amalgamated header.
|
|
6
|
+
2. Copies the freshly built snkv.h into this directory so the compiler
|
|
7
|
+
finds it alongside snkv_module.c.
|
|
8
|
+
|
|
9
|
+
Run from the repo root:
|
|
10
|
+
pip install -e python/
|
|
11
|
+
or from this directory:
|
|
12
|
+
python setup.py build_ext --inplace
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import shutil
|
|
17
|
+
import subprocess
|
|
18
|
+
import sys
|
|
19
|
+
import urllib.request
|
|
20
|
+
from setuptools import setup, Extension
|
|
21
|
+
from setuptools.command.build_ext import build_ext as _build_ext
|
|
22
|
+
|
|
23
|
+
# Absolute path of the repository root (one level above this file).
|
|
24
|
+
REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
25
|
+
HERE = os.path.abspath(os.path.dirname(__file__))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class BuildExtWithHeader(_build_ext):
|
|
29
|
+
"""Custom build_ext that regenerates snkv.h before compiling."""
|
|
30
|
+
|
|
31
|
+
def run(self) -> None:
|
|
32
|
+
self._regenerate_snkv_header()
|
|
33
|
+
# Ensure the package directory exists so the inplace copy succeeds
|
|
34
|
+
# on fresh clones where snkv/ may not yet contain any built artifact.
|
|
35
|
+
os.makedirs(os.path.join(HERE, "snkv"), exist_ok=True)
|
|
36
|
+
super().run()
|
|
37
|
+
|
|
38
|
+
def build_extension(self, ext: Extension) -> None:
|
|
39
|
+
self._regenerate_snkv_header()
|
|
40
|
+
super().build_extension(ext)
|
|
41
|
+
|
|
42
|
+
def _regenerate_snkv_header(self) -> None:
|
|
43
|
+
target = os.path.join(HERE, "snkv.h")
|
|
44
|
+
|
|
45
|
+
# Only regenerate once per build run.
|
|
46
|
+
if getattr(self, "_snkv_h_ready", False):
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
# Try `make snkv.h` first (works on Linux, macOS, and Windows MSYS2).
|
|
50
|
+
# If make is not on PATH, fall back to downloading from GitHub — same
|
|
51
|
+
# on all platforms.
|
|
52
|
+
try:
|
|
53
|
+
result = subprocess.run(
|
|
54
|
+
["make", "snkv.h"],
|
|
55
|
+
cwd=REPO_ROOT,
|
|
56
|
+
check=True,
|
|
57
|
+
capture_output=True,
|
|
58
|
+
text=True,
|
|
59
|
+
)
|
|
60
|
+
if result.stdout:
|
|
61
|
+
print(result.stdout, end="")
|
|
62
|
+
|
|
63
|
+
# Copy the generated header from repo root into python/
|
|
64
|
+
src = os.path.join(REPO_ROOT, "snkv.h")
|
|
65
|
+
if not os.path.exists(src):
|
|
66
|
+
raise FileNotFoundError(
|
|
67
|
+
f"`make snkv.h` succeeded but {src} was not found."
|
|
68
|
+
)
|
|
69
|
+
shutil.copy2(src, target)
|
|
70
|
+
print(f"-- snkv: copied {src} -> {target}")
|
|
71
|
+
|
|
72
|
+
except FileNotFoundError:
|
|
73
|
+
# make not on PATH — download from GitHub (same on all platforms)
|
|
74
|
+
if os.path.exists(target):
|
|
75
|
+
print(f"-- snkv: `make` not found; using existing {target}")
|
|
76
|
+
else:
|
|
77
|
+
print("-- snkv: `make` not found; downloading snkv.h from GitHub...")
|
|
78
|
+
self._download_snkv_header(target)
|
|
79
|
+
|
|
80
|
+
except subprocess.CalledProcessError as e:
|
|
81
|
+
raise RuntimeError(
|
|
82
|
+
f"`make snkv.h` failed (exit {e.returncode}):\n{e.stderr}"
|
|
83
|
+
) from e
|
|
84
|
+
|
|
85
|
+
self._snkv_h_ready = True
|
|
86
|
+
|
|
87
|
+
def _download_snkv_header(self, target: str) -> None:
|
|
88
|
+
import json
|
|
89
|
+
import tempfile
|
|
90
|
+
import zipfile
|
|
91
|
+
from urllib.error import HTTPError
|
|
92
|
+
|
|
93
|
+
headers = {"Accept": "application/vnd.github+json"}
|
|
94
|
+
|
|
95
|
+
# 1a. Try GitHub Releases API first (works once releases are published).
|
|
96
|
+
tag = None
|
|
97
|
+
release_zip_ok = False
|
|
98
|
+
api_url = "https://api.github.com/repos/hash-anu/snkv/releases/latest"
|
|
99
|
+
try:
|
|
100
|
+
req = urllib.request.Request(api_url, headers=headers)
|
|
101
|
+
with urllib.request.urlopen(req) as resp:
|
|
102
|
+
tag = json.loads(resp.read())["tag_name"] # e.g. "v0.1.3"
|
|
103
|
+
except HTTPError as exc:
|
|
104
|
+
if exc.code == 404:
|
|
105
|
+
pass # No releases published yet — fall through to tags API
|
|
106
|
+
else:
|
|
107
|
+
raise RuntimeError(
|
|
108
|
+
f"GitHub API error ({api_url}): {exc}\n"
|
|
109
|
+
"Either install make (Linux/macOS) or ensure internet access."
|
|
110
|
+
) from exc
|
|
111
|
+
|
|
112
|
+
# 1b. If no releases, fall back to the latest git tag.
|
|
113
|
+
if tag is None:
|
|
114
|
+
tags_url = "https://api.github.com/repos/hash-anu/snkv/tags"
|
|
115
|
+
try:
|
|
116
|
+
req = urllib.request.Request(tags_url, headers=headers)
|
|
117
|
+
with urllib.request.urlopen(req) as resp:
|
|
118
|
+
tags = json.loads(resp.read())
|
|
119
|
+
if not tags:
|
|
120
|
+
raise RuntimeError("No tags found in the snkv repository.")
|
|
121
|
+
tag = tags[0]["name"] # most-recent tag first
|
|
122
|
+
except Exception as exc:
|
|
123
|
+
raise RuntimeError(
|
|
124
|
+
f"Failed to fetch tags from GitHub ({tags_url}):\n{exc}\n"
|
|
125
|
+
"Either install make (Linux/macOS) or ensure internet access."
|
|
126
|
+
) from exc
|
|
127
|
+
|
|
128
|
+
version = tag.lstrip("v") # "0.1.3"
|
|
129
|
+
|
|
130
|
+
# 2a. Try the release asset zip (requires a published GitHub Release).
|
|
131
|
+
zip_url = (
|
|
132
|
+
f"https://github.com/hash-anu/snkv/releases/download/"
|
|
133
|
+
f"{tag}/snkv-{version}.zip"
|
|
134
|
+
)
|
|
135
|
+
print(f"-- snkv: downloading {zip_url} ...")
|
|
136
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
137
|
+
zip_path = os.path.join(tmp, f"snkv-{version}.zip")
|
|
138
|
+
try:
|
|
139
|
+
urllib.request.urlretrieve(zip_url, zip_path)
|
|
140
|
+
release_zip_ok = True
|
|
141
|
+
except HTTPError as exc:
|
|
142
|
+
if exc.code == 404:
|
|
143
|
+
release_zip_ok = False # asset not uploaded yet
|
|
144
|
+
else:
|
|
145
|
+
raise RuntimeError(
|
|
146
|
+
f"Failed to download release zip ({zip_url}): {exc}\n"
|
|
147
|
+
"Either install make (Linux/macOS) or ensure internet access."
|
|
148
|
+
) from exc
|
|
149
|
+
|
|
150
|
+
if release_zip_ok:
|
|
151
|
+
# Extract snkv.h from release/snkv-{version}/include/
|
|
152
|
+
inner_path = f"snkv-{version}/include/snkv.h"
|
|
153
|
+
with zipfile.ZipFile(zip_path) as zf:
|
|
154
|
+
if inner_path not in zf.namelist():
|
|
155
|
+
raise RuntimeError(
|
|
156
|
+
f"Expected '{inner_path}' inside {zip_url} but it was not found.\n"
|
|
157
|
+
f"Available entries: {zf.namelist()[:10]}"
|
|
158
|
+
)
|
|
159
|
+
with zf.open(inner_path) as src_f:
|
|
160
|
+
with open(target, "wb") as dst_f:
|
|
161
|
+
dst_f.write(src_f.read())
|
|
162
|
+
print(f"-- snkv: extracted snkv.h {tag} -> {target}")
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
# 2b. Release asset not available — download raw snkv.h from the tag.
|
|
166
|
+
raw_url = (
|
|
167
|
+
f"https://raw.githubusercontent.com/hash-anu/snkv/{tag}/snkv.h"
|
|
168
|
+
)
|
|
169
|
+
print(f"-- snkv: release asset not found; downloading raw {raw_url} ...")
|
|
170
|
+
try:
|
|
171
|
+
urllib.request.urlretrieve(raw_url, target)
|
|
172
|
+
print(f"-- snkv: downloaded snkv.h {tag} from GitHub -> {target}")
|
|
173
|
+
except Exception as exc:
|
|
174
|
+
raise RuntimeError(
|
|
175
|
+
f"Failed to download snkv.h from GitHub ({raw_url}):\n{exc}\n"
|
|
176
|
+
"Either install make (Linux/macOS) or ensure internet access."
|
|
177
|
+
) from exc
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
# Platform-specific linker flags
|
|
182
|
+
# ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
extra_link_args: list = []
|
|
185
|
+
|
|
186
|
+
if sys.platform == "darwin":
|
|
187
|
+
extra_link_args = ["-lpthread", "-lm"]
|
|
188
|
+
|
|
189
|
+
# ---------------------------------------------------------------------------
|
|
190
|
+
# C extension
|
|
191
|
+
# ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
ext = Extension(
|
|
194
|
+
"snkv._snkv",
|
|
195
|
+
sources=["snkv_module.c"],
|
|
196
|
+
include_dirs=[HERE],
|
|
197
|
+
extra_link_args=extra_link_args,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# ---------------------------------------------------------------------------
|
|
201
|
+
# setup()
|
|
202
|
+
# ---------------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
setup(
|
|
205
|
+
ext_modules=[ext],
|
|
206
|
+
cmdclass={"build_ext": BuildExtWithHeader},
|
|
207
|
+
)
|