Skip to main content

SightingDB Python Client

SightingDB counts things: how many times a value was seen, when it was first seen, when it was last seen, and how many namespaces hold it. This library is its REST API as Python objects, for both synchronous and asyncio code.

pip install sightingdb-client

The distribution is sightingdb-client; the module it installs is sightingdb, so import sightingdb is what you write. Requires Python 3.10+ and talks to SightingDB 0.5 or later.

Writing

import sightingdb

with sightingdb.SightingDB("https://localhost:9999", apikey="changeme") as db:
    db.write("feeds/misp/ips", "127.0.0.1")          # returns the new count
    db.write("feeds/misp/ips", "127.0.0.1", ttl=86400)   # expires a day after last seen
    db.write("feeds/misp/ips", "10.0.0.1", timestamp=1587364370)  # seen in the past

Make the client once and keep it: it holds a connection pool, and a client per call re-runs the TLS handshake every time. timestamp takes Unix seconds or a datetime; ttl takes seconds or a timedelta.

Many values go in one request:

result = db.write_many([
    ("feeds/misp/ips", "8.8.8.8"),
    sightingdb.Sighting("feeds/misp/ips", "1.1.1.1", ttl=86400),
    {"namespace": "feeds/misp/domains", "value": "example.com"},
])
print(result.written)

Anything the server refuses raises BulkWriteError, naming the items that did not land — pass strict=False to get them in result.errors instead.

For a feed you are streaming rather than holding in memory, batch it:

with db.batch(chunk_size=1000) as batch:      # one request per 1000 sightings
    for value in feed:
        batch.add("feeds/misp/ips", value)
print(batch.result.written)

Reading

attribute = db.read("feeds/misp/ips", "127.0.0.1")
attribute.count          # 2
attribute.consensus      # how many namespaces hold this value
attribute.first_seen     # 1566624658
attribute.first_seen_at  # datetime(2019, 8, 24, 5, 30, 58, tzinfo=timezone.utc)
attribute.expires_at     # None when the value has no TTL

read raises NotFoundError for a value that was never seen. When you are enriching a list, ask for them together instead — there a miss is an answer, not an error:

for result in db.read_many([("feeds/misp/ips", v) for v in indicators]):
    if result.found:
        print(result.value, result.count, result.consensus)
    else:
        print(result.value, result.error)   # "Value not found" / "Path not found"

Reading is itself recorded, as a "shadow sighting" under _shadow/, so that you can see how often a value was searched for. Pass shadow=False (or Sighting(..., noshadow=True) in a bulk read) to look without leaving a trace.

Other reads:

db.read("feeds/misp/ips", "127.0.0.1", stats=True).stats_by_hour  # hourly histogram
db.list_values("feeds/misp/ips")     # every value in a namespace
db.exists("feeds/misp/ips", "8.8.8.8")
db.delete("feeds/misp/ips")          # the whole namespace, with care
db.info()                            # what the server says it is

Namespaces are normalized for you: feeds/ips, /feeds/ips and /feeds/ips/ are the same namespace here. To the server they are not — a trailing slash is a different namespace with its own counts.

asyncio

AsyncSightingDB is the same API, awaited:

import asyncio, sightingdb

async def main():
    async with sightingdb.AsyncSightingDB(apikey="changeme") as db:
        async with db.batch(chunk_size=1000) as batch:
            for value in feed:
                await batch.add("feeds/misp/ips", value)

        results = await db.read_many([("feeds/misp/ips", v) for v in indicators])
        counts = await asyncio.gather(*(db.write("feeds/live", v) for v in values))

asyncio.run(main())

Configuration

Settings are resolved per field, most specific first:

  1. what you pass to the client,
  2. the environment: SIGHTINGDB_URL (or SIGHTINGDB_HOST, SIGHTINGDB_PORT, SIGHTINGDB_SSL), SIGHTINGDB_APIKEY, SIGHTINGDB_VERIFY, SIGHTINGDB_TIMEOUT, SIGHTINGDB_MAX_RETRIES,
  3. a TOML file — $SIGHTINGDB_CONFIG, else ~/.sightingdb/client.toml.
# ~/.sightingdb/client.toml
[client]
url = "https://sightingdb.example.com:9999"
apikey = "changeme"
verify = "/etc/ssl/sightingdb-ca.pem"
timeout = 10.0
max_retries = 2

So a config file can hold the URL while the key comes from the environment, without either repeating the other. use_env=False and use_file=False cut a client off from both, which is what you want in tests.

TLS

