Skip to content
Registry StackDocsv0.25.0

Query Registry Relay with Python

For the consumer or verifier

View as Markdown

The Registry Relay Python client gives a consumer or verifier one synchronous method for each fixed Relay V2 operation. In this tutorial you will install a released wheel, read one synthetic business record, advance discovery only when Relay returns a continuation, revalidate OpenAPI with a strong entity tag, and inspect a governed refusal.

Outcome
A Python program that reads the synthetic registered-business Relay and handles an optional continuation, caching, and errors explicitly.
Time
About 10 minutes after the publishing prerequisite
Level
Python consumer
Prerequisites
Python 3.10 or laterA shell with curlThe synthetic registered-business Relay running on 127.0.0.1:8080

Complete Publish a governed SQLite registry through the Serve it step. Leave Relay running and open a second shell. The resulting deployment is anonymous, contains synthetic records only, and listens at http://127.0.0.1:8080.

That second shell also needs the relay binary on its path. The install below reads the exact release version from it rather than asking you to type one.

Prebuilt Relay client packages start with Registry Stack v0.20.0. The running Relay must therefore be v0.20.0 or later. Install the client from that exact Relay release, using the wheel that matches this machine.

Work in a directory of its own, so the wheel and the virtual environment stay out of the registry project you built in the prerequisite:

Terminal window
mkdir relay-python-query
cd relay-python-query
python3 -m venv .venv
. .venv/bin/activate

Now read the release version from the running Relay, select the wheel for this machine, and install it. The release workflow attaches Python wheels to the matching GitHub Release, so the download comes from that release rather than from PyPI:

Terminal window
VERSION="$(relay --version | awk '{print $2}' | sed 's/^v//')"
case "$VERSION" in
*-dev*) echo "This relay was built from source ($VERSION), not installed from a release" >&2; exit 1 ;;
esac
case "$(uname -s)-$(uname -m)" in
Linux-x86_64) WHEEL_PLATFORM="linux_x86_64" ;;
Linux-aarch64|Linux-arm64) WHEEL_PLATFORM="linux_aarch64" ;;
Darwin-arm64) WHEEL_PLATFORM="macosx_11_0_arm64" ;;
*) echo "No prebuilt Relay client wheel for this platform" >&2; exit 1 ;;
esac
WHEEL="registry_relay_client-${VERSION}-cp310-abi3-${WHEEL_PLATFORM}.whl"
curl -fLO "https://github.com/registrystack/registry-stack/releases/download/v${VERSION}/${WHEEL}"
python -m pip install "./${WHEEL}"

The wheel uses the Python 3.10 stable ABI, so supported newer Python versions import the same file. The three platform names in that case are the whole set: the release provides no Windows or Intel macOS wheel.

The first case stops a build that came from source rather than from a release. Such a build reports a -dev version, which names no published release and so has no wheel to download.

Create query_relay.py. It constructs a client against the running deployment and reads one record:

import json
from registry_relay_client import RelayClient
client = RelayClient(base_url="http://127.0.0.1:8080")
result = client.read_record("registered-business", "BIZ-0001")
print(result["kind"])
print(result["value"]["data"]["recordIdentifier"])
print(json.dumps(result["value"]["data"]["domainData"], sort_keys=True))

Run it:

Terminal window
python query_relay.py
complete
BIZ-0001
{"legalForm": "COOPERATIVE", "legalName": "Aurora Freight Cooperative"}

complete distinguishes a returned representation from a cache revalidation response. The record envelope stays a plain Python mapping, so reading it is ordinary dictionary access and the client carries no schema for your records. domainData holds only the two fields the synthetic Relay contract discloses, not the registered address and registrar note stored beside them in the same row.

Pass fields=["legalName"] to read_record to narrow that response further. Narrowing is the only direction available: a caller cannot use fields to widen the contract’s disclosure profile. Read the Relay client API reference before adding authentication or private certificate roots.

Relay returns a continuation only when a further page exists, so a caller has to branch on its presence instead of assuming it. Append this discovery loop to query_relay.py, then run the file again:

page = client.resources(page_size=1)
while True:
for resource in page["value"]["items"]:
print(resource["resourceIdentifier"])
continuation = page["continuation"]
if continuation is None:
break
page = client.continue_resources(continuation)

