Quick Start

Create a database, define a template, store a resource, and query it back in about five minutes.

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)
beamline/amx

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)
Sample Plate 001
16
12

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.

client.close()

What you just built

A namespace, a template, one resource, and a query that found it. That is the whole create-and-read loop.

Missing so far is the part RECAP exists for: recording a process that consumes and produces resources, which is what turns isolated records into a provenance chain.


Next: record your first provenance chain.

Related how-to guides: Query Resources, Create a Resource Template, Edit Resource Properties.