Howler Client Library
howler-client is the Python client for the Howler API. Use it to:
- create, search, update, and delete hits
- manage comments, actions, users, and cases
- turn raw table rows into hit records offline, before you ingest them
It handles authentication, request formatting, retries, and response parsing.
- Package: howler-client on PyPI
- User guide: Howler client documentation
- Contributor guide: Client development
Installation
The client requires Python 3.10 or newer.
pip install howler-client
Connecting to Howler
get_client() returns a Client connected to a Howler server. It checks that the server supports the v1 API when
it connects, so the server must be reachable.
from howler_client import get_client
# API key (recommended): create one in the Howler UI under Settings -> User Security -> API Keys
client = get_client("https://howler.example.com", apikey=("my_username", "my_key_name:my_key_secret"))
# JWT, for example one obtained through an OAuth flow
client = get_client("https://howler.example.com", auth="eyJhbGciOi...")
# Username and password (only if the server allows internal authentication)
client = get_client("https://howler.example.com", auth=("my_username", "my_password"))
Other useful get_client() options:
verify: whether to verify TLS certificates.cert: a client certificate.timeout: the request timeout.retries: how many times to retry a request. The default0retries forever.throw_on_bad_requestandthrow_on_max_retries: whether to raiseClientErroron failures.authenticate: a callback that returns a bearer token.
What the client covers
| Attribute | Purpose |
|---|---|
client.hit |
Create hits (create), fetch one by ID (client.hit(hit_id)), overwrite, update, update_by_query, delete, generate_hash. Comments are under client.hit.comment (add, edit, delete). |
client.search |
Lucene search over hits (hit) and actions (action), plus facet, fields, grouped, histogram, stats, and stream helpers, each with a .hit(...) method. |
client.action |
List, create, and execute saved actions. |
client.user |
Add, delete, list, and update users; whoami. |
client.help |
Server metadata such as classification_definition(). |
client.v2.ingest |
v2 ingestion for the hit and event indexes: create, validate (check records without storing them), overwrite, update_by_query, delete. |
client.v2.search |
Search across several indexes at once (client.v2.search("hit,event", query)), plus count, facet, and explain. |
client.v2.case |
Fetch, create, update, hide, and delete cases; manage case items and correlation rules. |
client.mapping |
map_rows(...), the same as the top-level howler_client.map_rows. See below. |
For example:
results = client.search.hit("howler.status:open", rows=10, sort="event.created desc")
for hit in results["items"]:
print(hit["howler"]["id"], hit["howler"]["analytic"])
client.hit.create({"howler": {"analytic": "My Analytic", "score": 0.5}})
Failed requests raise howler_client.common.utils.ClientError. It has status_code and api_response attributes. See
the user guide for more examples.
Mapping raw table rows to hit records
RowMapping turns raw rows into Howler hit dictionaries using a YAML mapping file. It runs entirely offline:
- It does not need a Howler connection.
- It never stores or ingests records.
- It never sets server-owned IDs such as
howler.id.
To store the results, pass them to client.hit.create or client.v2.ingest.create.
from howler_client import RowMapping, map_rows
mapping = RowMapping.from_file("mapping.yaml")
for hit, error in map_rows(query_result_rows, mapping):
if error is not None:
handle_mapping_error(error) # a MappingError naming the row number and target field
continue
process_hit(hit)
How map_rows behaves:
- It is lazy: it reads each source row only when you ask for the next result.
- For each row, in order, it yields either
(hit, None)or(None, MappingError). - A bad row produces an error result, and the following rows are still processed.
- A bad mapping raises as soon as you call
map_rows, before any row is read. - Exceptions raised by your row iterator are passed through unchanged.
To map a single row, mapping.map_row(row) returns the hit or raises MappingError.
Mapping file format
version: 1
on_null: error # default for every target: error (default) or omit
mappings:
howler.analytic:
value: Directory User Inventory # constant
user.id:
column: user_identifier # copy a column
user.name:
column: account_name
transforms: [trim, lower]
user.email:
column: primary_email
on_null: omit # leave the field out when the value is null
organization.name:
column: profile
path: [organization, name] # read a value nested inside the column
user.roles:
concat: [directory_role_names, elevated_role_names] # join list columns
Each key under mappings is a dotted Howler field name. Each target needs exactly one source:
| Expression | Result |
|---|---|
column: name |
The value of that column. A missing column is a row error. |
column + path: [key, 0, ...] |
A value nested inside the column, found by dictionary keys and list indexes. A missing nested key or index counts as null. |
concat: [a, b, ...] |
Two or more list columns joined into one list. |
value: ... |
A constant, computed once when the mapping is loaded. |
A target can also set:
on_null, which overrides the document-wide setting. Witherror, a null value fails the row. Withomit, the field is left out.transforms, which are applied in order:
| Transform | Effect |
|---|---|
string |
Converts a scalar to a string. Use json for objects and lists. |
integer, number, boolean |
Strict conversions. boolean accepts only booleans or "true"/"false". |
trim, lower, upper |
String clean-up. |
list |
Wraps a scalar in a list. |
json |
Converts the value to compact JSON text with sorted keys. |
{op: timestamp} |
Converts an ISO-8601 string to UTC. Add assume_timezone: UTC to accept values with no timezone. |
Mapping files are parsed with yaml.safe_load. Quote constants that must stay strings, such as "yes", "off", or
"2026-01-01".
Mapping documents must contain JSON-compatible values: object keys must be strings, and YAML-only values such as dates, sets, and binary data (as well as non-finite numbers and cyclic structures) are rejected. The client validates mappings offline with its bundled Draft 2020-12 schema, which is included in the wheel and source distribution.
You can also build a mapping from YAML text with RowMapping.from_yaml(text), or from an already-loaded dictionary
with RowMapping(document). map_rows accepts a RowMapping, a dictionary, or a path to a YAML file. To use YAML
text, go through from_yaml so it isn't mistaken for a path.
examples/row_mapping/example_mapping.yaml is a complete example.
Checking targets against Howler's fields
You can pass Howler's field metadata to check targets before you ingest anything. Use the output of
client.search.fields.hit() or of the MCP get_hit_fields tool:
fields = client.search.fields.hit()
mapping = RowMapping.from_file("mapping.yaml", fields=fields)
With field metadata, RowMapping checks that:
- every target exists and is not deprecated
- every
concattarget is a list field
RowMapping does not validate mapped values against target field types or list shape. Validate records with
client.v2.ingest.validate("hit", hits) or let Howler validate them during ingestion.
Development
The client lives in the client/ folder of the Howler monorepo. It uses its own Poetry
environment, so run every command below from client/.
Setup
cd client
poetry install --with dev,test,types
The repository's pre-commit hooks run Ruff and check commit messages. From the repository root, install them with
pre-commit install.
Code quality
These are the checks CI runs:
poetry run ruff format howler_client --diff
poetry run ruff check howler_client --output-format=github
poetry run type_check # mypy over howler_client
Unit tests
Unit tests in test/unit/ run offline: they need no Howler server, Elasticsearch, or Redis.
poetry run test test/unit # all unit tests
poetry run test test/unit/test_row_mapping.py -k timestamp # focused run; arguments are passed to pytest
Integration tests
Integration tests in test/integration/ call a running Howler API at http://localhost:5000. Set UI_HOST to use a
different address. The API needs Elasticsearch, Redis, and seeded test data. To set this up the same way CI does,
starting from the repository root:
# 1. Prepare the API configuration
sudo mkdir -p /etc/howler/conf /etc/howler/lookups /var/log/howler
sudo chown -R "$USER" /etc/howler /var/log/howler
cp api/build_scripts/classification.yml /etc/howler/conf/classification.yml
cp api/build_scripts/mappings.yml /etc/howler/conf/mappings.yml
cp client/test/config/config.ci.yml /etc/howler/conf/config.yml
# 2. Start Elasticsearch and Redis
docker compose -f client/test/docker-compose.yml up -d
# 3. Install and start the API (leave it running in its own terminal)
cd api
poetry install --with test,dev
poetry run mitre /etc/howler/lookups
poetry run sigma
TESTING=true poetry run flask --app howler.app run --no-reload
# 4. In another terminal, seed users and random data (this creates the admin API key the tests use)
cd api
poetry run python -m howler.odm.random_data
# 5. Run the full client test suite with coverage
cd ../client
poetry run test
poetry run test with no arguments runs everything under test/ with branch coverage. Integration tests need the
running services. To run only part of the suite, pass paths or pytest options.
Releases
Releases are automated with Release Please using Conventional Commits. Use the client scope, for example
feat(client): .... CHANGELOG.md is generated, so don't edit it by hand.
Metadata
Release files for howler-client 3.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| howler_client-3.2.0.tar.gz | 33.1 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| howler_client-3.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 74.5 kB
Release files / howler_client-3.2.0.tar.gz
| Download URL | howler_client-3.2.0.tar.gz |
|---|---|
| Size | 33.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
72521c00f792448ef7c3d9cd2e55a9abb2a8fc82abb07547905ae0d55051e1a5
|
|
BLAKE2b-256 checksum How to use checksums |
33853f6b0a66b61e554a315cd9e0c25fac6732c12370a15cf72474e8fd0f994c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
poetry/2.5.1 CPython/3.12.3 Linux/6.17.0-1022-azure
|
Release files / howler_client-3.2.0-py3-none-any.whl
| Download URL | howler_client-3.2.0-py3-none-any.whl |
|---|---|
| Size | 41.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2a6d7802f9437374b405a95f53102a8ded36fa618099b8ee364dca1b1f7c443f
|
|
BLAKE2b-256 checksum How to use checksums |
d9551904d562bf13ee65a550c79fa3d09a1c009f054144d45484b523a562dbab
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
poetry/2.5.1 CPython/3.12.3 Linux/6.17.0-1022-azure
|
Security for howler-client 3.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.