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 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
+ [![Build](https://github.com/hash-anu/snkv/actions/workflows/c-cpp.yml/badge.svg)](https://github.com/hash-anu/snkv/actions/workflows/c-cpp.yml)
4
+ [![License](https://img.shields.io/badge/license-Apache%202.0-blue)](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
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
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
+ )