The run ends with one new line:

registered-business

This deployment has one resource, so its first page has no continuation and the loop breaks on the first pass. On a deployment with more resources, continuation is exactly {"cursor": "<opaque-cursor>"}. Pass that returned mapping unchanged to continue_resources. Do not extract its cursor, rebuild the mapping, or add first-page options.

Record-list and search pages use route-bound continuation mappings. Hand those unchanged to continue_list_records or continue_search, respectively. The client never advances a page on its own.

If you persist a continuation between runs, persist the complete returned mapping. The matching continuation method validates it again, so a resource, record-list, or search continuation cannot be substituted for another route.

Revalidate OpenAPI with a strong entity tag

Section titled “Revalidate OpenAPI with a strong entity tag”

The OpenAPI document is cacheable, which lets a second request ask Relay whether the copy you already hold is still current. Append this conditional request and run the file:

first = client.openapi()
print(first["kind"])
print(first["etag"] is not None)
second = client.openapi(etag=first["etag"])
print(second["kind"])
print(second["etag"] == first["etag"])

The run ends with four new lines:

complete
True
not_modified
True

The first response carries raw OpenAPI bytes and a validated strong ETag. The second call sends that tag as If-None-Match. Relay answers 304 Not Modified, and the binding returns a not_modified outcome with the echoed tag and trace identifier instead of a body. Nothing in that second outcome carries the document, so a caller that discarded the first body has nothing left to serve.

Store the complete response body and ETag together, then reuse the stored body only when the next outcome is not_modified and its ETag matches. Revalidation stays explicit for every cacheable method: the client sends If-None-Match only on a call where you passed etag.

A record identifier the register does not hold is a refusal, not an empty result. Append this request for a missing synthetic record and run the file:

from registry_relay_client import RelayClientError
try:
client.read_record("registered-business", "BIZ-9999")
except RelayClientError as error:
print(error.kind)
print(error.status)
print(error.code)

The run ends with three new lines:

problem
404
consultation.unresolved

A valid Relay Problem becomes RelayClientError with stable, value-free attributes. The client does not include response bodies, request selectors, credentials, URLs, or header values in the error. Only a registered 429 Problem can carry retry_after_seconds.

Branch on kind first, then use status and code when they are present. Treat trace_id as the correlation value to give an operator. Decide at your application boundary whether a request is safe to repeat, because the client performs no automatic retries.

Leave the virtual environment and remove the tutorial directory:

Terminal window
deactivate
cd ..
rm -rf relay-python-query

Stop Relay with Ctrl+C in its shell if it was running only for this tutorial. The synthetic registry project remains available for later Relay exercises.

  • A synchronous Python consumer that delegates Relay routing, response validation, and bounded body handling to the canonical Rust client.
  • An explicit branch that passes an optional continuation unchanged when Relay returns one, with no implicit pagination.
  • A strong-ETag revalidation path that distinguishes complete and 304 outcomes.
  • A refusal path that uses stable error facts without exposing request or credential values.
SymptomCauseFix
GitHub returns 404 for the wheelThe running Relay is older than v0.20.0, its exact release is not published, or the platform name is wrongUse a published v0.20.0 or later Relay, then download the wheel from that exact release for one of the three platforms in this tutorial.
built from source, not installed from a releaserelay --version reports a -dev version, which every build outside the release workflow doesInstall relay with the installer in the prerequisite, or download a client wheel from whichever release you want a client for.
No matching distribution foundThe wheel filename or local path does not match the downloaded assetKeep the original release filename and install that exact local file.
Connection refusedThe synthetic Relay is not listening on port 8080Return to the publishing tutorial, start Relay, and leave it running in its shell.
configuration at constructionThe base URL is not HTTPS or loopback HTTP, or it contains credentials, a query, or a fragmentUse http://127.0.0.1:8080 for this local deployment.
protocol during a responseRelay returned a response outside the fixed media type, trace, ETag, Problem, or body contractRecord trace_id when present and inspect the Relay operator logs. Do not parse the rejected body in application code.
A repeated call returns not_modifiedThe supplied strong ETag still identifies the current representationReuse the body stored with that same ETag.