By the end of this page you will have created a local database, defined one template, stored one resource, and read it back with a query.
Everything here runs against a local SQLite file. No server required.
1. Create a client and a namespace
from recap import RecapClient
client = RecapClient.from_sqlite("quickstart.db")
client.create_namespace("beamline")
client.create_namespace("beamline/amx", metadata={"beamline": "amx"})
namespace = client.namespace("beamline/amx")
print(namespace.namespace_path)
from_sqlite() creates the database file and applies any pending migrations.
Namespaces are hierarchical, so beamline must exist before beamline/amx. client.namespace(path) returns a view of the client scoped to that path; every template, resource, and query made through namespace now belongs to beamline/amx without your having to repeat the path.
2. Define a resource template
with namespace.build_resource_template(
name="Sample Plate",
type_names=["container", "plate"],
) as template:
template.add_properties({
"dimensions": [
{"name": "rows", "type": "int", "default": 8},
{"name": "columns", "type": "int", "default": 12},
]
})
This declares the shape every sample plate will have: a property group called dimensions holding two integers with defaults.
The with block is doing real work. Builders collect your changes and validate them as a single unit, then save once when the block exits cleanly. If the block raises, nothing is written.
3. Create a resource
with namespace.build_resource(
name="Sample Plate 001",
template_name="Sample Plate",
) as builder:
builder.resource.properties["dimensions"].values["rows"] = 16
builder.finalize()
This plate takes the template’s default of 12 columns but overrides rows to 16.
finalize() is required if you want to find this resource later. It marks the resource active. Until then it stays in a mutable draft state, and queries skip it by design, so half-built records never show up in results.
4. Query it back
plate = (
namespace.query_maker()
.resources()
.filter(name="Sample Plate 001")
.first()
)
print(plate.name)
print(plate.properties.dimensions.rows.value)
print(plate.properties.dimensions.columns.value)
The overridden value and the inherited default both come back.
Note the trailing .value. Each stored attribute is a small wrapper that also carries the unit, so plate.properties.dimensions.rows is the wrapper and .value is the number inside it:
rows = plate.properties.dimensions.rows
print("printed: ", rows)
print("rows == 16: ", rows == 16)
print("rows.value: ", rows.value)
print("rows.value==16:", rows.value == 16)
printed: 16
rows == 16: False
rows.value: 16
rows.value==16: True
The wrapper prints like its value, so a bare print() can look correct while a comparison silently fails. Reach for .value whenever you want the number or string itself.
Step parameters work exactly the same way. For every supported read and write form, see Property and Parameter Access.