FermiHDI SDK — Python
BETA. Builds and links against a real SDK on every change, and the module imports. It has not been exercised against a live cluster as heavily as the Go binding.
pybind11 bindings over the C++ SDK. Read the SDK overview first — especially §1, the control plane / data plane split.
Install
The module is not on PyPI: it is a compiled extension that links the SDK, and
it ships in the SDK package image alongside the library it needs. Copy the
package into your image and put its Python directory on PYTHONPATH:
COPY --from=cr.fermihdi.io/hd/sdk:<version> /opt/fermihdi-sdk /opt/fermihdi-sdk
ENV PYTHONPATH=/opt/fermihdi-sdk/python LD_LIBRARY_PATH=/opt/fermihdi-sdk/lib
The directory holds fermihdi_sdk.cpython-*.so, built for the package's
CPU variant and Python version (PACKAGE.txt records both). Match the
variant to the host the client runs on: an avx512 module raises
"Illegal instruction" on a machine without AVX-512.
You will need the TLS tunable
Python dlopens the extension, and the SDK carries the HD engine runtime's initial-exec
thread-local storage. Without surplus static TLS the import fails:
ImportError: ... cannot allocate memory in static TLS block
GLIBC_TUNABLES=glibc.rtld.optional_static_tls=1048576 python3 your_app.py
This is a property of the SDK, not of this binding — see §5.3.
API
FermiHDIQueryClient
FermiHDIQueryClient(proxy_url="", data_plane_iface="", ctrl_plane_iface="")
stop()
register_with_dms(dms_url, node_name, override_key_pem="") -> RegistrationResult
add_control_header(header)
set_scc_targets(targets) # [SccAddress]
load_dataset_schema(schema_json)
generate_batch_ids_time(from_sec, to_sec) -> [str]
generate_hd_query(hd_query) -> str
get_found_batch_ids() -> [str]
get_missing_batch_ids() -> [str]
prepare_query(query_id) # arm the listener BEFORE you dispatch
set_expected_batches(batch_ids) # the proxy's found_batches, AFTER
set_filter_verdict(would_exclude_batches, bloomrf_mode)
set_filter_verdict_json(verdict_json) # the proxy's result event, verbatim
get_filter_false_negatives() -> [str] # must be empty
get_bloomrf_mode() -> str
set_local_aggregations(agg_config)
execute_dataset(sql_or_json) -> ArrowTable # the raw record table, untouched
execute_dataset_count(sql_or_json) -> int
execute_to_sql(fermihdi_json, duckdb_sql) -> ArrowTable
execute_sql_on_empty(duckdb_sql) -> ArrowTable # the SQL over zero records
apply_sql(table, duckdb_sql) -> ArrowTable # SQL over a table you hold
ArrowTable # what every execute_* returns
num_rows() num_columns() schema()
to_ipc() -> bytes # pyarrow.ipc.open_stream(b).read_all()
table_from_ipc(bytes) -> ArrowTable # e.g. from pyarrow: sink.getvalue().to_pybytes()
execute_vector_search(sql, vector_column, target, top_k) -> pyarrow.Table
execute_graph(sql, src_col, dst_col)
Supports the context-manager protocol (__enter__ / __exit__), which is the
recommended way to use it — stop() is then guaranteed.
FermiHDIIngestClient
FermiHDIIngestClient(data_plane_iface="", ctrl_plane_iface="")
register_with_dms(...) discover_scc_targets(proxy_url) -> bool
load_dataset_schema(json) set_scc_targets(targets)
generate_batch_id_string(s) generate_batch_id_number(n)
generate_batch_id_time(sec) convert_record_to_binary(record)
append_record_to_arrow(record) flush_arrow_batch(batch_id)
stream_insert(batch_id, batch, broadcast=False) -> bool
send_eob(batch_id) send_bbn(batch_id)
Types
Filter(field_name, value, op) # op: FilterOp.EQUALITY | NOT_EQUAL | GREATER | LESS
SubQuery(filters=[...])
SccAddress(ip, port)
HDQuery(query_id, batch_ids, sub_queries, targets,
query_qos=1, acknowledge_full_dataset_scan=False)
Worked example
import os, json, secrets
import fermihdi_sdk as fsdk
schema = json.dumps({"datasets": [{"record_len": 89, "time_index_interval": 60}]})
with fsdk.FermiHDIQueryClient("https://proxy.internal:443") as client:
client.load_dataset_schema(schema)
batch_ids = client.generate_batch_ids_time(1788500000, 1788503600)
hq = fsdk.HDQuery()
hq.query_id = secrets.randbelow(2**32) # distinct per query
hq.batch_ids = batch_ids
hq.targets = [fsdk.SccAddress("10.20.0.14", 6987)]
sq = fsdk.SubQuery()
# An address is its integer: 10.0.0.1 is 167772161. Convert before the SDK sees it.
sq.filters = [fsdk.Filter("ip_src", "167772161", fsdk.FilterOp.EQUALITY)]
hq.sub_queries = [sq]
body = client.generate_hd_query(hq)
if not body:
raise RuntimeError("refused: open query without acknowledge_full_dataset_scan")
table = client.execute_to_sql(body, "SELECT ip_src, bytes FROM ipflows LIMIT 100")
print(table.num_rows, "rows")
Use cases
Notebook analysis. The context manager plus execute_to_sql returning a
pyarrow.Table drops straight into pandas:
df = table.to_pandas()
Vector search over stored embeddings.
hits = client.execute_vector_search(body, "embedding", target_vector, top_k=20)
A counting query, when you want cardinality without materialising rows:
n = client.execute_dataset_count(body)
Ingest from a Python producer. Build records, append to an Arrow batch, flush under a batch ID, then mark the batch complete:
ing = fsdk.FermiHDIIngestClient()
ing.load_dataset_schema(schema)
ing.discover_scc_targets("https://proxy.internal:443")
bid = ing.generate_batch_id_time(1788500000)
for rec in records:
ing.append_record_to_arrow(rec)
ing.flush_arrow_batch(bid)
ing.send_eob(bid)
Gotchas
generate_hd_queryreturning""is the full-dataset-scan guard, not a bug. Setacknowledge_full_dataset_scandeliberately or narrow the query.- Advertise a reachable address.
SccAddressintargetsis where storage nodes send records. Wrong address, zero rows, no error. - Two clients cannot both bind 6987 on one host.
- This binding does not submit the query for you. If you dispatch it
yourself you own the ending — relay
completeorcancel— and you own the filter verdict: pass the result event'swould_exclude_batchesandbloomrf_modethroughset_filter_verdict, or the false-negative check never runs.