Registry stack documentation: machine-readable Markdown.
Index of all pages: https://docs.registrystack.org/llms.txt
Full corpus: https://docs.registrystack.org/llms-full.txt

# Query Registry Relay with Python

> Install the thin Python client, read a synthetic business record, handle an optional continuation, revalidate a document, and handle a Relay refusal.

import QuickstartMeta from '../../../components/QuickstartMeta.astro';

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.

<QuickstartMeta
  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 later', 'A shell with curl', 'The synthetic registered-business Relay running on 127.0.0.1:8080']}
/>

## Before you start

Complete [Publish a governed SQLite registry](../publish-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.

## Install the client and read a record

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:

```sh
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:

```sh
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:

```python
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:

```sh
python query_relay.py
```

```text
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](../../reference/relay-client-api/) before adding authentication or
private certificate roots.

## Handle an optional continuation exactly

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:

```python
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:

```text
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

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:

```python
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:

```text
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`.

## Handle a Relay refusal

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:

```python
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:

```text
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.

## Clean up

Leave the virtual environment and remove the tutorial directory:

```sh
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.

## What you built

- 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.

## Troubleshooting

| Symptom | Cause | Fix |
| --- | --- | --- |
| GitHub returns `404` for the wheel | The running Relay is older than v0.20.0, its exact release is not published, or the platform name is wrong | Use 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 release` | `relay --version` reports a `-dev` version, which every build outside the release workflow does | Install `relay` with the installer in the prerequisite, or download a client wheel from whichever release you want a client for. |
| `No matching distribution found` | The wheel filename or local path does not match the downloaded asset | Keep the original release filename and install that exact local file. |
| `Connection refused` | The synthetic Relay is not listening on port 8080 | Return to the publishing tutorial, start Relay, and leave it running in its shell. |
| `configuration` at construction | The base URL is not HTTPS or loopback HTTP, or it contains credentials, a query, or a fragment | Use `http://127.0.0.1:8080` for this local deployment. |
| `protocol` during a response | Relay returned a response outside the fixed media type, trace, ETag, Problem, or body contract | Record `trace_id` when present and inspect the Relay operator logs. Do not parse the rejected body in application code. |
| A repeated call returns `not_modified` | The supplied strong ETag still identifies the current representation | Reuse the body stored with that same ETag. |

## Next

- [Read the Relay client API reference](../../reference/relay-client-api/) for every Rust, Python,
  and Node operation and outcome shape.
- [Author a Registry Relay project](../../configure/relay/) to add list, lookup, search, artifact,
  and SDMX operations to a deployment.
- [Review errors and status codes](../../reference/errors/) before mapping failures into an
  application-facing API.