#!/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}, "mesa_sine_wave": {"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 subprocess\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(" timeout=10,\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, subprocess.TimeoutExpired):\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("try:\n") f.write(" from importlib.metadata import version as _version\n") f.write(" __version__ = _version(__name__)\n") f.write("except Exception:\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(" if len(arr) == 0:\n") f.write(" buf = (_CD * 1)()\n") f.write(" buf[0] = 0.0\n") f.write(" else:\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 AttributeError:\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()