Skip to main content

Astro-COLIBRI Python SDK

The astro-colibri distribution provides the astrocolibri Python package, the SDK for Astro-COLIBRI. It has two parts:

What it does You need
Consumer Receives multi-messenger alerts in real time, as JSON or VOEvent/XML, from the Astro-COLIBRI broker Broker credentials from astro-colibri.com/broker
Client Queries the Astro-COLIBRI API: looks up events and sources, and searches for transients by time window or around a sky position Your user ID from astro-colibri.com/account (lookups work without one)

Full documentation, with tutorials, is at astro-colibri.science/sdkdoc.


Installation

pip install astro-colibri

Requires Python 3.9+. confluent-kafka and requests are installed automatically.


Quick start: real-time alerts

1. Get your credentials

Go to astro-colibri.com/broker. The link opens the broker access page directly and prompts you to sign in if you are not already. Request access there to be issued your SCRAM username and password; the same page lets you rotate or revoke them later. A verified Astro-COLIBRI account is required.

2. Subscribe and listen

from astrocolibri import Consumer

with Consumer(
    username="your-username",
    password="your-password",
) as consumer:
    consumer.subscribe(["astrocolibri.all.JSON"])

    # Runs until you stop it, waiting for the next alert.
    for alert in consumer.consume():
        data = alert.value()
        print(f"Alert received: {data['id']} — RA={data['ra']}, Dec={data['dec']}")

That loop is the whole listener: consume() never ends on its own, so the script keeps waiting for the next alert instead of exiting after the current backlog. Stop it with Ctrl-C.


Quick start: querying the API

1. Get your user ID

Open astro-colibri.com/account (the link asks you to sign in if needed) and copy your user ID. It identifies your account to the API, which uses it for the per-account search quota and for anything specific to you, such as your watchlist. Treat it like a personal key: keep it out of shared notebooks and published code.

Event and source lookups work without a user ID; searches need one.

2. Configure it

Export it once in your shell or deployment environment:

export ASTROCOLIBRI_UID="your-user-id"
from astrocolibri import Client

client = Client()              # reads ASTROCOLIBRI_UID

or pass it explicitly, for example from a variable your script already holds. The explicit argument wins:

client = Client(uid=my_uid)

The SDK keeps the user ID in memory only. It writes no configuration file, and leaves the ID out of repr(client) and every error message.

from astrocolibri import Client

with Client() as client:
    results = client.latest_transients(
        start="2026-09-01T00:00:00Z",
        end="2026-09-02T00:00:00Z",
    )
    for event in results["voevents"]:
        print(
            f"Name: {event.get('source_name', 'N/A')}, "
            f"Type: {event.get('type', 'N/A')}, "
            f"Detection date/time: {event.get('time', 'N/A')}, "
            f"RA: {event.get('ra', 'N/A')}, "
            f"Dec: {event.get('dec', 'N/A')}, "
            f"Trigger ID: {event.get('trigger_id', 'N/A')}"
        )

Alerts arrive decoded

Kafka carries bytes, but you don't need to handle them: consume() yields Alert objects whose value() is the payload already decoded:

Topic alert.value() returns
astrocolibri.*.JSON, astrocolibri.heartbeat dict — the parsed alert
astrocolibri.*.VOEvent str — the XML document
for alert in consumer.consume():
    if alert.format == "json":
        data = alert.value()            # dict
        print(data["id"], data["type"], data["ra"], data["dec"])
    else:
        print(alert.value())            # XML, as text

Everything else about the record stays available:

alert.value() Decoded payload — dict or XML str
alert.text() Payload as text, whatever the format
alert.json() Payload parsed as JSON, whatever the topic
alert.raw bytes — payload exactly as published
alert.format "json" or "voevent"
alert.key() str — the event id (all updates of an event share it)
alert.headers() dict[str, str] — event_id, dedupe_key, event_version, update_type, important, changed_fields
alert.topic(), .partition(), .offset(), .timestamp() Kafka coordinates
alert.message The underlying confluent_kafka.Message

A payload that cannot be decoded raises AstrocolibriDecodeError — and only when you read it, so one bad record never takes down a running listener:

from astrocolibri import AstrocolibriDecodeError

for alert in consumer.consume():
    try:
        data = alert.value()
    except AstrocolibriDecodeError as e:
        print(f"skipping: {e}")
        continue
    handle(data)

