Skip to content

Memory Service ORM

datarobot[application-utils] ships a lightweight async ORM over the DataRobot Agentic Memory Service. Think of it as a typed, Pydantic v2-style document store that persists sessions (documents) and events (log entries) through a bearer-auth REST API, using your existing DATAROBOT_ENDPOINT and DATAROBOT_API_TOKEN credentials.

Install

pip install "datarobot[application-utils]"

Quick-start

import asyncio
from typing import Annotated

from datarobot.application_utils.persistence import (
    DRConcurrencyField,
    DRDeduplicationKey,
    DREvent,
    DRMemorySpace,
    DRRangeKey,
    DRSession,
    DRMemoryServiceClient,
    SYSTEM_PARTICIPANT,
)


# ── 1. Define your domain models ─────────────────────────────────────────────


class ChatSession(DRSession):
    """A chat session stored in the Memory Service."""

    __description_prefix__ = "chat"  # stable prefix; part of every query

    tenant: Annotated[str, DRRangeKey]  # description segment 1 (range queries)
    topic: Annotated[str, DRRangeKey]  # description segment 2 (range queries)
    chat_id: Annotated[str, DRDeduplicationKey]  # point-lookup / idempotent create
    rev: Annotated[int, DRConcurrencyField]  # mirrors server version integer
    title: str = ""  # plain metadata (payload only)


class ChatMessage(DREvent, session=ChatSession):
    """A single message in a chat session."""

    __event_type__ = "message"

    score: float = 0.0  # extra body field; round-trips through the wire


# ── 2. Use the ORM ────────────────────────────────────────────────────────────


async def demo() -> None:
    async with DRMemoryServiceClient() as client:
        # Create or adopt an existing space (idempotent via deduplication_key)
        space = await DRMemorySpace.post(
            client,
            description="My agent memory",
            deduplication_key="my-agent-space-v1",
        )

        # Create (or adopt) a session
        session = await ChatSession.post(
            space,
            tenant="acme",
            topic="billing",
            chat_id="billing-chat-001",
            title="Billing enquiry",
        )
        # `rev` is never passed in: it mirrors the server-assigned version.
        assert session.rev == 1

        # Append events
        msg = await ChatMessage.post(
            session,
            content="Hello, I need help with my invoice.",
            emitter_type="user",
            emitter_id="aabbccddeeff001122334455",  # 24-hex ObjectId
            score=0.9,
        )
        print(msg.sequence_id, msg.created_at)

        # List recent messages
        recent = await ChatMessage.last(session, n=10)

        # Fetch by range-key prefix (all billing sessions for "acme")
        billing_sessions = await ChatSession.list(space, tenant="acme", topic="billing")

        # Update session metadata (optimistic-concurrency guard via If-Match)
        await session.patch(title="Resolved billing enquiry")

        # Patch an event (guarded by its createdAt token)
        await msg.patch(score=0.5)


asyncio.run(demo())

Environment variables

Variable Description
DATAROBOT_ENDPOINT Full DataRobot API base URL, e.g., https://app.datarobot.com/api/v2.
DATAROBOT_API_TOKEN DataRobot API bearer token.

Both are resolved automatically; pass them as constructor arguments to override.

Core concepts

Memory space (DRMemorySpace)

A namespace that owns sessions and events. Create one per-agent deployment or application context. Idempotent via deduplication_key.

space = await DRMemorySpace.post(client, deduplication_key="my-app-v1")
space2 = await DRMemorySpace.get(client, space.id)
spaces = await DRMemorySpace.list(client, deduplication_key="my-app-v1")
await space.patch(description="Updated description")
await space.delete()

Sessions (DRSession)

Documents stored in a memory space. Subclass DRSession and annotate fields with ORM markers:

Marker Wire field Purpose
Annotated[T, DRDeduplicationKey] deduplicationKey Unique key; idempotent create.
Annotated[T, DRRangeKey] description segment Range / prefix queries.
Annotated[T, DRConcurrencyField] version mirror User-visible version counter.
(plain field) metadata Arbitrary payload; not queryable.

Declare range-key fields in query order — a list query must specify a contiguous leading prefix (see §Range-key encoding below).

Lifecycle strategies (TTL)

