Unreleased documentation. These pages follow the main branch and can change before the next release. For supported guidance, use v0.43.0.
Query a registry from Python and Node
For the consumer or verifier
In about twenty minutes you will have a handful of short programs, in Python and in Node, that read the contacts you added in Create your first registry and Change a registry with an Action, add one, change one, and register one more. Each step shows both languages: follow the one your application uses, or run both against the same registry.
Get a token for your programs
Section titled “Get a token for your programs”In the project directory, request a token for the editor test caller and put it, with the
registry’s address, where your programs will read them:
bregctl dev token editorexport REGISTRY_URL=http://127.0.0.1:8090export REGISTRY_TOKEN="$(sed 's/^Authorization: Bearer //' .breg/dev/secrets/editor.header)"bregctl dev token succeeded.Use the address bregctl dev start reported if it differs. The token lasts five minutes: when a
program fails with status 401, run these three commands again in the project directory.
Install the client
Section titled “Install the client”The client’s version must match the registry’s, so read it from breg rather than typing it.
Make a directory beside the project for your programs and install the client there:
mkdir ../contacts-appcd ../contacts-appversion="$(breg --version | awk '{print $2}')"For Python, in a virtual environment:
python3 -m venv .venv. .venv/bin/activatepython -m pip install --quiet "registry-stack-client==$version"For Node:
npm install --silent "@registrystack/client@$version"Each package carries a native build for Linux amd64, Linux arm64, and macOS arm64.
Connect to the registry
Section titled “Connect to the registry”Every program starts the same way, so put that in one file. Create registry.py:
import os
from registry_client import breg
PROFILE = "directory-editor"
client = breg.BaseRegistryClient( base_url=os.environ["REGISTRY_URL"], authorization={"static": os.environ["REGISTRY_TOKEN"]},)contract = client.registry_contract(access_profile=PROFILE)Or, for Node, registry.mjs:
import { breg } from '@registrystack/client';
export const PROFILE = 'directory-editor';
export const client = new breg.BaseRegistryClient({ baseUrl: process.env.REGISTRY_URL, authorization: { static: process.env.REGISTRY_TOKEN },});export const contract = await client.registryContract(PROFILE);static sends the token unchanged with every call. directory-editor is the
access profile in registry.yaml that grants
editor its reads and changes. The contract is what the registry publishes about itself for that
profile: its name and version, and the operations and Actions the profile may use.
Read the contacts
Section titled “Read the contacts”Create read.py:
from registry import PROFILE, client, contract
print(f"{contract.registry_identifier} version {contract.registry_version}")
page = client.list_records("contacts", access_profile=PROFILE)for record in sorted(page["value"]["items"], key=lambda r: r["domainData"]["name"]): print(record["domainData"]["name"], record["domainData"]["email"])python read.pylocal-directory version 0.1.0Example contact contact@example.testMoving contact new@example.testRegistered contact registered@example.testOr create read.mjs:
import { PROFILE, client, contract } from './registry.mjs';
console.log(`${contract.registryIdentifier} version ${contract.registryVersion}`);
const page = await client.listRecords('contacts', { accessProfile: PROFILE });const byName = (a, b) => a.domainData.name.localeCompare(b.domainData.name);for (const record of page.value.items.toSorted(byName)) { console.log(record.domainData.name, record.domainData.email);}node read.mjslocal-directory version 0.1.0Example contact contact@example.testMoving contact new@example.testRegistered contact registered@example.testThe first line is the registry block of registry.yaml: its id and the version you give your
project. The others are the contacts from the first two tutorials, each a
Record of the contact
Record type. contacts is the Record type’s route, and
each item carries its fields under domainData. The list comes back in no particular order, so
the programs sort it by name.
Add a contact
Section titled “Add a contact”Create add_contact.py, which adds a test contact and prints its identifier:
from registry import PROFILE, client, contract
create = contract.select_create("records.contact.create", PROFILE)created = client.create_record( create, {"name": "Ada Lovelace", "email": "ada@example.test", "jurisdiction": "zone-a"}, "add-ada-lovelace",)print(created["value"]["data"]["recordIdentifier"])Or add_contact.mjs:
import { PROFILE, client, contract } from './registry.mjs';
const create = contract.selectCreate('records.contact.create', PROFILE);const created = await client.createRecord( create, { name: 'Ada Lovelace', email: 'ada@example.test', jurisdiction: 'zone-a' }, 'add-ada-lovelace',);console.log(created.value.data.recordIdentifier);select_create finds the create operation in the contract and fails at once if the profile may
not use it. The last argument, add-ada-lovelace, is the idempotency key: send the same request
under the same key again, after a timeout or a crash, and the registry returns the contact it
already created instead of adding a second one. Run the program and keep the identifier:
contact_id="$(python add_contact.py)"echo "$contact_id"<record-id>The Node program prints the same identifier, because it sends the same request under the same key:
node add_contact.mjs<record-id>python read.pylocal-directory version 0.1.0Ada Lovelace ada@example.testExample contact contact@example.testMoving contact new@example.testRegistered contact registered@example.testChange an email with an Action
Section titled “Change an email with an Action”The project allows one change to an existing contact: the
Action update-contact-email, which the registry applies in
the same request. Create change_email.py, which takes a contact’s identifier and a new address:
import sys
from registry_client import breg
from registry import PROFILE, client, contract
record_id, email = sys.argv[1], sys.argv[2]action = contract.select_action("update-contact-email", PROFILE)
try: record = client.get_record("contacts", record_id, access_profile=PROFILE) revision = record["value"]["data"]["revisionIdentifier"] conditions = client.action_target_conditions(action, {"contactId": record_id}) outcome = client.invoke_action( action, {"contactId": record_id, "email": email}, f"change-email-{record_id}-{revision}", conditions, )except breg.BaseRegistryClientError as error: sys.exit(f"refused: {error.kind} ({error.code})")
print(outcome["value"])Or change_email.mjs:
import { breg } from '@registrystack/client';
import { PROFILE, client, contract } from './registry.mjs';
const [recordId, email] = process.argv.slice(2);const action = contract.selectAction('update-contact-email', PROFILE);
try { const record = await client.getRecord('contacts', recordId, { accessProfile: PROFILE }); const revision = record.value.data.revisionIdentifier; const conditions = await client.actionTargetConditions(action, { contactId: recordId }); const outcome = await client.invokeAction( action, { contactId: recordId, email }, `change-email-${recordId}-${revision}`, conditions, ); console.log(outcome.value);} catch (error) { if (!(error instanceof breg.BaseRegistryClientError)) throw error; console.error(`refused: ${error.kind} (${error.code})`); process.exit(1);}The target conditions are the contact’s current state as the registry sees it. The registry makes the change only if the contact still matches them, so two programs changing the same contact at once cannot overwrite each other unseen. The idempotency key includes the contact’s revision: a retry of this change reuses it, and the next change gets a new one.
python change_email.py "$contact_id" ada@lovelace.example.test{'action': 'update-contact-email', 'applicationId': '<application-id>', 'results': {'contact-email': {'recordId': '<record-id>', 'recordType': 'contact', 'revision': 2}}}node change_email.mjs "$contact_id" ada@analytical.example.test{ action: 'update-contact-email', applicationId: '<application-id>', results: { 'contact-email': { recordId: '<record-id>', recordType: 'contact', revision: 3 } }}The registry answered each call with an ActionReceipt. It names the Action, gives the change an
applicationId, and lists under each effect of the Action’s
Plan, here the single effect contact-email, the Record it
changed and that Record’s new revision.
When the registry refuses, the client raises BaseRegistryClientError, and its kind says what
went wrong. Ask for a contact that does not exist:
python change_email.py 00000000-0000-4000-8000-000000000000 someone@example.testrefused: not-found (resource.not_found)The Node program prints the same line. kind is what a program branches on; code is the
registry’s own reason. An address the Action does not accept, such as an empty one, is refused
with invalid-request before anything is sent. The
client capabilities reference lists every operation,
and the client API reference every error kind.
Register a contact through a Proposal
Section titled “Register a contact through a Proposal”The other Action, register-contact, changes nothing when it starts. It opens a
Proposal: a draft you submit, which the registry then
applies. This project requires no review, so the registry applies it straight after the submit.
Create register.py:
import time
from registry import PROFILE, client, contract
action = contract.select_action("register-contact", PROFILE)proposal = client.start_proposal( action, {"name": "Grace Hopper", "email": "grace@example.test", "jurisdiction": "zone-a"}, "register-grace-hopper",)print(proposal.identifier, proposal.document["state"])
submit = client.prepare_proposal(proposal, "submit", {}, "submit-grace-hopper")proposal = client.execute_proposal(submit)while proposal.document["state"] != "applied": time.sleep(1) proposal = client.read_proposal(proposal)
print(proposal.document["state"], proposal.document["application"]["resultLinks"][0])print(proposal.document["links"]["cancel"]["availability"])Or register.mjs. The Node client takes the inputs and the submit body as JSON text, and returns
the Proposal’s document the same way:
import { setTimeout } from 'node:timers/promises';
import { PROFILE, client, contract } from './registry.mjs';
const action = contract.selectAction('register-contact', PROFILE);let proposal = await client.startProposal( action, JSON.stringify({ name: 'Grace Hopper', email: 'grace@example.test', jurisdiction: 'zone-a' }), 'register-grace-hopper',);let document = JSON.parse(proposal.documentJson);console.log(proposal.identifier, document.state);
const submit = client.prepareProposal(proposal, 'submit', '{}', 'submit-grace-hopper');proposal = await client.executeProposal(submit);document = JSON.parse(proposal.documentJson);while (document.state !== 'applied') { await setTimeout(1000); proposal = await client.readProposal(proposal); document = JSON.parse(proposal.documentJson);}
console.log(document.state, document.application.resultLinks[0]);console.log(document.links.cancel.availability);python register.py<proposal-id> draftapplied {'recordId': '<record-id>', 'recordType': 'contact', 'revision': 1}{'available': False, 'observedRevision': 3, 'reason': 'already-applied', 'recovery': 'receipt-only'}The Proposal started as a draft. After the submit, the program reads it until the registry has
applied it, then prints the Record the registry created. The last line is the availability of the
Proposal’s cancel link. A Proposal lists the moves it allows, each with an availability: whether
the move is open now and, if not, the reason and the recovery, what you can still do.
receipt-only means the Proposal is finished and only its record of what happened remains.
node register.mjs<proposal-id> draftapplied { recordType: 'contact', recordId: '<record-id>', revision: 1}{ available: false, observedRevision: 3, reason: 'already-applied', recovery: 'receipt-only'}The Node program used the same keys, so the registry handed back the same Proposal rather than opening a second one. List the contacts once more:
python read.pylocal-directory version 0.1.0Ada Lovelace ada@analytical.example.testExample contact contact@example.testGrace Hopper grace@example.testMoving contact new@example.testRegistered contact registered@example.test