To handle the raw bytes yourself, turn decoding off — consume() then yields the confluent_kafka.Message unchanged:

consumer = Consumer(username="...", password="...", decode=False)

Example scripts

Each script is a complete, runnable program:

Script Shows Needs
example.py A broker listener that subscribes to every topic and prints alerts as they arrive; stop it with Ctrl-C Broker credentials
examples/get_event.py Looking up one event by trigger ID or name Nothing
examples/resolve_source.py Listing every localization of a source, then fetching the best one Nothing
examples/latest_transients.py Searching a time window, with user ID and quota errors handled User ID
examples/cone_search.py Searching around a position written in sexagesimal notation User ID
examples/contour_search.py Searching inside a gravitational-wave localization contour User ID
python example.py
python examples/resolve_source.py "GRB 190829A"

Available topics

Topic Description
astrocolibri.all.JSON Every alert, Astro-Colibri JSON format
astrocolibri.all.VOEvent Every alert, VOEvent/XML format
astrocolibri.important.JSON Important alerts, Astro-Colibri JSON format
astrocolibri.important.VOEvent Important alerts, VOEvent/XML format
astrocolibri.heartbeat Pipeline liveness message, JSON format

Querying the API

Events and sources

get_event() accepts a trigger ID, a source name or a discoverer designation, and tolerates spacing differences ("GRB190829A" finds "GRB 190829A"):

event = client.get_event("GRB 190829A")
print(event["trigger_id"], event["ra"], event["dec"], event["err"])

A name can belong to several events: GRB 190829A was localized by both Fermi/GBM and Swift/XRT. A lookup by name returns only one of them, not necessarily the best. resolve_source() lists them all, best-localized first:

for summary in client.resolve_source("GRB 190829A"):
    print(summary["trigger_id"], summary.get("observatory"), summary["err"])
# 922968 swift 0.0016
# 588801358 fermi 2.21

best = client.get_event("922968")

resolve_source() only covers transient events. Catalog sources (TeVCat, 4FGL, X-ray binaries), bright stars and objects known only to SIMBAD give an empty list; get_source_summary() returns their position instead, or None for a name nobody knows. A SIMBAD lookup can be slow, so allow it time:

summary = client.get_source_summary("Crab", timeout=300)

Timestamps in source summaries are in milliseconds since the Unix epoch.

Several events in one request, in the order given:

events = client.get_events(["922968", "588801358", "S230518h"], missing="skip")

Heavy fields are left out unless you ask for them; otherwise gw_contours and archive come back as {"Parameter": "Not requested"}:

event = client.get_event("S230518h", optional_parameters=["gw_contours"])

Searches

latest_transients() searches a time window; cone_search() also restricts it to a region of the sky. Both return the API's JSON as a dict. The transients are in "voevents", one dict per event, and the API documentation lists every event parameter. A cone search adds catalog matches in "sources", "Xsources" and "icecat", which are not restricted to the time window.

Times must carry a timezone: an aware datetime, or an ISO 8601 string ending in Z or an offset. A time without one is rejected rather than guessed.

Angles accept decimal degrees or sexagesimal strings, and the unit is read from the notation, never from the size of the number:

You write Read as
83.6331, "83.6331" degrees
"05h34m31.94s" hours, minutes, seconds
"+22d00m52.2s", "22°00′52.2″" degrees, arcminutes, arcseconds
ra="05:34:31.94" colons for right ascension: hours
dec="+22:00:52.2", radius="00:30:00" colons for declination and radius: degrees
radius="30′" or "30'" arcminutes (0.5°)
results = client.cone_search(
    ra="05h34m31.94s",
    dec="+22d00m52.2s",
    radius=2,
    start="2026-08-01T00:00:00Z",
    end="2026-09-01T00:00:00Z",
)

Gravitational waves, Fermi/GBM and IPN bursts, IceCube neutrinos and MAXI transients are localized to regions far from circular. Search inside the event's 90% localization contour instead of a circle, optionally widened by a margin in degrees. If no contour is stored, the circle is searched, and "search_region" says which one was used:

results = client.cone_search(
    ra=100.72,
    dec=-22.10,
    radius=15,
    start="2023-05-18T12:59:08Z",
    end="2023-05-25T12:59:08Z",
    contour_trigger_id="S230518h",
    contour_margin=1.0,
)
print(results["search_region"])   # "contour_90" or "circle"

Filters

