This release is a pre-release and may not be stable for production use.
ContextBase Python SDK
The Python client for ContextBase, a platform for retrieval-augmented generation over your own documents.
Install the preview release from PyPI with
pip install contextbase-sdk==2026.1.0rc1
Once the stable release is on PyPI, install it with
pip install contextbase-sdk
The distribution is named contextbase-sdk; the package you import is
contextbase. Python 3.11 or newer.
The source repository and its issue tracker require GitHub access during this preview.
Quick start
A key is minted by a person in the web interface (or through
POST /api/v1/auth/api-keys/). A key that creates a space is granted manage
on it, so one key is enough for everything below. Set the key and the server's
address, then run this as it stands:
export CONTEXTBASE_API_KEY=cb_...
export CONTEXTBASE_BASE_URL=https://contextbase.example.com
from contextbase import ContextBase, Done, ErrorEvent, Token
with ContextBase() as client:
client.check_compatibility()
space = client.spaces.create(
'Quick start',
llm_config={'model': 'openai/gpt-4o'},
)
try:
document = space.documents.upload(
b'The capital of France is Paris.',
filename='facts.txt',
)
space.documents.wait_indexed([document], timeout=120)
answer = space.query('What is the capital of France?')
print('answer:', answer.answer)
print('sources:', [source.text for source in answer.sources])
for event in space.query_stream('What is the capital of France?'):
if isinstance(event, Token):
print(event.delta, end='', flush=True)
elif isinstance(event, Done):
print('\nstreamed:', len(event.answer.sources), 'sources')
elif isinstance(event, ErrorEvent):
print('\nstream failed:', event.detail)
for source in space.search('capital', top_k=3):
print('found:', source.text)
finally:
space.delete()
The same with asyncio: AsyncContextBase has every call above, awaited,
and async for over list() and query_stream().
import asyncio
from contextbase import AsyncContextBase
async def main() -> None:
async with AsyncContextBase() as client:
async for space in client.spaces.list():
print(space.id, space.name)
asyncio.run(main())
Configuring a space and evaluating it
Reading a space's pipeline needs any grant on it; checking, applying and
rebuilding it, and evaluating the space, need manage, which a key that
created the space has. A key granted less is refused with
PermissionDeniedError. The catalogue and the presets are readable with any
key, and evaluation datasets belong to the key's owner.
from contextbase import ContextBase
from contextbase.types import PipelineConfigStatus
def show(status: PipelineConfigStatus) -> None:
states = ', '.join(f'{i.kind} {i.status}' for i in status.representations)
print('rebuild:', states)
with ContextBase() as client:
print('presets:', [preset.id for preset in client.pipeline.presets()])
space = client.spaces.create(
'Evaluated',
llm_config={'model': 'openai/gpt-4o'},
)
try:
space.documents.upload(
b'The capital of France is Paris.',
filename='facts.txt',
)
hybrid = {
**space.config.get().to_dict(),
'retrievers': [{'node': 'bm25'}, {'node': 'dense'}],
'fusion': {'node': 'rrf'},
}
report = space.config.validate(hybrid)
print('preflight:', report.status)
change = space.config.apply(hybrid, on_progress=show)
print('applied:', change.result.scope, 'stale:', change.stale)
dataset = client.eval_datasets.create(
'Capitals',
corpus=[{'id': 'fr', 'text': 'The capital of France is Paris.'}],
queries=[
{
'question': 'What is the capital of France?',
'relevant_ids': ['fr'],
},
],
)
run = space.evaluations.run(
dataset,
on_progress=lambda run: print(
'run:', run.status, run.progress.stage
),
)
print('metrics:', run.metrics)
for outcome in space.evaluations.queries(run):
print(outcome.position, outcome.question, outcome.answer)
finally:
space.delete()
config.apply and config.reindex wait for the space's indexes to be
rebuilt, unless wait=False. A worker takes the rebuild up some time after the
change is accepted, and until then the space's status still describes the
indexes as they were, so the wait is in two steps: up to start_timeout
seconds (60 by default) for the rebuild to begin -- RebuildNotStartedError
if it does not -- and then up to timeout for nothing to be building --
RebuildTimeoutError if something still is. Indexes left out of date with no
worker on them do not hold the wait up; change.stale names them, and a
config.reindex() rebuilds them. A change over storage this platform did not
make starts a pass over that storage, reported as change.storage_pass and
not waited for; the space's storage index, through client.raw, follows it.
A configuration its preflight checks refuse raises PreflightBlockedError
carrying their report -- as PreflightConflictError (a ConflictError) for
409 or PreflightUnprocessableError (an UnprocessableEntityError) for 422.
evaluations.run waits for the run: a failed one raises
EvaluationFailedError with the server's reason, one still going at timeout
raises EvaluationTimeoutError naming its stage and how far it got. The
timeouts derive from WaitTimeoutError, as does IndexingTimeoutError from
documents.wait_indexed.
Anything else the server offers -- the knowledge graph, a space's metadata
schema, its storage index, the chunk map, the agent graph -- is reached through
the generated operations with client.raw:
from uuid import UUID
from contextbase import ContextBase
from contextbase._generated.api.graph import get_graph_status
with ContextBase() as client:
space = next(iter(client.spaces.list()))
print(get_graph_status.sync(UUID(space.id), client=client.raw))
Downloading an original
Use the generated operation through client.raw. A successful response is a
File; read its payload to get the original bytes. The detailed variant also
provides the response headers and status. Error responses retain their generated
JSON models.
from uuid import UUID
from contextbase._generated.api.documents import download_document
from contextbase._generated.types import File
original = download_document.sync(
UUID(space.id),
UUID(document.id),
client=client.raw,
)
if isinstance(original, File):
original_bytes = original.payload.read()
For an asynchronous client, await download_document.asyncio with the same
arguments. The SDK does not write a path based on the attachment filename.
Typed inputs, or plain ones
A filter or a configuration is either a model from contextbase.types, which
an editor completes field by field, or a mapping of the same shape; both send
the same request. A model carries its defaults, so it may send a field the
mapping leaves to the server.
from contextbase import ContextBase
from contextbase.types import (
MetadataCondition,
MetadataFilter,
MetadataFilterJoin,
MetadataFilterOperator,
SpaceLLMConfig,
)
with ContextBase() as client:
space = client.spaces.create(
'Typed',
llm_config=SpaceLLMConfig(model='openai/gpt-4o', temperature=0.2),
)
try:
support_only = MetadataFilter(
join=MetadataFilterJoin.ALL,
conditions=[
MetadataCondition(
field='team',
operator=MetadataFilterOperator.EQ,
value='support',
),
],
)
same_as_a_mapping = {
'join': 'all',
'conditions': [
{'field': 'team', 'operator': 'eq', 'value': 'support'},
],
}
for narrowing in (support_only, same_as_a_mapping):
print(len(space.search('refunds', metadata_filter=narrowing)))
finally:
space.delete()
An id is taken as a string or a uuid.UUID, wherever a call takes one.
What the client does for you
- Keys and addresses.
ContextBase(api_key=..., base_url=...), orCONTEXTBASE_API_KEY/CONTEXTBASE_BASE_URL. Without a key onlymeta()andcheck_compatibility()work; everything else raisesMissingAPIKeyErrorbefore sending anything. - Timeouts. 60 seconds a request unless
timeout=says otherwise. A streamed answer is held to it only while connecting. - Repeats. A
GET,PUTorDELETEthe server answers with 429 or 503 is asked again, up tomax_retries=2times, waiting asRetry-Afterasks or doubling from half a second. A question (POST) is never asked twice. - Listings.
client.spaces.list()andspace.documents.list()walk every page, fetching the next only when it is read. - Streams.
space.query_stream()yieldsStage,Check,Token, thenDoneorErrorEvent. Stage and check events retainattempt_id; checks also identifynode_id, phase, verdict and action. Unknown event names are ignored for compatibility with newer servers. - Publication.
answer.outcomeisacceptedorblocked, for buffered queries andDone.answeralike. Check it before treating an empty answer as success.query_idlocates the authorized inspection record.postgenerationcarries the server's redacted execution summary; rejected drafts and judge explanations remain behind the server's content-access controls. Old servers that omit these fields default toaccepted,NoneandNone. - Errors. What the server or the connection causes derives from
ContextBaseError; arguments no request can be made from (no address, bytes with no filename) raiseValueError. An error status is anAPIErrorof its own class --AuthenticationError(401),PermissionDeniedError(403),NotFoundError(404),ConflictError(409),PayloadTooLargeError(413),RateLimitError(429),ServerError(5xx) and the rest -- carryingstatus_code,detail(the server's sentences),reason(its machine name for the refusal) andnames. A 404 for a path the server has no route for is anUnknownEndpointError, whose message suggests the server is older than this client. Not reaching the server is anAPIConnectionError;wait_indexedrunning out of time is anIndexingTimeoutErrorholding each document's last state. - Compatibility.
client.check_compatibility()raisesIncompatibleServerErrorfor a server older thanMINIMUM_SERVER_VERSIONor speaking another API version. That version is carried in the package rather than in the distribution's metadata, which has no field for it. - Escape hatch.
client.rawis the generated client underneath, sharing the client's connection, key and timeout, for an operation the client does not cover. It is typed as the generated operations take it, solist_spaces.sync(client=client.raw)type-checks. Without a key there is no authenticated client to hand over, and asking for it raisesMissingAPIKeyError. - Changing a space's settings is left to a person: the server refuses it
to every key, so the client has no call for it. The generated operation is
contextbase._generated.api.spaces.update_space, throughclient.raw.
Developing the SDK
Everything below runs from sdk/python/ and goes through one entry point,
scripts/sdk.py. The SDK has its own environment, managed by
uv (0.5.18 or newer), and shares nothing with the
server's Poetry environment.
uv sync # create the environment from uv.lock
uv run scripts/sdk.py gen # regenerate the derived sources
uv run scripts/sdk.py check-stale # refuse derived sources that are out of date
uv run scripts/sdk.py lint # ruff, the formatter's check, mypy
uv run scripts/sdk.py test # the unit suite, 100 % coverage
uv run scripts/sdk.py build # sdist and wheel into dist/
uv run scripts/sdk.py smoke-wheel # build, install into a fresh environment, call meta()
uv run scripts/sdk.py bump-check # refuse a version that understates a breaking change
uv run scripts/sdk.py release # see below
What is written and what is derived
src/contextbase/_generated/is generated by openapi-python-client fromopenapi/openapi.jsonat the repository root, the same snapshot the web client is generated from. Never edit it by hand. Binary response construction is customised by the file-property template intemplates/, so a binary schema uses the HTTP body even when its media type includes JSON. The template preserves the generator's file input handling. The generator is pinned inpyproject.toml; its configuration isopenapi-python-client.json.src/contextbase/_async/is the client, written by hand.src/contextbase/_sync/is produced from_async/by unasync. Edit_async/and rungen; the names renamed beyond unasync's own list are inSYNC_NAMESinscripts/sdk.py. Docstrings are copied across unchanged, so prose in_async/is written to be true of both clients.
Both derived directories are committed. check-stale refuses a package whose
derived directories are not what a regeneration writes; how it compares is
described at command_check_stale in scripts/sdk.py. The repository's gate
(docker/django/ci.sh) runs it together with lint and test. When the
server's API changes, the refusal of scripts/check_openapi_snapshot.py at the
repository root says how to refresh the snapshot and what to regenerate with
it.
oasdiff
bump-check judges schema changes with
oasdiff, and only with release 1.32.1:
another release may classify a change differently, so the command refuses any
other. Install it with either of
brew install oasdiff # while Homebrew ships 1.32.1
go install github.com/oasdiff/oasdiff@v1.32.1
Versions and releases
The SDK is versioned independently of the server. The version the next release will
carry is version in pyproject.toml; a release is the commit tagged
sdk-py/v<that version>.
bump-check compares openapi/openapi.json with the snapshot committed at the
newest final release tag older than the current version (pre-releases and
development releases promise nothing, so they are passed over). If oasdiff
reports a breaking change, the version must raise the major number -- before
1.0, the minor. With no earlier final release there is nothing to compare, and
any version passes.
release runs, in order and stopping at the first refusal:
- the tag at
HEAD(or the one named by--tag) points atHEADand names the version inpyproject.toml; sdk/python/andopenapi/hold no changegit statusreports, since the build andbump-checkread the working tree and the release isHEAD;bump-check;test;build;uv publish.
The upload goes where --to says (testpypi or pypi); without it, to
UV_PUBLISH_URL if that is set, and otherwise to TestPyPI. --to wins over
UV_PUBLISH_URL, and the release says so when the two disagree. The token is
read from UV_PUBLISH_TOKEN, and a release without one is refused before
anything runs. --dry-run runs every step but the upload.
UV_PUBLISH_TOKEN=pypi-... uv run scripts/sdk.py release # to TestPyPI
uv run scripts/sdk.py release --dry-run # stop before uploading
Record every release in CHANGELOG.md.
Supported Python versions
3.11 through 3.14. The gate runs the suite under the version
.python-version names; the oldest and the newest supported are checked by
hand, each in an environment kept outside the checkout, and by installing the
wheel under it:
UV_PROJECT_ENVIRONMENT=/tmp/sdk-py311 uv run --frozen --python 3.11 scripts/sdk.py test
UV_PROJECT_ENVIRONMENT=/tmp/sdk-py314 uv run --frozen --python 3.14 scripts/sdk.py test
uv run scripts/sdk.py smoke-wheel --python 3.11
uv run scripts/sdk.py smoke-wheel --python 3.14
Licence
Apache License 2.0; see LICENSE.
Applications and reference
See examples/README.md in the source repository for the executable notebook,
document ingestion workflow, FastAPI service, and LangChain/LlamaIndex
retrievers. The examples' dependencies live in the pinned development group;
they do not add framework dependencies to the SDK runtime. The SDK test
command runs their offline HTTP smoke tests as part of the repository gate.
The Sphinx site's SDK guide includes these source files directly. Its Python
reference is generated from the public SDK and its REST reference renders the committed
OpenAPI snapshot. Regenerate the Python reference with
uv --directory sdk/python run --frozen scripts/examples_reference.py --write.
The SDK suite refuses a stale committed reference. Build the site from the repository root with
poetry run sphinx-build -W -b html docs /tmp/contextbase-docs.
Canonical extraction volume
Document.extracted_char_count is the server's exact Unicode codepoint count
of canonical extracted text, including whitespace before chunking. It is
independent of the original byte size, chunk overlap, and indexing status.
A confirmed empty extraction has 0; unknown legacy extraction has null.
Python represents an absent field from an older server as UNSET (from
contextbase._generated.types), preserving it separately from None.
Neither unknown nor absent values should be treated as zero. The field is
server-owned: owner metadata with the same key does not change it.
A search or query source exposes document_id independently of locator.
It is the canonical document UUID when the server verified the returned leaf
text against its Space-scoped canonical chunk. Missing offsets do not prevent
known identity, and known identity does not manufacture a locator, page or
section. Unknown provenance remains null (older servers may omit the field).
Do not parse document identity from source id, metadata or text. Parent text
and graph context remain separate evidence; this UUID identifies the leaf.
Sources may carry source.bibliography and source.parent_bibliography even
when no verified locator is available. Their optional title is the canonical
contextual document title; author, year and url are document-owner metadata,
not extractor-verified facts or model attributions:
from contextbase import Answer
def show_bibliography(answer: Answer) -> None:
for source in answer.sources:
citation = source.bibliography
if citation is not None:
print(citation.title, citation.author, citation.year, citation.url)
The URL is an owner-supplied bibliographic display reference. Syntax checks
cannot guarantee public access or absence of secrets in its contents, and known
original/media routes are recognised only on configured backend/storage origins.
Authenticated originals remain on the generated download_document API
described above; bibliography does not grant file access. Older responses without bibliography decode as
None, and unknown metadata fields are not included in source bibliography.
Tracking document deletion
documents.delete accepts deletion; its return does not confirm physical cleanup.
For completion tracking, keep the operation ID returned by the explicit request
method. The operation remains readable by its space and ID after the document
is absent. An optional command UUID identifies a repeated deletion command.
operation_id = space.documents.request_delete(
document.id, command_id=command_id
)
status = space.documents.get_operation(operation_id)
print(status.state)
A missing or malformed operation header raises APIError after acceptance;
the client does not repeat DELETE to obtain another ID.
Document lifecycle commands
activate, deactivate and reindex accept a required caller-owned command
UUID and return DocumentOperationStatus. Keep that UUID for an explicit
replay of the same command; using it for another action is a conflict. POST
commands are never retried automatically, including after a 503 response.
from uuid import uuid4
command_id = uuid4()
operation = space.documents.deactivate(document.id, command_id=command_id)
status = space.documents.get_operation(operation.id)
The asynchronous methods take the same arguments and are awaited. Commands
need write access; reading retained status needs read access. An attached
storage document is refused with a conflict; another space's document is
masked as not found. succeeded, failed and cancelled are terminal status
values. A document's required is_active describes settled managed visibility;
accepting a queued command does not flip it. indexed describes ingestion
status independently. Existing delete and request_delete remain available.
Metadata
Release files for contextbase-sdk 2026.1.0rc1
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 | Actions |
|---|---|---|---|---|
| contextbase_sdk-2026.1.0rc1.tar.gz | 333.3 kB | Without provenance | Details for contextbase_sdk-2026.1.0rc1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| contextbase_sdk-2026.1.0rc1-py3-none-any.whl | Python 3 | none | any | Details for contextbase_sdk-2026.1.0rc1-py3-none-any.whl |
Total release size: 998.9 kB
Release files / contextbase_sdk-2026.1.0rc1.tar.gz
| Download URL | contextbase_sdk-2026.1.0rc1.tar.gz |
|---|---|
| Size | 333.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
fed3bfd8e06ed52ef39f0930310582d56d6a15ee2fef5051a7cf6664ed05c045
|
|
BLAKE2b-256 checksum How to use checksums |
16ca0f271da4aadd572214bf144ac58e479aa8fa9c790c4de83623f6ddbe1c1d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.5.18
|
Release files / contextbase_sdk-2026.1.0rc1-py3-none-any.whl
| Download URL | contextbase_sdk-2026.1.0rc1-py3-none-any.whl |
|---|---|
| Size | 665.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
093f2f654545a9844a5209286ed40d60ab8241f5fb8ae718d5cc42293a009651
|
|
BLAKE2b-256 checksum How to use checksums |
9d5baf8555a6f9ab9da4a73cc6e2a034d73b4c3f66993e61b6cdc237fcb9a070
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.5.18
|
Security for contextbase-sdk 2026.1.0rc1
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.
To learn more about protecting yourself from malicious actors, visit our package security documentation.