Skip to main content

Python client for UmaDB event store

Project description

UmaDB Python Client

Official Python client for UmaDB using Rust-powered bindings via PyO3.

This package is adapted into the Python eventsourcing library via the eventsourcing-umadb package.

Installation

From PyPI

To install with pip, create and activate a virtual environment, then:

pip install umadb

Connecting to UmaDB

Use the Client class as the main entry point for connecting to an UmaDB server.

class Client(
    url: str,
    ca_path: str | None = None,
    api_key: str | None = None,
    batch_size: int | None = None,
):
    ...

Parameters

Name Type Description
url str Required connection string. If the argument starts with https or grpcs, a secure TLS channel is created; otherwise an insecure channel is used.
ca_path str|None Optional path to a PEM-encoded root/CA certificate for TLS connections (useful for self-signed servers).
api_key str|None Optional API key used for authenticating with the server.
batch_size int|None Optional hint for how many events to buffer per batch when reading. The server may cap this; a sensible default is used if unset..

Connection Examples

from umadb import Client

# Insecure (no TLS)
client = Client("http://localhost:50051")

# Secure with TLS (system CAs)
client = Client("https://example.com:50051")

# Secure with TLS using self-signed CA
client = Client(
    url="https://localhost:50051",
    ca_path="server.pem",
)

# TLS + API key
client = Client(
    url="https://example.com:50051",
    ca_path="server.pem",
    api_key="umadb:example-api-key-4f7c2b1d9e5f4a038c1a",
)

# TLS + API key + batch size hint
client = Client(
    url="https://example.com:50051",
    ca_path="server.pem",
    api_key="umadb:example-api-key-4f7c2b1d9e5f4a038c1a",
    batch_size=1000,
)

Appending Events

The Client.append() method writes new events to an UmaDB server.

def append(
    events: list[Event],
    condition: AppendCondition | None = None,
    tracking_info: TrackingInfo | None = None,
) -> int:
    ...

The Client.append() method can be used to append new Event instances to UmaDB atomically, with an optional append condition, and optional tracking information. Events are written in order.

Conditional appends with event UUIDs are idempotent. The server does not enforce uniqueness of events IDs.

Parameters

Name Type Description
events list[Event] The list of events to append. Each includes an event type, tags, and data payload.
condition AppendCondition|None Optional append condition to ensure no conflicting writes occur.
tracking_info TrackingInfo|None Optional tracking information – for event-processing components only.

Return Value

Returns the sequence number (int) of the last successfully appended event from this operation. This value can be used to wait for downstream event-processing components in a CQRS system to become up-to-date.

Example

import uuid

from umadb import AppendCondition, Client, Event, IntegrityError, Query, QueryItem

client = Client("http://localhost:50051")

# Define a consistency boundary (same query you use while reading)
cb = Query(items=[QueryItem(types=["example"], tags=["tag1", "tag2"])])

# Read to build decision model
read_resp = client.read(query=cb)
for r in read_resp:
    print(f"Existing event at {r.position}: {r.event}")

last_known = read_resp.head()
print("Last known position:", last_known)

# Produce a new event with a UUID (for idempotent retries)
ev = Event(
    event_type="example",
    tags=["tag1", "tag2"],
    data=b"Hello, world!",
    uuid=str(uuid.uuid4()),
)

# Append with an optimistic condition: fail if conflicting events exist after last_known
cond = AppendCondition(fail_if_events_match=cb, after=last_known)
position1 = client.append([ev], condition=cond)
print("Appended at:", position1)

# Conflicting append should raise an error (e.g. ValueError)
try:
    client.append(
        [
            Event(
                event_type="example",
                tags=["tag1", "tag2"],
                data=b"Hello, world!",
                uuid=str(uuid.uuid4()),
            )
        ],
        condition=cond,
    )
except IntegrityError as e:
    print("Conflicting event was rejected:", e)

# Idempotent retry with same event UUID and condition should return same commit position
position2 = client.append([ev], condition=cond)
assert position1 == position2
print("Idempotent retry returned position:", position2)

Reading Events

