summaryrefslogtreecommitdiffstats
path: root/scripts/gen_bindings.py
diff options
context:
space:
mode:
authorEthan Werner <mail@ethanwerner.com>2026-07-01 09:42:39 +0400
committerEthan Werner <mail@ethanwerner.com>2026-07-01 09:42:39 +0400
commit07e18043145f3d91c8ea7ec3ad41b3c905585277 (patch)
tree2bdeae46973f4bdfce263679a4008ac768011f28 /scripts/gen_bindings.py
initial commit: ts time series analysis library v0.1.0
- 354 financial and mathematical time-series indicators - Uniform C11 API with consistent function signatures - 349 passing tests with -Werror - Static and shared library builds - pkg-config integration - Python bindings - Fuzzing and sanitizer support - Security hardening flags
Diffstat (limited to 'scripts/gen_bindings.py')
-rw-r--r--scripts/gen_bindings.py274
1 files changed, 274 insertions, 0 deletions
diff --git a/scripts/gen_bindings.py b/scripts/gen_bindings.py
new file mode 100644
index 0000000..845c8a5
--- /dev/null
+++ b/scripts/gen_bindings.py
@@ -0,0 +1,274 @@
+#!/usr/bin/env python3
+"""Generate python/ts/__init__.py from C source.
+
+Produces a compact module with a single _dispatch() function and a
+FUNC_META lookup table instead of 350+ individual wrappers.
+"""
+
+import importlib.util
+import os
+import py_compile
+import sys
+
+_spec = importlib.util.spec_from_file_location(
+ "ts_parser",
+ os.path.join(os.path.dirname(os.path.abspath(__file__)), "ts_parser.py"),
+)
+if _spec and _spec.loader:
+ _ts_parser = importlib.util.module_from_spec(_spec)
+ _spec.loader.exec_module(_ts_parser)
+ parse_all = _ts_parser.parse_all
+else:
+ print("Error: could not load ts_parser", file=sys.stderr)
+ sys.exit(1)
+
+ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+OUT = os.path.join(ROOT, "python", "ts", "__init__.py")
+
+DEFAULT_PARAMS = {
+ "ichimoku": {"tenkan": 9, "kijun": 26, "senkou": 52},
+}
+
+MIN_PARAM_DEFAULTS = {
+ "hull_ma": {"k": 2},
+ "variable_index_dynamic_average": {"period": 2},
+ "logarithm": {"b": 2.0},
+ "choppiness_index": {"k": 2},
+ "performance_ratio": {"k": 2},
+ "standard_error": {"k": 2},
+}
+
+
+def _py_default(pytype, py_fname, name):
+ if py_fname in DEFAULT_PARAMS.get(name, {}):
+ return DEFAULT_PARAMS[name][py_fname]
+ if py_fname in MIN_PARAM_DEFAULTS.get(name, {}):
+ return MIN_PARAM_DEFAULTS[name][py_fname]
+ return 1 if "uint64" in pytype else 1.0
+
+
+def main():
+ funcs = parse_all(detect_x_access=True)
+ if not funcs:
+ print("Warning: no ts_parameter declarations found.", file=sys.stderr)
+ sys.exit(1)
+
+ os.makedirs(os.path.dirname(OUT), exist_ok=True)
+
+ with open(OUT, "w", encoding="utf-8") as f:
+ # --- Header ---
+ f.write('"""Python bindings for ts (time series) C library.\n')
+ f.write("\nAuto-generated. Uses a single dispatch function backed by\n")
+ f.write('a FUNC_META lookup table.\n"""\n\n')
+
+ f.write("import ctypes\n")
+ f.write("import ctypes.util\n")
+ f.write("import os\n")
+ f.write("import numpy as np\n")
+ f.write("import threading\n\n")
+
+ f.write("_tls = threading.local()\n\n")
+
+ # --- Library loader ---
+ f.write("def _find_lib():\n")
+ f.write(' """Try system path, pkg-config, then build dir."""\n')
+ f.write(" try:\n")
+ f.write(' return ctypes.CDLL("libts.so", ctypes.RTLD_GLOBAL)\n')
+ f.write(" except OSError:\n")
+ f.write(" pass\n")
+ f.write(" try:\n")
+ f.write(" import subprocess\n")
+ f.write(" libdir = subprocess.check_output(\n")
+ f.write(' ["pkg-config", "--variable=libdir", "ts"],\n')
+ f.write(" stderr=subprocess.DEVNULL,\n")
+ f.write(" ).decode().strip()\n")
+ f.write(
+ ' return ctypes.CDLL(os.path.join(libdir, "libts.so"), ctypes.RTLD_GLOBAL)\n'
+ )
+ f.write(" except (OSError, subprocess.CalledProcessError):\n")
+ f.write(" pass\n")
+ f.write(" build_dir = os.path.join(\n")
+ f.write(
+ " os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),\n"
+ )
+ f.write(' "build",\n')
+ f.write(" )\n")
+ f.write(' lib_path = os.path.join(build_dir, "libts.so")\n')
+ f.write(" if os.path.exists(lib_path):\n")
+ f.write(" return ctypes.CDLL(lib_path, ctypes.RTLD_GLOBAL)\n")
+ f.write(
+ ' raise OSError("Cannot find libts.so. Run make install or build from source.")\n\n\n'
+ )
+
+ f.write("__version__ = '0.1.0'\n\n")
+ f.write("_lib = _find_lib()\n\n")
+
+ # --- Helpers ---
+ f.write("_CD = ctypes.c_double\n\n\n")
+
+ f.write("def _cp(arr):\n")
+ f.write(' """Copy numpy array to ctypes buffer (tracked in TLS list)."""\n')
+ f.write(" buf = (_CD * len(arr))(*arr)\n")
+ f.write(" if not hasattr(_tls, 'bufs'):\n")
+ f.write(" _tls.bufs = []\n")
+ f.write(" _tls.bufs.append(buf)\n")
+ f.write(" return buf\n\n\n")
+
+ f.write("def _clr():\n")
+ f.write(' """Clear tracked ctypes buffers."""\n')
+ f.write(" try:\n")
+ f.write(" if hasattr(_tls, 'bufs'):\n")
+ f.write(" _tls.bufs.clear()\n")
+ f.write(" except Exception:\n")
+ f.write(" pass\n\n\n")
+
+ # --- Parameter structs ---
+ f.write("# --- Parameter structs ---\n")
+ seen_structs = set()
+ for fm in sorted(funcs, key=lambda x: x.name):
+ if fm.struct_fields and fm.name not in seen_structs:
+ cls_name = f"ts_{fm.name}_params_t"
+ f.write(f"class {cls_name}(ctypes.Structure):\n")
+ f.write(" _fields_ = [\n")
+ for c_fname, py_fname, pytype in fm.struct_fields:
+ f.write(f' ("{py_fname}", {pytype}),\n')
+ f.write(" ]\n\n")
+ seen_structs.add(fm.name)
+
+ # --- FUNC_META table ---
+ f.write("# --- Function metadata table ---\n")
+ f.write(
+ "# Each entry: (name, x_n, y_n, struct_class_or_None, [(param_name, default), ...])\n"
+ )
+ f.write("FUNC_META = [\n")
+ for fm in sorted(funcs, key=lambda x: x.name):
+ struct_cls = None
+ params = []
+ if fm.struct_fields:
+ struct_cls = f"ts_{fm.name}_params_t"
+ for c_fname, py_fname, pytype in fm.struct_fields:
+ d = _py_default(pytype, py_fname, fm.name)
+ params.append(f" ('{py_fname}', {d!r}),")
+ f.write(
+ f" ({fm.name!r}, {fm.x_n}, {fm.y_n}, {struct_cls}, [{chr(10).join(params)}]),\n"
+ )
+ f.write("]\n\n")
+
+ # --- Single dispatch function ---
+ f.write("# --- Dispatch ---\n")
+ f.write("def _dispatch(meta, *x_arrays):\n")
+ f.write(' """Call a ts indicator via ctypes, given a FUNC_META entry."""\n')
+ f.write(" name, x_n, y_n, struct_cls, param_defaults = meta\n")
+ f.write(" if len(x_arrays) != x_n:\n")
+ f.write(
+ ' raise TypeError(f"Expected {x_n} input arrays, got {len(x_arrays)}")\n'
+ )
+ f.write(" if x_n > 0:\n")
+ f.write(" n = x_arrays[0].shape[0]\n")
+ f.write(" for a in x_arrays:\n")
+ f.write(" if a.shape[0] != n:\n")
+ f.write(
+ ' raise ValueError(f"Array length mismatch: expected {n}, got {a.shape[0]}")\n'
+ )
+ f.write(" else:\n")
+ f.write(" n = 0\n")
+ f.write(" y = [np.zeros(n, dtype=np.float64) for _ in range(y_n)]\n")
+ f.write(" try:\n")
+ f.write(" x_bufs = [_cp(x_arrays[i]) for i in range(x_n)]\n")
+ f.write(
+ " y_ptrs = [y[i].ctypes.data_as(ctypes.POINTER(ctypes.c_double)) for i in range(y_n)]\n"
+ )
+ f.write(" c_func = getattr(_lib, f'ts_{name}_flat')\n")
+ f.write(" if struct_cls is not None:\n")
+ f.write(" _p = struct_cls()\n")
+ f.write(" for k, v in param_defaults:\n")
+ f.write(" setattr(_p, k, v)\n")
+ f.write(
+ " ret = c_func(*(y_ptrs + x_bufs + [ctypes.c_uint64(n), ctypes.byref(_p)]))\n"
+ )
+ f.write(" else:\n")
+ f.write(
+ " ret = c_func(*(y_ptrs + x_bufs + [ctypes.c_uint64(n), None]))\n"
+ )
+ f.write(" if ret != 0:\n")
+ f.write(
+ ' raise RuntimeError(f"ts_{name} failed with return code {ret}")\n'
+ )
+ f.write(" return y[0] if y_n == 1 else tuple(y)\n")
+ f.write(" finally:\n")
+ f.write(" _clr()\n\n\n")
+
+ # --- Helper for param override ---
+ f.write(
+ "def _dispatch_with_params(name, x_n, y_n, struct_cls, _p, x_arrays):\n"
+ )
+ f.write(' """Call indicator with a pre-built params struct."""\n')
+ f.write(" if len(x_arrays) != x_n:\n")
+ f.write(
+ ' raise TypeError(f"Expected {x_n} input arrays, got {len(x_arrays)}")\n'
+ )
+ f.write(" n = x_arrays[0].shape[0] if x_n > 0 else 0\n")
+ f.write(" y = [np.zeros(n, dtype=np.float64) for _ in range(y_n)]\n")
+ f.write(" try:\n")
+ f.write(" x_bufs = [_cp(x_arrays[i]) for i in range(x_n)]\n")
+ f.write(
+ " y_ptrs = [y[i].ctypes.data_as(ctypes.POINTER(ctypes.c_double)) for i in range(y_n)]\n"
+ )
+ f.write(" c_func = getattr(_lib, f'ts_{name}_flat')\n")
+ f.write(
+ " ret = c_func(*(y_ptrs + x_bufs + [ctypes.c_uint64(n), ctypes.byref(_p)]))\n"
+ )
+ f.write(" if ret != 0:\n")
+ f.write(
+ ' raise RuntimeError(f"ts_{name} failed with return code {ret}")\n'
+ )
+ f.write(" return y[0] if y_n == 1 else tuple(y)\n")
+ f.write(" finally:\n")
+ f.write(" _clr()\n\n\n")
+
+ # --- Dynamic attribute access ---
+ f.write("# --- Dynamic attribute access ---\n")
+ f.write("_FUNC_MAP = {m[0]: m for m in FUNC_META}\n\n\n")
+
+ f.write("def __getattr__(name):\n")
+ f.write(' """Return a callable wrapper for indicator `name`."""\n')
+ f.write(" if name.startswith('_'):\n")
+ f.write(
+ ' raise AttributeError(f"module {__name__!r} has no attribute {name!r}")\n'
+ )
+ f.write(" meta = _FUNC_MAP.get(name)\n")
+ f.write(" if meta is None:\n")
+ f.write(
+ ' raise AttributeError(f"{name!r} is not a known ts indicator")\n'
+ )
+ f.write(" name_s, x_n, y_n, struct_cls, param_defaults = meta\n")
+ f.write(" def wrapper(*x_arrays, **kw):\n")
+ f.write(" for a in x_arrays:\n")
+ f.write(" if not isinstance(a, np.ndarray):\n")
+ f.write(
+ ' raise TypeError(f"Expected numpy.ndarray, got {type(a).__name__}")\n'
+ )
+ f.write(" if struct_cls is not None and kw:\n")
+ f.write(" _p = struct_cls()\n")
+ f.write(" for k, v in kw.items():\n")
+ f.write(" if hasattr(_p, k):\n")
+ f.write(" setattr(_p, k, v)\n")
+ f.write(" else:\n")
+ f.write(
+ ' raise TypeError(f"Unknown parameter {k!r} for {name_s}")\n'
+ )
+ f.write(
+ " return _dispatch_with_params(name_s, x_n, y_n, struct_cls, _p, x_arrays)\n"
+ )
+ f.write(" return _dispatch(meta, *x_arrays)\n")
+ f.write(" return wrapper\n\n\n")
+
+ # --- __all__ ---
+ f.write("__all__ = [m[0] for m in FUNC_META]\n")
+
+ print(f"Generated {len(funcs)} entries -> {OUT}")
+ py_compile.compile(OUT, doraise=True)
+
+
+if __name__ == "__main__":
+ main()