A search returns every event your account may access unless you narrow it. Pass an API filter dictionary, the same structure the Astro-COLIBRI app builds. The filter notebook walks through that structure interactively, and the photometry sub-filter notebook shows how to select fast-rising or fast-fading transients from their light curves. Optical transients only:

optical_transients = {
    "type": "FieldSpecification",
    "operation": "==",
    "field": "type",
    "value": "ot",
    "typeField": "string",
}
results = client.latest_transients(start=start, end=end, event_filter=optical_transients)

or apply the filters you saved in the app:

results = client.latest_transients(start=start, end=end, use_saved_filters=True)

Your quota

Searches count against a daily per-account quota (100 requests at the time of writing); lookups do not. An identical search repeated within the same half hour is normally answered from the API's cache without counting again, which is why the SDK sends an explicit match-everything filter by default. Two things defeat the cache: use_saved_filters=True, which always runs a fresh search, and a window that moves between calls, such as one ending at datetime.now().

The SDK never retries a request on its own. When the quota runs out you get AstrocolibriRateLimitError, whose reset_time says when it renews.

Scientific formats

voevent = client.get_event_voevent("GRB 190829A")            # VOEvent XML
votable = client.cone_search(..., return_format="votable")   # VOTable XML

Errors

Exception When
AstrocolibriConfigError An argument is invalid. Raised before anything is sent, so it costs no quota
AstrocolibriAuthError A search without a user ID, or with one the API does not recognize
AstrocolibriNotFoundError No event matches the identifier
AstrocolibriRateLimitError The daily search quota is used up; see reset_time
AstrocolibriAPIError The API rejected or failed the request; see status_code and message
AstrocolibriTransportError The API could not be reached, or the request timed out

All of them derive from AstrocolibriError.


API reference

Consumer

Consumer(
    username: str,
    password: str,
    *,
    broker_url: str | None = None,
    group_id: str | None = None,
    start_at: str = "earliest",       # "earliest" | "latest"
    security_protocol: str = "SASL_SSL",
    decode: bool = True,               # False yields raw confluent_kafka.Message
    poll_interval: float = 1.0,        # seconds per internal poll while listening
    config: dict | None = None,        # advanced confluent-kafka options
)

consumer.subscribe(topics, *, on_assign=None, on_revoke=None, on_lost=None)

Subscribe to a list of topics.

consumer.consume(num_messages=1, timeout=-1)

Generator of Alert objects.

  • timeout=-1 (default): listens continuously. The generator never ends; it waits for the next alert. Internally it polls in poll_interval slices, so Ctrl-C stays responsive.
  • timeout=N (seconds): ends after N seconds without a new alert, so your program can do something else between bursts.
# Continuous listener — this is the normal case
for alert in consumer.consume():
    handle(alert.value())

# Batch mode (lets you do other work between bursts)
while True:
    for alert in consumer.consume(timeout=5.0):
        handle(alert.value())
    check_app_state()

Transient Kafka conditions — end of partition, a dropped broker connection, a group rebalance — are logged through the standard logging module and skipped, because the client recovers from them on its own. Only an unrecoverable error raises AstrocolibriKafkaError. To see those warnings:

import logging
logging.basicConfig(level=logging.INFO)

consumer.close()

Cleanly closes the connection (called automatically by the context manager).

Client

Client(
    uid: str | None = None,          # default: the ASTROCOLIBRI_UID variable
    *,
    timeout: float = 30.0,           # seconds per request
    session: requests.Session | None = None,
)

Client talks to the public Astro-COLIBRI API at https://astro-colibri.science.

Method Returns Quota
get_event(identifier, *, optional_parameters=(), simbad=False, include_custom_events=False) event dict free
get_events(identifiers, *, optional_parameters=(), simbad=False, include_custom_events=False, missing="raise") list of event dicts free
get_event_voevent(identifier, *, optional_parameters=()) VOEvent XML str free
resolve_source(name) list of localization summaries, best first free
get_source_summary(name) summary dict, or None free
latest_transients(*, start, end, event_filter=None, use_saved_filters=False, optional_parameters=(), include_watchlist=False, return_format="json") results dict, or str for "votable" and "url" counts
cone_search(*, ra, dec, radius, start, end, contour_trigger_id=None, contour_margin=None, event_filter=None, use_saved_filters=False, optional_parameters=(), include_watchlist=False, return_format="json") results dict, or str for "votable" and "url" counts
close()