verify defaults to True. SightingDB generates a self-signed certificate on first run, which nothing will verify, so either point verify at that certificate — verify="/Users/you/.sightingdb/ssl/cert.pem" — or, knowing what it costs, pass verify=False.

Errors

Everything raised derives from SightingDBError.

Exception When
ConfigurationError The settings do not make sense.
TransportError, TimeoutError No answer arrived: DNS, TCP, TLS, timeout.
ProtocolError The answer was not what the API promises — usually a wrong URL, or a server too old for an endpoint.
BadRequestError (400) The server could not make sense of the request.
AuthenticationError (401) No API key was sent.
PermissionDeniedError (403) The key is unknown, or not granted this namespace.
NotFoundError (404) No such namespace, or no such value. Carries .namespace and .value.
ServerError (5xx) The server failed to handle the request.
BulkWriteError A bulk write landed only in part. Carries the full .result.

Requests that fail in a way a retry could fix are retried with backoff. Writes are only retried when the connection was never established: SightingDB counts, and a retried write that did land would count twice.

Development

pip install -e '.[dev]'
pytest                     # unit tests, no server needed

SIGHTINGDB_TEST_URL=https://localhost:9999 \
SIGHTINGDB_TEST_APIKEY=changeme \
SIGHTINGDB_TEST_VERIFY=false \
pytest -m live             # against a real server

samples/everything.py walks through the whole API, and samples/async_feed.py shows the asyncio client ingesting and enriching a feed.

CI runs the unit tests on 3.10 through 3.14, builds the distribution, and runs the live tests against a real SightingDB downloaded from the server's own releases — because every bug this client has had was a disagreement with the server that mocks could not have caught.

Releasing

Tag it:

git tag python-v1.0.1 && git push origin python-v1.0.1

.github/workflows/release.yml does the rest: if __version__.py disagrees with the tag it bumps, commits and moves the tag onto that commit, then tests, builds, publishes to PyPI over Trusted Publishing, and cuts a GitHub Release with the sdist and wheel attached. python-v1.0.1 and v1.0.1 are both accepted, and 1.0.1rc1 publishes as a prerelease.

Upgrading from 0.0.x

The 0.0.x API is gone. It predates several versions of the server, reported failures as successes, and its auth object called endpoints that no longer exist — API keys now live in the server's acl.toml.

0.0.x 1.0
sightingdb.connection(host=..., apikey=...) sightingdb.SightingDB(url, apikey=...)
writer.add(...) then writer.commit() db.write_many([...]) or with db.batch() as b: b.add(...)
writer.write_one(ns, v) db.write(ns, v)
reader.add(...) then reader.fetch() db.read_many([...])
reader.read_one(ns, v) db.read(ns, v)
reader.read_one_with_stats(ns, v) db.read(ns, v, stats=True)
delete(con).delete(ns) db.delete(ns)
con.disable_ssl_warnings() nothing to disable; httpx does not warn
sightingdb.auth(con) removed — keys are configured server-side

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sightingdb_client-0.0.1.tar.gz (33.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sightingdb_client-0.0.1-py3-none-any.whl (26.8 kB view details)

Uploaded Python 3

File details

Details for the file sightingdb_client-0.0.1.tar.gz.

File metadata

  • Download URL: sightingdb_client-0.0.1.tar.gz
  • Upload date:
  • Size: 33.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for sightingdb_client-0.0.1.tar.gz
Algorithm Hash digest
SHA256 3e30b10d13e0ada98e0bfc5f75d127273ae71dd72a722f5e420abf2c4289fdb6
MD5 2ca50e217da56a35fbf338c5ba0c01da
BLAKE2b-256 8c96ab7805f55f08131a8e3897b6d4282e4e61dc896473dbcdebafb16ab4ae5c

See more details on using hashes here.

Provenance

The following attestation bundles were made for sightingdb_client-0.0.1.tar.gz:

Publisher: release.yml on stricaud/sightingdb-client

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file sightingdb_client-0.0.1-py3-none-any.whl.

File metadata

File hashes

Hashes for sightingdb_client-0.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 693e16fd6549782ab48c7c15e7bd6e2f52a575b5c46e740c01f04c62d3cbbb92
MD5 c7f3e27a3491a6e5220ae88be28f9d32
BLAKE2b-256 1e14677e7cf23f373f0b18f43b9879262694a2a20fe0f6552a82aedffe8b7556

See more details on using hashes here.

Provenance

The following attestation bundles were made for sightingdb_client-0.0.1-py3-none-any.whl:

Publisher: release.yml on stricaud/sightingdb-client

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page