python-sonic-client
Python client for the Sonic search backend, providing ingest, search, and control channel operations over its lightweight TCP protocol.
Repository Health
Technical Analysis
python-sonic-client is a lightweight Python client for Sonic, the fast, schema-less search backend written in Rust. It implements Sonic’s three-channel protocol — Ingest, Search, and Control — directly over a raw TCP socket, giving Python applications a way to push, query, suggest, and manage search indexes without pulling in a heavier full-text search dependency like Elasticsearch or Solr.
The client wraps connection handling, command formatting, and response parsing in a small set of context-manager-friendly classes (IngestClient, SearchClient, ControlClient), plus an optional connection pool for reusing sockets across requests. It targets teams who already run a Sonic server and want a synchronous, dependency-free way to talk to it from Python code, whether for indexing content on write or querying a search bucket on read.
What You Get
- Three purpose-built clients — IngestClient, SearchClient, and ControlClient — each scoped to one Sonic channel and its allowed commands
- A ConnectionPool that reuses TCP sockets and pings them before reuse, avoiding a fresh handshake per request
- Automatic response parsing (pythonify_result) that converts raw Sonic replies into Python booleans, integers, and lists
- Context-manager support (
with IngestClient(...) as cl:) so connections close cleanly even on error
Common Use Cases
- Indexing rows or documents from a Python app into Sonic as they’re created or updated (push)
- Powering a search-as-you-type or autocomplete field via the suggest command
- Running lightweight full-text search over a bucketed collection without deploying Elasticsearch
- Triggering Sonic housekeeping commands (e.g. consolidate) from a scheduled Python job
Under The Hood
Architecture
The project is a single-file monolith (sonic/client.py, ~740 lines) re-exported through sonic/__init__.py. It layers cleanly from the bottom up: SonicConnection owns the raw socket, command formatting, and response parsing (validating each command against a per-channel ALL_CMDS table); ConnectionPool pools and pings SonicConnection instances for reuse; SonicClient acquires a pooled connection around every call and releases it in a finally block; and the three public classes (IngestClient, SearchClient, ControlClient) mix SonicClient with a shared CommonCommandsMixin for PING/QUIT/HELP. Data flows synchronously from a client method through _execute_command/_execute_command_async down to a blocking socket write and readline(). Because command validation and protocol framing live entirely in SonicConnection, none of the channel classes duplicate that logic — but it also means every channel is tightly coupled to that one class’s behavior.
Tech Stack
The library has zero runtime third-party dependencies — it relies solely on Python’s standard library (socket, re, enum, queue.Queue, itertools). Packaging uses classic setuptools/distutils via setup.py with a single sonic package. The only declared dependency at all is a dev-only pdoc3 entry in the Pipfile, used by the one-line Makefile target to regenerate the docs/api HTML reference. There is no bundler, build step, or CI configuration in the repository.
Code Quality
No automated test suite exists — there is no tests/ directory, no test_*.py files, and no pytest/unittest configuration. The closest thing to tests are three top-level functions (test_ingest, test_search, test_control) at the bottom of client.py that print output when run manually against a live Sonic server; they are not assertions and don’t run in CI. Error handling is explicit and purposeful: raise_for_error() detects Sonic’s ERR response prefix and raises a dedicated SonicServerError, and ChannelError guards against invoking a command not permitted on the current channel. Type hints appear on most constructor signatures but are inconsistent through method bodies. Docstrings are comprehensive (Google-style Arguments/Returns blocks on nearly every method), but there’s no linter, formatter, or CI workflow configured anywhere in the repo.
API Design
The public API mirrors Sonic’s own channel model directly — one class per channel, each restricted at the Python level to its legal command set, which fails fast with ChannelError rather than letting an invalid command reach the server. Ergonomics are solid for the common path: context-manager support, automatic protocol/buffer-size negotiation on connect(), and internal text escaping via quote_text() so callers never think about wire-format quoting. The tradeoff is real boilerplate for multi-channel use — callers instantiate a separate client object per channel against the same server, manage host/port/password by hand, and have no async/await variant despite Sonic’s own pending/event response pattern for search commands.