Skip to content

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:

  1. The existing community graph (community + all members + all assets) is deleted.
  2. The new graph from the bundle is created.
  3. 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.