The Client.read() method returns recorded events from an UmaDB server.

def read(
    query: Query | None = None,
    start: int | None = None,
    backwards: bool = False,
    limit: int | None = None,
    subscribe: bool = False,
) -> ReadResponse:
    ...

The Client.read() method can be used both for constructing decision models in a domain layer, and for projecting events into materialized views in CQRS. An optional Query can be provided to select by tags and types.

Parameters

Name Type Description
query Query|None Optional structured query to filter events (by tags, event types, etc).
start int|None Read events from this sequence number. Only events with positions greater than or equal will be returned (or less than or equal if backwards is True.
backwards bool If True events will be read backwards, either from the given position or from the last recorded event.
limit int|None Optional cap on the number of events to retrieve.
subscribe bool If True, keeps the stream open to deliver future events as they arrive.

Return Value

Returns an iterable "read response" instance from which SequencedEvent instances, and the most relevant "last known" sequence number, can be obtained.

Example

from umadb import Client, Query, QueryItem

client = Client("http://localhost:50051")

# Filter by type(s) and tag(s)
q = Query(items=[QueryItem(types=["example"], tags=["tag1", "tag2"])])

resp = client.read(
    query=q, start=None, backwards=False, limit=None, subscribe=False
)
for item in resp:
    print(f"Got event at position {item.position}: {item.event}")

last_known = resp.head()
print("Last known position:", last_known)

# Subscribe to new events
subscription = client.read(subscribe=True)
for se in subscription:
    print("New event:", se.position, se.event)
    # Break for demo purposes
    break

Getting Head Position

The Client.head() method returns the position of the last event recorded in an UmaDB server.

def head(self) -> int | None: ...

The Client.head() method can be used both for constructing decision models in a domain layer, and for projecting events into materialized views in CQRS. An optional DCBQuery can be provided to select by tags and types.

Return Value

Returns the position (u64) of the last recorded event in the event store, or None if no events have been recorded yet.

Getting Tracking Info

The Client.get_tracking_info() method returns the last recorded position of an upstream event.

def get_tracking_info(self, source: str) -> int | None: ...

The Client.get_tracking_info() method can be used when starting or resuming an event-processing component. The event-processing component will start by requesting new events from the upstream sequence after this position. The position of an upstream event that has been processed successfully can be recorded atomically when appending new events generated by processing that event.

Parameters

Name Type Description
source str The name of the upstream source.

Returns the position (int) of an event in an upstream sequence, or None if no such event has been processed yet.

Data Structures

Event

An Event represents a single event either to be appended or already stored in the event log.

Field Type Description
event_type str The event’s logical type (e.g. "UserRegistered").
tags list<str> Tags assigned to the event (used for filtering and indexing).
data bytes Binary payload associated with the event.
uuid str|None Unique event ID.

Idempotent support for append operations is activated by setting a UUID on appended events.

Include in:

  • Append requests when writing new events to the store.

Included in:

  • SequencedEvent objects when the server responds to read requests.

Matched by:

  • QueryItem during read() and append() operations.

Append Condition

An AppendCondition causes an append request to fail if events match its Query, optionally after a sequence number.

Field Type Description
fail_if_events_match Query Query for conflicting events.
after int|None Sequence number.

Include in:

  • Append requests to define optimistic concurrent control.

To implement a consistency boundary, command handlers can use the same Query used when reading events as the value of fail_if_events_match, and the "head" sequence number received from the read response as the value of after.

Tracking Info

A TrackingInfo instance indicates the source and position of an upstream event that has been processed.

Field Type Description
source str Source name.
position int Sequence number.

Include in:

  • Append requests to specify tracking information; for event-processing components only.

To implement exactly-once semantics in event-processing components, pull events from an upstream source after the last recorded upstream position, then record the upstream positions of upstream events along with new state that results from processing those events. By processing event sequentially in this way, each event will be processed at least once. And by recording tracking information along with new state, the new state will be recorded at most once. The combination of "at least once" processing and "at most once" recording will give "exactly once" semantics from the point of view of consumers of the recorded state.

Query

A Query defines criteria for selecting events in the event store.

Field Type Description
items list[QueryItem] A list of selection criteria (logical OR).

An Event is selected if any QueryItem matches or the items field is empty.

Include in:

  • Read requests to select events returned by the server.
  • An AppendCondition to select conflicting events.

Query Item

A QueryItem defines a criterion for matching events.

Field Type Description
types list[str] List of event types (logical OR).
tags list[str] List of tags (logical AND).

A QueryItem will match an `Event if:

  • one of its types matches the Event.event_type or its types field is empty; AND
  • all of its tags match one of the Event.tags or its tags field is empty.

Sequenced Event

A SequencedEvent represents a recorded Event along with its assigned sequence number.

Field Type Description
position int The sequence number.
event Event The recorded event.

Included in:

  • Read responses when the server responds to read requests.

Error Handling

The Python client raises Python exceptions on error:

  • Integrity/condition failure: IntegrityError
  • Transport/connection errors: TransportError
  • Authentication failures: AuthenticationError
  • Other internal errors: RuntimeError or OSError

Your application should catch these as appropriate.

Complete Example

import uuid

from umadb import AppendCondition, Client, Event, IntegrityError, Query, QueryItem

# Connect to the gRPC server (TLS + API key)
client = Client(
    url="http://localhost:50051",
)

# Define a consistency boundary
cb = Query(items=[QueryItem(types=["example"], tags=["tag1", "tag2"])])

# Read events for a decision model
read_response = client.read(query=cb)
for result in read_response:
    print(f"Got event at position {result.position}: {result.event}")

# Remember the last-known position
last_known_position = read_response.head()
print("Last known position is:", last_known_position)

# Create an event with a UUID to enable idempotent append
event = Event(
    event_type="example",
    tags=["tag1", "tag2"],
    data=b"Hello, world!",
    uuid=str(uuid.uuid4()),
)

# Append event within the consistency boundary
condition = AppendCondition(fail_if_events_match=cb, after=last_known_position)
commit_position1 = client.append([event], condition=condition)
print("Appended event at position:", commit_position1)

# Append conflicting event — expect an error
try:
    conflicting_event = Event(
        event_type="example",
        tags=["tag1", "tag2"],
        data=b"Hello, world!",
        uuid=str(uuid.uuid4()),  # different UUID
    )
    client.append([conflicting_event], condition=condition)
except IntegrityError as e:
    print("Conflicting event was rejected:", e)

# Idempotent retry — same event ID and condition
print("Retrying to append event at position:", last_known_position)
commit_position2 = client.append([event], condition=condition)
assert commit_position1 == commit_position2
print("Append returned same commit position:", commit_position2)

# Subscribe to all events for a projection
subscription = client.read(subscribe=True)
for ev in subscription:
    print(f"Processing event at {ev.position}: {ev.event}")
    if ev.position == commit_position2:
        print("Projection has processed new event!")
        break

Example with Tracking

import uuid

from umadb import (
    AppendCondition,
    Client,
    Event,
    IntegrityError,
    Query,
    QueryItem,
    TrackingInfo,
)

# Connect to the gRPC server (TLS + API key)
client = Client(
    url="http://localhost:50051",
)

# Get last processed upstream event position
last_processed_position = client.get_tracking_info("upstream")

# Pull next unprocessed upstream event...
next_upstream_event_position = 1 + (last_processed_position or 0)

# Construct tracking information from next unprocessed event
tracking_info = TrackingInfo("upstream", next_upstream_event_position)

# Define a consistency boundary
cb = Query(items=[QueryItem(types=["example"], tags=["tag1", "tag2"])])

# Read events for a decision model
read_response = client.read(query=cb)
for result in read_response:
    print(f"Got event at position {result.position}: {result.event}")

# Remember the last-known position
last_known_position = read_response.head()
print("Last known position is:", last_known_position)

# Create an event with a UUID to enable idempotent append
event = Event(
    event_type="example",
    tags=["tag1", "tag2"],
    data=b"Hello, world!",
    uuid=str(uuid.uuid4()),
)

# Append event within the consistency boundary
condition = AppendCondition(fail_if_events_match=cb, after=last_known_position)
commit_position1 = client.append(
    [event], condition=condition, tracking_info=tracking_info
)
print("Appended event at position:", commit_position1)

# Idempotent retry — same event ID and condition
print("Retrying to append event at position:", last_known_position)
commit_position2 = client.append(
    [event], condition=condition, tracking_info=tracking_info
)
assert commit_position1 == commit_position2
print("Append returned same commit position:", commit_position2)

# Check tracking information
assert tracking_info.position == client.get_tracking_info("upstream")

# Unconditional append with conflicting tracking information — expect an error
try:
    conflicting_event = Event(
        event_type="example",
        tags=["tag1", "tag2"],
        data=b"Hello, world!",
        uuid=str(uuid.uuid4()),  # different UUID
    )
    client.append([conflicting_event], condition=None, tracking_info=tracking_info)
except IntegrityError as e:
    print("Conflicting event was rejected:", e)

Notes

  • Python client is synchronous and blocking; if you need async integration, run client calls in a thread pool or use an async worker that offloads to threads.
  • Event data is binary (bytes). Use a consistent serialization (e.g., JSON serialized to UTF-8 bytes, protobuf, msgpack) for your domain.
  • API keys must match the server configuration.
  • For TLS with self-signed certs, pass ca_path with your root/CA certificate.

Testing

The Python bindings can be tested by running a UmaDB server and executing Python code against it.

License

Licensed under either of:

at your option.

Project details


Download files

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

Source Distribution

umadb-0.3.0.tar.gz (60.7 kB view details)

Uploaded Source

Built Distributions

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

umadb-0.3.0-cp310-abi3-win_amd64.whl (1.9 MB view details)

Uploaded CPython 3.10+Windows x86-64

umadb-0.3.0-cp310-abi3-win32.whl (1.6 MB view details)

Uploaded CPython 3.10+Windows x86

umadb-0.3.0-cp310-abi3-musllinux_1_2_x86_64.whl (2.4 MB view details)

Uploaded CPython 3.10+musllinux: musl 1.2+ x86-64

umadb-0.3.0-cp310-abi3-musllinux_1_2_aarch64.whl (2.2 MB view details)

Uploaded CPython 3.10+musllinux: musl 1.2+ ARM64

umadb-0.3.0-cp310-abi3-manylinux_2_28_x86_64.whl (2.3 MB view details)

Uploaded CPython 3.10+manylinux: glibc 2.28+ x86-64

umadb-0.3.0-cp310-abi3-manylinux_2_28_aarch64.whl (2.0 MB view details)

Uploaded CPython 3.10+manylinux: glibc 2.28+ ARM64

umadb-0.3.0-cp310-abi3-macosx_11_0_arm64.whl (2.0 MB view details)

Uploaded CPython 3.10+macOS 11.0+ ARM64

umadb-0.3.0-cp310-abi3-macosx_10_12_x86_64.whl (2.2 MB view details)

Uploaded CPython 3.10+macOS 10.12+ x86-64

File details

Details for the file umadb-0.3.0.tar.gz.

File metadata

  • Download URL: umadb-0.3.0.tar.gz
  • Upload date:
  • Size: 60.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: maturin/1.11.5

File hashes

Hashes for umadb-0.3.0.tar.gz
Algorithm Hash digest
SHA256 f49f91df9ca57960b0660213fb7f153fe26c74f75df6506df71bff4e394aee59
MD5 852cec1c6c121fa9745975072585d9ef
BLAKE2b-256 61660627e85f66862662fb21337a81046bb4f3fd22dab034e07ce3824ca764f6

See more details on using hashes here.

File details

Details for the file umadb-0.3.0-cp310-abi3-win_amd64.whl.

File metadata

  • Download URL: umadb-0.3.0-cp310-abi3-win_amd64.whl
  • Upload date:
  • Size: 1.9 MB
  • Tags: CPython 3.10+, Windows x86-64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: maturin/1.11.5

File hashes

Hashes for umadb-0.3.0-cp310-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 c860c312bcb328e63f041aa71ab21cd3f4a074b962001297d4cea180643687b7
MD5 e412067dc8e799950e9c4f1e877d1bff
BLAKE2b-256 c7932be3d22f0b83b9442666efac2884cb04b28f3c3eb62a5f01cca1e5c0e9bc

See more details on using hashes here.

File details

Details for the file umadb-0.3.0-cp310-abi3-win32.whl.

File metadata

  • Download URL: umadb-0.3.0-cp310-abi3-win32.whl
  • Upload date:
  • Size: 1.6 MB
  • Tags: CPython 3.10+, Windows x86
  • Uploaded using Trusted Publishing? No
  • Uploaded via: maturin/1.11.5

File hashes

Hashes for umadb-0.3.0-cp310-abi3-win32.whl
Algorithm Hash digest
SHA256 9458831c88c5fee2170cc9235bf75a37c65d9fc01803c8dcd0abf7b9aa64d565
MD5 28178333b81b9c2032d82bb1b2ef447f
BLAKE2b-256 f1af4713dc164baef5521b9fa86dc8158b6a740314431c041fa1802a1e8d3afe

See more details on using hashes here.

File details

Details for the file umadb-0.3.0-cp310-abi3-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for umadb-0.3.0-cp310-abi3-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 ade937564d76536bf683681924415436b1b8d9a2cb4e0c64b25a28f4f4fb5eb4
MD5 82fd7c85b8c8be3c27a616ab2cd50cb2
BLAKE2b-256 8e8f5ec04cab6697465f615d738960337d678881da26edf97a12f08198b0a936

See more details on using hashes here.

File details

Details for the file umadb-0.3.0-cp310-abi3-musllinux_1_2_aarch64.whl.

File metadata

File hashes

Hashes for umadb-0.3.0-cp310-abi3-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 4eaf8b000610e06dc47e12f935b4e649576a5d6df2808b17d1f81b324a411eb2
MD5 7ca67c94d1b094b65ed71e32f9e1fea8
BLAKE2b-256 1f7e263c61cd90749fb7653d6803d93f8494a60cb94b8bed42ac50066292fdc2

See more details on using hashes here.

File details

Details for the file umadb-0.3.0-cp310-abi3-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for umadb-0.3.0-cp310-abi3-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 daeee1535f7e5b4f2095b6ff2a90e13f986215c1e33aaaf60eb66c0b4f18c0e4
MD5 53298f441ae16b1556903ecf8485588e
BLAKE2b-256 41f3b00e45725d7b2fc468d1878a8af00f4f9ea73683e0a6fc9f3954aba4ba00

See more details on using hashes here.

File details

Details for the file umadb-0.3.0-cp310-abi3-manylinux_2_28_aarch64.whl.

File metadata

File hashes

Hashes for umadb-0.3.0-cp310-abi3-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 54e16df787a3047788060f3387d706e089307da5b106372ab5ef3536d2875401
MD5 7e1b782f0c9b8d464a87aa7a7c8b80f6
BLAKE2b-256 732ae5a88329d44ff347e3fca6ff71acc2ba1e713709dacf6284f41094ba6fda

See more details on using hashes here.

File details

Details for the file umadb-0.3.0-cp310-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for umadb-0.3.0-cp310-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 9e228b0374422f008253a90611c7e4416db0e66fefe00e70d82459f26ec3e089
MD5 fbd0d173d32801d0565fa3945c85e9e2
BLAKE2b-256 133226824ba723207cacb4a40dbdcb37f2187e7b3eca8dd140eb0d0ab69e0569

See more details on using hashes here.

File details

Details for the file umadb-0.3.0-cp310-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for umadb-0.3.0-cp310-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 1731c555b9903c0b501f8c85a741d688af881060b52bdae01dc0e434e92c978c
MD5 d9cc85f017efa27c6e926d2195f41f7f
BLAKE2b-256 9702d3d819f8f453962339dced4afcb60822ed6c74ca7f4b7ee346db3160dd57

See more details on using hashes here.

Supported by

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