By default, every DRSession subclass sends a single soft_delete lifecycle strategy on creation, triggered by a 2-year TTL (DEFAULT_SESSION_TTL_SECONDS, 63072000 seconds) — the Memory Service’s own maximum for a TTL trigger. Sessions therefore auto-clean unless you override this.

Override __lifecycle_strategies__ to use a shorter TTL (or a different strategy):

class ChatSession(DRSession):
    __description_prefix__ = "chat"
    __lifecycle_strategies__ = [
        {"type": "soft_delete", "trigger": {"ttl": 30 * 86400}},  # 30 days
    ]
    ...

Set it to an empty list to send no lifecycle strategies at all:

class ChatSession(DRSession):
    __lifecycle_strategies__ = []

Lifecycle strategies are sent on create only; session.patch(...) never touches them. Up to five strategy objects are allowed per session.

Events (DREvent)

An append-only log under a session. Bind to a session type with class MyEvent(DREvent, session=MySession). All plain declared fields map to the body dict.

class ChatMessage(DREvent, session=ChatSession):
    __event_type__ = "message"  # "message" | "tool_output" | "status"
    score: float = 0.0

Declared fields round-trip through any Pydantic-expressible type — nested models, list[Model], Optional[Model], enums and dicts all serialize into the event body (and session metadata) and validate back on read. This is what lets the chat-history layer nest typed ToolCall / Reasoning models inside a single message event.

Range-key encoding

Range keys are encoded in the session description field using a hierarchical path scheme:

description = "//" + esc(prefix) + "/" + esc(k1) + "/" + esc(k2) + "/"

esc(v) percent-encodes % (→ %25) then / (→ %2F), so values can contain arbitrary text including slashes. The leading // and trailing / after every segment create an anchored prefix — a substring-match on the service side is equivalent to a hierarchy prefix query.

Example — two sessions stored under chat/acme:

//chat/acme/billing/    ← tenant=acme, topic=billing
//chat/acme/support/    ← tenant=acme, topic=support

Query list(tenant="acme") sends description=//chat/acme/ which matches both. Query list(tenant="acme", topic="billing") sends description=//chat/acme/billing/ which matches only the first.

A subclass’ .list always sends at least its //<prefix>/ description filter — even with no range-key arguments — so ChatSession.list(space) returns only chat-prefixed sessions, never sessions of another DRSession subclass that happens to share the same space. (Trailing-slash anchoring keeps prefixes disjoint, so //chat/ never matches a //chatx/… or //loc/… session.) This subclass isolation is what lets the chat-history layer keep its Chat sessions and its EntityLocator index sessions (prefix loc) in one space without either bleeding into the other’s .list results.

⚠️ Case-insensitive caveat — the service performs a case-insensitive substring match, so Acme and acme are treated as the same tenant. Values that differ only in case will collide.

Optimistic concurrency

Sessions

Session PATCH sends an If-Match: <version> header. If the server’s version has advanced since you last fetched the session, the service returns HTTP 409 and the ORM raises DRMemoryVersionConflictError. Resolve by re-fetching (DRSession.get) and retrying.

try:
    await session.patch(title="New title")
except DRMemoryVersionConflictError:
    session = await ChatSession.get(space, id=session.id)
    await session.patch(title="New title")

Events

Event PATCH uses createdAt as a query-string concurrency token instead of a header. A stale token yields HTTP 422 and DRMemoryVersionConflictError.

try:
    await msg.patch(content="Corrected text")
except DRMemoryVersionConflictError:
    # Re-list to obtain fresh tokens
    events = await ChatMessage.list(session)
    msg = next(e for e in events if e.sequence_id == msg.sequence_id)
    await msg.patch(content="Corrected text")

Batch operations

# Atomic batch create (up to 200 events)
msgs = await ChatMessage.post_batch(
    session,
    events=[
        {"content": "First", "emitter_type": "agent"},
        {"content": "Second", "emitter_type": "agent", "score": 0.5},
    ],
)

# Atomic batch patch (up to 200 events)
await ChatMessage.patch_batch(
    session,
    updates=[
        (msgs[0], {"score": 0.9}),
        (msgs[1], {"content": "Updated second"}),
    ],
)

Emitter participant check

When emitter_type="user", the emitter’s ObjectId must be in the session’s participants list. The ORM raises DRMemoryBadRequestError early (before the HTTP call) if the emitter is not a participant.

The system sentinel SYSTEM_PARTICIPANT = "000000000000000000000000" is a client-side convention for agent-owned sessions. It is a valid ObjectId accepted by the service.

