Import & Export
Bundle Format (v0.6)
The registry uses a YAML/JSON bundle format for import and export. A bundle encodes a full community graph:
version: "1.0"
schema_version: "0.6"
community:
id: example_rec
name: Example REC
description: A renewable energy community
legal:
name: "Example REC"
vat: "IT0123456789"
legal_form: "APS"
links:
website: "https://example.org"
contact:
email: "info@example.org"
settings:
timezone: "Europe/Rome"
currency: "EUR"
operators:
example_dso:
name: Example DSO
country: IT
areas:
northern:
name: northern
topology:
- "AC221E00020"
topology:
- id: "PS-001"
type: primary_substation
name: "Primary Substation"
operator_id: example-dso
members:
member-001:
user_id: alice
# Optional. Minted a step after the member is registered, so it is usually
# written by `PATCH` at runtime rather than authored here — and omitted from
# an export entirely when the member has none.
did: "did:web:dataspace.example%3A30005:alice"
name: Alice Rossi
role: prosumer
area: northern
status: active
type: "schema:Person"
delivery_points:
- id: "IT001E00001234"
type: withdrawal
active: true
assets:
pv:
- key: pv-001
name: Rooftop PV
capacity_kwp: 5.0
meter:
- key: meter-001
name: Main Meter
sensor_id: "SEN-001"
meter_type: bidirectional
pod: "IT001E00001234"
Key differences from v0.4:
- members is a dict keyed by member key (not a list)
- Assets are nested under members as typed dicts (assets.pv, assets.meter, assets.storage, etc.)
- Community has areas, topology, legal, links, contact, settings, operators
- Schema version is "0.6" with bundle version "1.0"
v0.6 adds member.did and changes nothing else, so a v0.5 file is a valid v0.6 one. See
schemas/community/v0.6/README.md for the field, and
note that import reports an unmatched schema_version rather than refusing it — restoring a
backup is the path a refusal would break.
Import
JSON Import — POST /admin/import
Import a single community from a JSON request body. Full replace: deletes the existing community and all related entities, then recreates from the bundle. Atomic operation.
curl -X POST http://localhost:8004/admin/import \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d @community-bundle.json
YAML Import — POST /admin/import/yaml
Import one or more communities from a YAML multidocument body. Each YAML document is a separate community bundle.
curl -X POST http://localhost:8004/admin/import/yaml \
-H "Authorization: Bearer <token>" \
-H "Content-Type: text/yaml" \
--data-binary @communities.yaml
CLI Import
celine-rec-registry import --file recs/rec-example.yaml
# or via taskfile:
task import:community -- --file recs/rec-example.yaml
The CLI authenticates via client credentials or password grant (configurable via flags).
Export
API Export — GET /admin/export
Exports all communities as a YAML multidocument.
curl "http://localhost:8004/admin/export" \
-H "Authorization: Bearer <token>" \
-o communities.yaml
CLI Export
celine-rec-registry export --community example_rec -o export.yaml
Replace Semantics
Import is a full replace operation:
- The existing community graph (community + all members + all assets) is deleted.
- The new graph from the bundle is created.
- The operation is atomic — if any step fails, the entire import is rolled back.
Missing entities in the new bundle are deleted. Changed entities are recreated. New entities are added.
The force guard
Because step 1 deletes everything, and because members now arrive at runtime
through the member API, restoring a stale export is the likeliest way to lose
weeks of approvals. So an import naming a community that already exists is
refused unless force is set:
# Refused, naming what would have gone
celine-rec-registry import --file rec.yaml
# Refused: Community 'example_rec' already exists with 17 member(s) and 33
# asset(s); importing would delete them.
# See the effect first — this is how you decide whether force is warranted
celine-rec-registry import --file rec.yaml --dry-run
# Accept it
celine-rec-registry import --file rec.yaml --force
Over HTTP the same guard answers 409, with force as a body field on
POST /admin/import and a query parameter on POST /admin/import/yaml. A
dry_run is never blocked — reporting the counts is exactly how a caller judges
whether to force.
Creating a community that does not exist yet needs no force.
Seeding versus changing
A community is seeded from a bundle and changes through the member API
(POST /admin/communities/{key}/members and its sub-resources). Both paths build
the same rows — they share src/celine/rec_registry/services/members.py — so an export taken after
runtime changes re-imports to the same state. That property is pinned by
tests/test_writes.py::TestRoundTrip; if it ever breaks, the two write paths
have diverged and a re-import will quietly revert live data.
The practical rule: the file is a seed, GET /admin/export is a backup,
and once members arrive at runtime the database is the source of truth.
Idempotency
Import is idempotent: importing the same bundle twice produces the same state
(with force, since the second run is by definition a replacement). Use exports
as authoritative backups and re-import to restore.