Every method also accepts timeout=. Client is a context manager. Like the requests.Session it holds, it is not thread-safe: use one per thread.


Your read position

Your read position (offset) is always persisted. With no group_id, the client joins the consumer group <your-username>.default, so restarting a program resumes exactly where it left off: nothing is re-read, nothing is missed.

start_at only applies the first time a given consumer group connects. On every later run the stored offset wins, so changing start_at on an existing group has no effect. To deliberately re-read the retention window, use a group_id you have never used before.

The client automatically prefixes group_id with your Kafka username to satisfy the per-user ACL, so group_id="my-program-v1" becomes the Kafka group your-username.my-program-v1.

consumer = Consumer(
    username="your-username",
    password="your-password",
    group_id="my-program-v1",   # its own independent read position
    start_at="latest",          # only applies on this group's very first run
)

Running several scripts with the same credentials

One set of credentials can drive as many scripts as you like, but give each script its own group_id. Consumers that share a group are treated by Kafka as one logical reader and have the partitions divided between them, so each script would receive only a slice of the stream rather than every alert.

# ingest.py
consumer = Consumer(username="alice", password="...", group_id="ingest")

# alerting.py
consumer = Consumer(username="alice", password="...", group_id="alerting")

Each group keeps its own independent read position, so the two scripts can run at different speeds, restart independently, and both still see the full alert stream.

Leaving group_id unset in more than one script is the case to avoid: they all land in <your-username>.default and silently share the stream between them.

Running the same script as several replicas is the one case where sharing a group_id is what you want: that is how you spread the load, and Kafka rebalances the partitions across the replicas automatically.


Testing locally against your own broker

If you're running the Astro-Colibri broker stack locally (see the broker's QUICKSTART.md), point the client at it directly:

consumer = Consumer(
    username="alice",
    password="alice-strong-password",
    broker_url="localhost:9092",
    security_protocol="SASL_PLAINTEXT",  # local trusted broker only
)

Install the package in editable mode from the repository root for development:

cd Colibri_v2/colibri_client
pip install -e ".[dev]"
pytest tests/ -v

Citation

Please cite the software release using CITATION.cff and the Astro-COLIBRI platform papers:

  • Reichherzer et al. (2023), Astro-COLIBRI 2 - An Advanced Platform for Real-Time Multi-Messenger Discoveries, Galaxies 11, 22, doi:10.3390/galaxies11010022.
  • Reichherzer et al. (2021), Astro-COLIBRI - The COincidence LIBrary for Real-time Inquiry for Multimessenger Astrophysics, ApJS 256, 5, doi:10.3847/1538-4365/ac1517.

License

This source-available software is licensed under the PolyForm Noncommercial License 1.0.0. It may be used, modified, and redistributed for permitted noncommercial purposes, including use by educational institutions and public research organizations.

Commercial use requires a separate written license. Contact Astro-COLIBRI (Fabian Schüssler) and see COMMERCIAL-LICENSE.md.

Use of Astro-COLIBRI hosted services, including the Kafka broker and its data, is governed separately by the Astro-COLIBRI Terms of Service.

Metadata

Release files for astro-colibri 1.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for astro-colibri 1.1.0
File Size Uploaded
astro_colibri-1.1.0.tar.gz 62.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for astro-colibri 1.1.0
File Interpreter ABI Platform
astro_colibri-1.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 101.8 kB

Release files / astro_colibri-1.1.0.tar.gz

Download URL astro_colibri-1.1.0.tar.gz
Size 62.8 kB
Tags Source
SHA-256 checksum
How to use checksums
19a44297f2eea3f99faea2e602590e2f98ee9af7920034e2c6d0a40d5cf18b47
BLAKE2b-256 checksum
How to use checksums
f7382fb91552ff310505b5f972a281ff888c44370f6459c27dfdbeee34a91bcb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.

Transparency log

Release files / astro_colibri-1.1.0-py3-none-any.whl

Download URL astro_colibri-1.1.0-py3-none-any.whl
Size 39.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
9df77d4fdd0dbcad79916427cc0b7b074dcecf96dd882b3b30388e3a0062f382
BLAKE2b-256 checksum
How to use checksums
a6c711f9a4826b87ce732d592452bee845732c408268260dfd5c796dda76b887
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.

Transparency log

Release history Release notifications | RSS feed

1.2.0

2 release files

This release

1.1.0 This release

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page