Error types

Exception HTTP status Cause
DRMemoryBadRequestError 400 Invalid request (emitter not a participant, etc.)
DRMemoryNotFoundError 404 Resource does not exist
DRMemoryConflictError 409 Deduplication conflict on create (ORM auto-adopts)
DRMemoryVersionConflictError 409 / 422 Stale If-Match or createdAt token
DRMemoryValidationError 422 Schema validation error
DRMemoryRateLimitError 429 Trial quota / rate limit; retry_after carries the Retry-After seconds (None for the storage cap, which sends no header)
DRMemoryUnavailableError
DRMemoryServiceError other 4xx/5xx Unexpected error

Public API

from datarobot.application_utils.persistence import (
    DRMemorySpace,
    DRSession,
    DREvent,
    DRDeduplicationKey,
    DRRangeKey,
    DRConcurrencyField,
    DRMemoryServiceClient,
    SYSTEM_PARTICIPANT,
    DEFAULT_SESSION_TTL_SECONDS,
    DRMemoryServiceError,
    DRMemoryNotFoundError,
    DRMemoryBadRequestError,
    DRMemoryValidationError,
    DRMemoryConflictError,
    DRMemoryVersionConflictError,
    DRMemoryRateLimitError,
    DRMemoryUnavailableError,
)

Client and models

DRMemoryServiceClient

class datarobot.application_utils.persistence.DRMemoryServiceClient

Async HTTP client for the DataRobot Agentic Memory Service.

Resolves DATAROBOT_ENDPOINT and DATAROBOT_API_TOKEN from the environment; constructor arguments take precedence. Owns an httpx.AsyncClient unless one is injected.

The instance itself is lightweight — the resolved base URL plus a headers dict. Applications that act on behalf of many principals (a different API token per end user) are supported by constructing one DRMemoryServiceClient per principal over a single shared http_client: the shared pool carries the connections, each instance carries only an identity.

That isolation holds only while the shared client stays identity-free: construct it with no auth (request() also sends auth=None as a guard, since identity always travels in this instance’s headers) and a cookie jar that stores nothing — httpx.AsyncClient persists Set-Cookie responses in a jar shared by every request through it, which would replay one principal’s cookies on another principal’s requests. The service authenticates by header, not cookies, so refusing cookies loses nothing (see the multi-principal example below).

Parameters

Parameter Type Description
endpoint str \| None DataRobot API endpoint (e.g. https://app.datarobot.com/api/v2). Defaults to the DATAROBOT_ENDPOINT environment variable.
api_token str \| None DataRobot API token. Defaults to the DATAROBOT_API_TOKEN environment variable. Multi-principal applications must pass this explicitly — the environment fallback is the application’s own credential, not the requesting user’s.
base_path str Sub-path appended to the endpoint. Defaults to "memory" — the gateway mount path for the Memory Service (/api/v2/memory).
http_client httpx.AsyncClient \| None Injected async client. When supplied, the DRMemoryServiceClient will not close it on aclose() — the caller owns its lifetime. This is a supported production pattern (share one connection pool across many per-principal client instances) as well as the hook for testing with respx. A client shared across principals must be identity-free: no auth, and a non-storing cookie jar (see Examples). When omitted, the client creates and owns a pool of its own.
timeout float Default per-request timeout in seconds. Ignored when http_client is injected — configure the timeout on the injected client instead.

Examples

One client, one credential (scripts, single-principal apps):

import asyncio
from datarobot.application_utils.persistence import (
    DRMemorySpace,
    DRMemoryServiceClient,
)

async def main() -> None:
    async with DRMemoryServiceClient() as client:
        space = await DRMemorySpace.post(client, description="my-space")
        print(space.id)

asyncio.run(main())

One shared pool, one thin client per principal (multi-user web apps):

import http.cookiejar

import httpx

# App startup.  The shared pool must stay identity-free: no auth
# (identity belongs on each DRMemoryServiceClient, not the
# transport), and a cookie jar that stores nothing — httpx would
# otherwise replay one principal's Set-Cookie on another's requests.
shared_http = httpx.AsyncClient(
    timeout=30.0,
    cookies=http.cookiejar.CookieJar(
        policy=http.cookiejar.DefaultCookiePolicy(allowed_domains=[])
    ),
)

def client_for(user_token: str) -> DRMemoryServiceClient:
    # Cheap: per-request construction, no new connections.
    return DRMemoryServiceClient(
        api_token=user_token,
        http_client=shared_http,
    )

async def shutdown() -> None:
    await shared_http.aclose()  # close the pool once, at app shutdown

base_url

property base_url

The resolved Memory Service base URL.

request()

method request()

Send a request and return the response, mapping errors to typed exceptions.

Parameters

Parameter Type Description
method str HTTP method ("GET", "POST", "PATCH", "DELETE").
path str Path relative to base_url. Must end with "/".
params dict \| None Query parameters.
json Any JSON-serializable request body.
extra_headers dict \| None Additional headers (e.g. {"If-Match": "3"}).

Returns

Returns Description
On 2xx status codes.

Return type: httpx.Response

Raises

Exception Description
DRMemoryBadRequestError HTTP 400 — bad request (e.g. emitter not a participant).
DRMemoryNotFoundError HTTP 404 — resource not found.
DRMemoryConflictError HTTP 409 — deduplication conflict on create.
DRMemoryVersionConflictError HTTP 409 — stale If-Match on session patch(), or HTTP 422 with the event-version detail.
DRMemoryValidationError HTTP 422 — schema validation error.
DRMemoryRateLimitError HTTP 429 — quota or rate limit exceeded; carries retry_after seconds parsed from the Retry-After response header.
DRMemoryUnavailableError
DRMemoryServiceError Any other 4xx/5xx error.

aclose()

method aclose()

Close the underlying HTTP client (only if owned by this instance).

Return type: None

DRMemorySpace

class datarobot.application_utils.persistence.DRMemorySpace

Represents a DataRobot Agentic Memory Service memory space.

Acts as the container for sessions; every session and event call is scoped to a space. Construct via the class methods post() or get() rather than directly instantiating.

Variables

Attribute Type Description
description str \| None Human-readable description of the space.
deduplication_key str \| None Unique client-assigned key; enables idempotent space creation.
llm_model_name str \| None LLM model name for extract_memories lifecycle strategies.
llm_base_url str \| None LLM base URL override.
custom_instructions str \| None Custom instructions passed to the LLM when extracting memories.
created_at str ISO-8601 creation timestamp (server-assigned).

id

property id

Server-assigned memory space UUID.

user_id

property user_id

Owner user ID.

tenant_id

property tenant_id

Tenant UUID.

post()

method post()

Create a new memory space, or adopt an existing one on a deduplication conflict.

If a deduplication_key is supplied and a space with that key already exists, the existing space is fetched and returned (409 → adopt).

Parameters

Parameter Type Description
client DRMemoryServiceClient Transport client.
description str \| None Human-readable description (max 1000 chars).
deduplication_key str \| None Unique client key for idempotent creation (1–72 chars).
llm_model_name str \| None LLM model name for extract_memories strategies.
llm_base_url str \| None LLM base URL override.
custom_instructions str \| None Custom LLM instructions (max 10 000 chars).

Returns

Returns Description
The newly created or adopted memory space.

Return type: DRMemorySpace

get()

method get()

Fetch a memory space by its server-assigned ID.

Parameters

Parameter Type Description
client DRMemoryServiceClient Transport client.
space_id str UUID of the memory space.

Return type: DRMemorySpace

Raises

Exception Description
DRMemoryNotFoundError If no space with the given ID exists (or it belongs to another user).

list()

method list()

List memory spaces visible to the authenticated user.

Parameters

Parameter Type Description
client DRMemoryServiceClient Transport client.
deduplication_key str \| None Exact-match filter on deduplicationKey.
offset int Number of spaces to skip (for pagination).
limit int Maximum number of spaces to return (1–100).

Return type: list[DRMemorySpace]

patch()

method patch()

Update this memory space in place.

Only the supplied keyword arguments are changed; omitted fields keep their current values on the server.

Parameters

Parameter Type Description
description str \| None New description.
llm_model_name str \| None New LLM model name.
llm_base_url str \| None New LLM base URL.
custom_instructions str \| None New custom instructions.

Raises

Exception Description
ValueError When no field is supplied, rather than sending a no-op request.

Return type: None

delete()

method delete()

Soft-delete this memory space.

After deletion the space is no longer accessible via get or list.

Return type: None

DRSession

class datarobot.application_utils.persistence.DRSession

Abstract base class for Memory Service ORM session models.

Do not instantiate directly; subclass and declare fields with ORM markers.

Class variables

__description_prefix__ : Prefix injected at the start of the encoded description. Defaults to the subclass name. Keep it short and stable; it is part of every stored description and every list-query filter.

__lifecycle_strategies__ : Lifecycle strategy objects sent on session creation. Defaults to a single soft_delete strategy with a DEFAULT_SESSION_TTL_SECONDS (2 year) TTL trigger, so sessions auto-clean unless a subclass overrides this. Override with a different strategy list to change the TTL/strategy, or set to [] to send no lifecycle strategies at all.

Read-only properties

id : Server-assigned session UUID.

created_at : ISO-8601 creation timestamp.

version : Server-assigned version integer (optimistic-concurrency token).

id

property id

Server-assigned session UUID (read-only).

created_at

property created_at

ISO-8601 creation timestamp (read-only).

version

property version

Current server version integer (read-only; updated on every patch).

post()

method post()

Create a session, or adopt the existing one on a deduplication conflict.

Parameters

Parameter Type Description
space DRMemorySpace Memory space to create the session in. * **kwargs (Any) – Session field values. Pass participants=["<objectid>"] to scope to a user; omit to use the system sentinel. A DRConcurrencyField may not be passed — it mirrors the server-assigned version and is populated from the response.

Returns

Returns Description
The newly created or adopted session.

Return type: DRSession

Raises

Exception Description
ValueError On undeclared kwargs, or when a DRConcurrencyField is supplied.

Examples

session = await ChatSession.post(
    space,
    tenant="acme",
    topic="billing",
    chat_id="chat-001",
    title="Billing enquiry",
)

get()

method get()

Fetch a session by its server-assigned id or by deduplication_key.

Exactly one of id= or the subclass’s DRDeduplicationKey field name must be supplied.

Parameters

Parameter Type Description
space DRMemorySpace Memory space containing the session.
id str \| None Server-assigned session UUID. * **kwargs (Any) – Pass the DRDeduplicationKey field name as a keyword argument for an exact-match point lookup (e.g. chat_id="billing-chat-001").

Return type: DRSession

Raises

Exception Description
DRMemoryNotFoundError If no matching session is found.
ValueError If neither id nor a dedup key is supplied, or the subclass has no DRDeduplicationKey field and a keyword arg is provided.

Examples

# By server id
session = await ChatSession.get(space, id="uuid-string")

# By dedup key
session = await ChatSession.get(space, chat_id="billing-chat-001")

list()

method list()

List sessions matching a range-key prefix and/or participant filter.

The query is always scoped to this subclass by its __description_prefix__ (so list() never returns sessions of a different subclass sharing the space); range-key kwargs narrow it further.

Range-key kwargs must form a contiguous leading prefix of the declared DRRangeKey fields (e.g. for fields [tenant, topic] you may filter on tenant= alone or on tenant= + topic=, but not on topic= alone).

Parameters

Parameter Type Description
space DRMemorySpace Memory space to query.
participant str \| None Filter to sessions that include this ObjectId in participants. * **kwargs (Any) – Leading range-key field values for a prefix query.

Returns

Returns Description
All matching sessions (auto-paginated).

Return type: list[DRSession]

Raises

Exception Description
ValueError If range-key kwargs are not a contiguous leading prefix.

Examples

# All sessions for tenant "acme"
sessions = await ChatSession.list(space, tenant="acme")

# Scoped to a user
sessions = await ChatSession.list(space, participant=user_oid)

# Combined: user + range prefix
sessions = await ChatSession.list(
    space, participant=user_oid, tenant="acme", topic="billing"
)

patch()

method patch()

Update this session in place.

Pass any combination of metadata fields and/or DRRangeKey fields. participants and DRDeduplicationKey fields cannot be changed.

Parameters

Parameter Type Description
**kwargs (Any) – Fields to update.

Raises

Exception Description
DRMemoryVersionConflictError If the session was updated concurrently (stale If-Match).

Return type: None

Examples

await session.patch(title="New title")
await session.patch(topic="support", title="Re: billing")

delete()

method delete()

Soft-delete this session.

After deletion the session is no longer returned by get or list.

Return type: None

DREvent

class datarobot.application_utils.persistence.DREvent

Abstract base class for Memory Service ORM event models.

Do not instantiate directly; subclass with session=<SessionClass>.

Parameters

Parameter Type Description
content str Event text (1–100 000 characters).
emitter_type Literal[```”user”, "agent"]` Who produced the event.
emitter_id str \| None ObjectId of the emitting user. Required when emitter_type="user" and must be a member of the session’s participants.
properties (Read-only)
--------------------
sequence_id int Server-assigned monotonic integer address (−1 before the event is posted).
created_at str ISO-8601 timestamp; also serves as the concurrency token for patch().

sequence_id

property sequence_id

Server-assigned event address (read-only; −1 before posting).

created_at

property created_at

ISO-8601 creation timestamp (read-only; also the concurrency token).

post()

method post()

Append a single event to the session.

Parameters

Parameter Type Description
session DRSession Session to append the event to.
content str Event text (1–100 000 characters).
emitter_type Literal[```”user”, "agent"]` Who produced the event.
emitter_id str \| None ObjectId of the emitter. Required when emitter_type="user" and must be in session.participants. * **kwargs (Any) – Any declared body fields (e.g. score=0.9).

Return type: DREvent

post_batch()

method post_batch()

Atomically append up to 200 events to the session.

All events are appended in list order. If any event fails validation the entire batch is rolled back.

Parameters

Parameter Type Description
session DRSession Session to append the events to.
events list[dict[str, Any]] Each dict contains the same keyword arguments as post() (content, emitter_type, optionally emitter_id and body fields).

Return type: list[DREvent]

Raises

Exception Description
ValueError If the batch exceeds 200 events.

Examples

msgs = await ChatMessage.post_batch(
    session=my_session,
    events=[
        {"content": "Hi", "emitter_type": "user", "emitter_id": oid},
        {"content": "Hello", "emitter_type": "agent"},
    ],
)

list()

method list()

List events under a session, optionally filtered by type.

Parameters

Parameter Type Description
session DRSession Session to query.
type str \| None Event type filter: "message", "tool_output", or "status". None returns all types.
offset int Number of events to skip.
limit int Maximum number of events to return (1–100).

Return type: list[DREvent]

last()

method last()

Return the last n events in chronological order.

lastN and offset are mutually exclusive on the service API; this method never sends offset.

Parameters

Parameter Type Description
session DRSession Session to query.
n int Number of tail events to return (1–100).
type str \| None Optional event-type filter.

Return type: list[DREvent]

patch()

method patch()

Update this event in place, guarded by its created_at token.

At least one field must be supplied.

Parameters

Parameter Type Description
content str \| None New event text.
emitter_type Literal[```”user”, "agent"] | None` New emitter type.
emitter_id str \| None New emitter ObjectId. * **kwargs (Any) – Any declared body fields to update.

Raises

Exception Description
DRMemoryVersionConflictError If the event was updated concurrently (stale createdAt token).

Return type: None

delete()

method delete()

Soft-delete this event.

After deletion the event is no longer returned by list or last.

Return type: None

patch_batch()

method patch_batch()

Atomically update up to 200 events, each guarded by its created_at token.

Parameters

Parameter Type Description
session DRSession Session containing the events. All events must belong to this session.
updates list[tuple[DREvent, dict[str, Any]]] Each tuple is (event_instance, kwargs). The kwargs accept the same arguments as patch().

Returns

Returns Description
Updated event instances (in the order of the input list).

Return type: list[DREvent]

Raises

Exception Description
ValueError If the batch exceeds 200 updates or no fields are provided for an item.
DRMemoryVersionConflictError If any event was updated concurrently.

Examples

await ChatMessage.patch_batch(
    session=my_session,
    updates=[
        (event_a, {"score": 0.9}),
        (event_b, {"content": "Updated text"}),
    ],
)

Field markers and constants

DRDeduplicationKey

class datarobot.application_utils.persistence.DRDeduplicationKey

Marker: field maps to the session deduplicationKey (point-lookup primary key).

At most one field per DRSession subclass may carry this marker. Enables MySession.get(space, my_key="value") exact-match point lookups and idempotent session creation (409 → adopt the existing session).

DRRangeKey

class datarobot.application_utils.persistence.DRRangeKey

Marker: field maps to a segment of the session description (range/prefix queries).

Segments are appended to description in declaration order, encoded with the //prefix/seg1/seg2/ scheme. Values must be non-empty strings.

Known limitation

The Memory Service description filter is case-insensitive, so values differing only in case (e.g. Foo vs foo) will collide when querying.

DRConcurrencyField

class datarobot.application_utils.persistence.DRConcurrencyField

Marker: field is kept in sync with the server version integer.

Enables user code to inspect the current optimistic-concurrency version without calling .version directly. At most one field per DRSession subclass may carry this marker.

Whether or not this marker is present, the ORM always tracks the server version internally for If-Match concurrency control on patch().

datarobot.application_utils.persistence.markers.SYSTEM_PARTICIPANT : str = '000000000000000000000000'

Sentinel ObjectId for sessions that belong to no specific user. This is a client-side convention; the Memory Service treats it as any other valid 24-hex ObjectId.

datarobot.application_utils.persistence.markers.DEFAULT_SESSION_TTL_SECONDS : int = 63072000

Default session TTL in seconds (2 years). Also the Memory Service maximum for a TTL trigger; used to build the default soft_delete lifecycle strategy.

Errors

exception datarobot.application_utils.persistence.DRMemoryServiceError

Base exception for all Memory Service ORM errors.

Parameters

Parameter Type Description
detail str Human-readable error detail.
status_code int \| None HTTP status code, if applicable.
payload dict \| None Raw response body, if available.

exception datarobot.application_utils.persistence.DRMemoryNotFoundError

Raised when the requested resource does not exist (HTTP 404).

exception datarobot.application_utils.persistence.DRMemoryBadRequestError

Raised on client-side validation errors from the service (HTTP 400).

Common causes: an event emitter is not a session participant, an invalid ObjectId is supplied for a participant filter.

exception datarobot.application_utils.persistence.DRMemoryValidationError

Raised on schema validation errors from the service (HTTP 422).

This typically means the request body or query parameters failed the service’s Pydantic validation (e.g. a field is too long, a required field is missing, or mutually exclusive parameters are both supplied).

exception datarobot.application_utils.persistence.DRMemoryConflictError

Raised on a deduplication conflict when creating a session or DRMemorySpace (HTTP 409).

The ORM automatically adopts the existing resource (by fetching it via existing_id) rather than propagating this exception to callers in the normal post() flow.

Parameters

Parameter Type Description
existing_id str \| None Server-assigned ID of the existing resource.
location str \| None URL from the service Location header pointing to the existing resource.

exception datarobot.application_utils.persistence.DRMemoryVersionConflictError

Raised on an optimistic-concurrency failure.

Surfaces as HTTP 409 for session patch() (stale If-Match header) and as HTTP 422 for event patch() (stale createdAt token).

Resolution: re-read the resource to get the current version, then retry.

exception datarobot.application_utils.persistence.DRMemoryRateLimitError

Raised when the service rejects a request due to quota or rate limits (HTTP 429).

The Memory Service enforces per-tenant trial quotas: monthly read/write counts answer 429 with a Retry-After header (seconds until the window resets), while the storage cap answers 429 without Retry-After — storage is a level, not a windowed quota, so freeing data (or upgrading) is the remedy rather than waiting.

Resolution: when retry_after is set, wait that many seconds before retrying or propagate the value to your own HTTP response so the caller can back off correctly. When it is None, retrying later will not help by itself — inspect detail for the limit that was hit.

Parameters

Parameter Type Description
retry_after int \| None Whole seconds to wait before retrying, parsed from the Retry-After response header (supports both delta-seconds and HTTP-date forms;integer so it can be propagated verbatim into another Retry-After header). None when the service did not send the header (e.g. the trial storage-cap 429).

exception datarobot.application_utils.persistence.DRMemoryUnavailableError

Raised when no HTTP response was received from the service.

Covers request timeouts and transport failures (connection refused, DNS resolution, TLS errors, protocol violations). The original httpx exception is preserved as __cause__.

status_code is always None: the failure happened before a status code existed. Catching DRMemoryServiceError therefore covers both service-side errors and an unreachable service, without importing httpx at call sites.

Running integration tests

Integration tests are skipped by default. To run them against a live endpoint:

export DATAROBOT_ENDPOINT="https://app.datarobot.com/api/v2"
export DATAROBOT_API_TOKEN="<your-token>"

pytest tests/application_utils/persistence/acceptance -m integration -vv