Skip to content

Example scripts

Each script uses sync Nexus(), creates its own project, and generates any local sample data it needs. Quick start builds a tiny XDMF case tree in a temp dir; full OpenFOAM trees use shared/data/openfoam/ (openfoam_data.py).

The three connector scripts are the exception on both counts. They need a connector an administrator already registered, so they read it from the environment (see Before you start) instead of generating fixtures, and they all use AsyncNexus: connector inventory and transfer both poll a remote job, so that whole path is async-only.

Topic Reference
Quick start quick_start.py
Upload modes and plan/apply upload.py
Item navigation items.py
Annotations and ItemSelect annotations.py
Recipe spec authoring and validation transform.py
Recipe create / conflict / revise recipe_lifecycle.py
Browse admitted CaseSet case_set.py
Admit existing collection cases existing_collection_case_grouping.py
Collection Parquet upload and read parquet.py
Catalog asset ref and batch preview catalog.py
Live catalog Flight reads catalog_stream.py
Pandas / PyTorch collection adapters adapters.py
DatasetSource collection vs catalog dataset.py
Connector import end to end connector_import.py
Inventory scope, reuse, and replay connector_inventory.py
Connector types, registration, tests connector_admin.py
Shared environment lookup connector_env.py
Shared case schemas schemas.py

Quick start: quick_start.py

"""Getting Started path: upload cases → inspect → recipe → pandas DataFrame.

Requires ``miura-nexus[pandas,sigma]`` and a live Nexus deployment.
"""

from __future__ import annotations

from datetime import datetime, timezone
from pathlib import Path
import sys
from tempfile import TemporaryDirectory

from miura import Nexus
from miura.adapters.dataframe import to_dataframe
from miura.display import print_upload_result
from miura.sdk import LocalDataSource, RecipeSpec, Schema
from miura.sdk.sigma import sigma

_examples = Path(__file__).resolve().parent
if str(_examples) not in sys.path:
    sys.path.insert(0, str(_examples))

from schemas import xdmf_case_schema, xdmf_case_structure


def _xdmf_stub(*, param_value: float, param_name: str = "param_1") -> str:
    # GlobalExtractionSchema / ExtractGlobal reads XDMF Information keys.
    return f"""<?xml version="1.0" ?>
<Xdmf Version="3.0">
  <Domain>
    <Grid Name="grid" GridType="Uniform">
      <Topology TopologyType="Triangle" NumberOfElements="1">
        <DataItem Dimensions="1 3" NumberType="Int" Format="XML">0 1 2</DataItem>
      </Topology>
      <Geometry GeometryType="XYZ">
        <DataItem Dimensions="3 3" NumberType="Float" Precision="8" Format="XML">
          0 0 0
          1 0 0
          0 1 0
        </DataItem>
      </Geometry>
    </Grid>
  </Domain>
  <Information Name="{param_name}" Value="{param_value}"/>
</Xdmf>
"""


def write_xdmf_cases(root: Path) -> None:
    for index, value in enumerate((1.02, 0.97), start=1):
        case_dir = root / f"case_{index:06d}"
        case_dir.mkdir(parents=True, exist_ok=True)
        (case_dir / "field.xdmf").write_text(
            _xdmf_stub(param_value=value),
            encoding="utf-8",
        )


def main() -> None:
    run_id = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")

    with TemporaryDirectory(prefix="epico-quickstart-") as tmp:
        data_path = Path(tmp)
        write_xdmf_cases(data_path)

        with Nexus() as nexus:
            project = nexus.create_project(f"epico-quickstart-{run_id}")
            source = project.create_collection("cases", schema=xdmf_case_schema())
            target = project.create_collection("tables", schema=Schema.permissive())

            # upload() plans then applies; use source.plan(...) to inspect gates first
            structure = xdmf_case_structure()
            upload = source.upload(
                LocalDataSource(data_path),
                case_grouping=structure.case_grouping(),
            )
            print_upload_result(upload)

            # Browse the admitted cases and the logical assets they published
            case_set = source.case_set(upload.cases)
            try:
                case_set.get_projection()
            except Exception:
                case_set.materialize_projection()
            cases = case_set.list_cases()
            assets = case_set.list_logical_assets()
            print(f"cases: {len(cases.items)}")
            for case in cases.items:
                print(f"  {case.case_group_id}: {dict(case.coordinates)}")
            print(f"logical_assets: {len(assets.items)}")
            for asset in assets.items:
                print(f"  {asset.role} → {asset.modality_id}")

            # Extract param_1 from each case's XDMF Information entry
            spec = RecipeSpec.from_sigma_dataset_generator(
                source=source.name,
                target=target.name,
                dataset_generator=sigma.DataSetGenerator(
                    sigma.GlobalExtractionSchema(
                        sealed_algorithm={"global_param_name": "param_1"},
                    )
                ),
                writer="parquet_tabular",
            )
            recipe = project.deploy_recipe(
                "cases-to-tables",
                spec,
                description="Quick start cases to tables",
                cases=upload.cases,  # pin to the admitted cases from the upload
            )
            recipe.run().wait()

            df = to_dataframe(target, limit_rows=10)
            print(df.head())


if __name__ == "__main__":
    main()

Upload modes and plan/apply: upload.py

from __future__ import annotations

from datetime import datetime, timezone
from pathlib import Path
import sys
from tempfile import TemporaryDirectory

from miura import Nexus
from miura.display import print_ingestion_plan, print_items, print_upload_result
from miura.nexus.case_grouping import CaseGrouping
from miura.sdk import LocalDataSource, UploadMode

_examples = Path(__file__).resolve().parent
if str(_examples) not in sys.path:
    sys.path.insert(0, str(_examples))

from openfoam_data import transport_properties, write_case
from schemas import openfoam_case_schema


def write_demo_tree(root: Path, *, version: int) -> None:
    write_case(root / "case_000001", case_index=1)
    if version >= 2:
        write_case(root / "case_000002", case_index=2)


def main():
    run_id = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S-%f")

    with TemporaryDirectory(prefix="epico-upload-") as tmp:
        data_root = Path(tmp)
        write_demo_tree(data_root, version=1)

        with Nexus() as nexus:
            project = nexus.create_project(f"epico-upload-{run_id}")

            collection = project.create_collection(
                collection_name="upload-demo",
                schema=openfoam_case_schema(),
            )
            datasource = LocalDataSource(data_root)

            print("Step 1: Plan a replace upload")
            replace_plan = collection.plan(datasource, mode=UploadMode.REPLACE)
            print_ingestion_plan(replace_plan, data_root=datasource.path)

            print("\nStep 1b: Optional case grouping on the same plan")
            # Folder-per-case depth under the collection root; omit when you only
            # need files in the collection without admitting cases.
            grouping = CaseGrouping.root_path("/", 1)
            grouped_plan = collection.plan(
                datasource,
                mode=UploadMode.REPLACE,
                case_grouping=grouping,
            )
            print(
                f"  gate={grouped_plan.gate_status} "
                f"admissible={grouped_plan.is_admissible} "
                f"cases={grouped_plan.cases is not None}"
            )

            print("\nStep 2: Apply the plan")
            replace_result = replace_plan.apply()
            print_upload_result(replace_result)

            print("\nStep 3: Add another case with UPSERT")
            write_demo_tree(data_root, version=2)
            upsert_result = collection.upload(datasource, mode=UploadMode.UPSERT)
            print_upload_result(upsert_result)

            print("\nStep 4: Inspect uploaded files")
            items = collection.list_items(
                path=None,
                include_dirs=False,
                sort_by="item_uri",
            )
            print_items(items, max_items=20)

            print("\nStep 5: Append a new case folder with INSERT")
            write_case(data_root / "case_000003", case_index=3)
            insert_plan = collection.plan(datasource, mode=UploadMode.INSERT)
            print_ingestion_plan(insert_plan, data_root=datasource.path)
            if insert_plan.is_admissible and insert_plan.requires_execution:
                insert_result = insert_plan.apply()
                print_upload_result(insert_result)

            print("\nStep 6: Refresh an existing file with UPDATE")
            transport_path = data_root / "case_000001" / "constant" / "transportProperties"
            transport_path.write_text(
                transport_properties(nu="2.5e-05"),
                encoding="utf-8",
            )
            update_plan = collection.plan(datasource, mode=UploadMode.UPDATE)
            print_ingestion_plan(update_plan, data_root=datasource.path)
            if update_plan.is_admissible and update_plan.requires_execution:
                update_result = update_plan.apply()
                print_upload_result(update_result)

            print("\nAvailable ingestion modes:")
            for mode in (
                UploadMode.INSERT,
                UploadMode.UPDATE,
                UploadMode.UPSERT,
                UploadMode.REPLACE,
            ):
                print(f"  {mode.value}")


if __name__ == "__main__":
    main()

Item navigation: items.py

from __future__ import annotations

from datetime import datetime, timezone
from pathlib import Path
import sys

from miura import Nexus
from miura.display import print_items, print_upload_result
from miura.sdk import LocalDataSource, UploadMode

_examples = Path(__file__).resolve().parent
if str(_examples) not in sys.path:
    sys.path.insert(0, str(_examples))

from schemas import openfoam_case_schema
from openfoam_data import openfoam_data_root


def take(source, limit):
    rows = []
    for row in source:
        rows.append(row)
        if len(rows) >= limit:
            break
    return rows


def print_uris(label, items):
    print(label)
    for item in items:
        suffix = "/" if item.is_folder and not item.item_uri.endswith("/") else ""
        print(f"  {item.item_uri}{suffix}")


def main():
    examples_root = Path(__file__).resolve().parent
    data_path = openfoam_data_root(examples_root)
    run_id = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S-%f")

    with Nexus() as nexus:
        project = nexus.create_project(f"epico-items-{run_id}")

        print("Step 1: Create a collection and upload OpenFOAM cases")
        collection = project.create_collection(
            collection_name="openfoam-items",
            schema=openfoam_case_schema(),
        )
        upload_result = collection.upload(
            LocalDataSource(data_path),
            mode=UploadMode.REPLACE,
        )
        print_upload_result(upload_result)

        print("\nStep 2: List case folders at the root (one level)")
        cases = collection.list_items(path="/", include_dirs=True, sort_by="item_uri")
        print_uris("Top-level items:", cases[:5])

        first_folder = next((item for item in cases if item.is_folder), None)
        if first_folder is None:
            print("No folder found under root.")
            return

        print("\nStep 3: Navigate down with .children")
        folder_children = take(first_folder.children, limit=5)
        print_uris(f"Children of {first_folder.item_uri}:", folder_children)

        first_file = next(
            (item for item in folder_children if not item.is_folder), None
        )
        if first_file is None:
            for child in folder_children:
                if not child.is_folder:
                    continue
                nested = take(child.children, limit=5)
                first_file = next((item for item in nested if not item.is_folder), None)
                if first_file is not None:
                    break
        if first_file is None:
            first_file = next(
                collection.walk_items(path=first_folder.item_uri, include_dirs=False),
                (None, None),
            )[0]
        if first_file is None:
            print(f"No file found under {first_folder.item_uri}.")
            return

        print("\nStep 4: Navigate up with .parent")
        parent = first_file.parent
        print(f"Item: {first_file.item_uri}")
        print(f"Parent: {parent.item_uri if parent else None}")

        print("\nStep 5: Navigate sideways with .siblings")
        siblings = take(first_file.siblings, limit=5)
        print_uris(f"Siblings of {first_file.item_uri}:", siblings)

        print("\nStep 6: Fetch the same item directly by path")
        same_item = collection.get_item(first_file.item_uri)
        print(f"Direct lookup: {same_item.item_uri if same_item else None}")

        print("\nStep 7: Walk all files under the collection")
        for item, depth in take(collection.walk_items(path="/", include_dirs=False), limit=5):
            indent = "  " * depth
            print(f"{indent}{item.item_uri} ({item.file_size} B)")

        print("\nStep 8: Climb toward the root from one file")
        current = first_file
        while current is not None:
            print(f"  {current.item_uri}")
            current = current.parent


if __name__ == "__main__":
    main()

Annotations and ItemSelect: annotations.py

from __future__ import annotations

from datetime import datetime, timezone
from pathlib import Path
import sys

from miura import Nexus
from miura.api.annotations import ItemAttributes, ItemSelect
from miura.display import print_items, print_upload_result
from miura.sdk import LocalDataSource, UploadMode

_examples = Path(__file__).resolve().parent
if str(_examples) not in sys.path:
    sys.path.insert(0, str(_examples))

from schemas import openfoam_case_schema
from openfoam_data import openfoam_data_root


def first_files(collection, limit):
    files = []
    for item in collection.iter_items(
        path=None,
        include_dirs=False,
        sort_by="item_uri",
        page_size=20,
    ):
        files.append(item)
        if len(files) >= limit:
            break
    return files


def main():
    examples_root = Path(__file__).resolve().parent
    data_path = openfoam_data_root(examples_root)
    run_id = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S-%f")

    with Nexus() as nexus:
        project = nexus.create_project(f"epico-annotations-{run_id}")

        print("Step 1: Create a collection and upload OpenFOAM cases")
        collection = project.create_collection(
            collection_name="openfoam-annotations",
            schema=openfoam_case_schema(),
        )
        upload_result = collection.upload(
            LocalDataSource(data_path),
            mode=UploadMode.REPLACE,
        )
        print_upload_result(upload_result)

        files = first_files(collection, limit=6)
        if not files:
            print("No files found to annotate")
            return

        print("\nStep 2: Add attributes to one item")
        baseline = files[0]
        baseline.set_attributes(
            split="train",
            labels={"solver": "simpleFoam", "stage": "baseline"},
            annotations={"note": "single item update"},
        )
        print(f"Tagged one item: {baseline.item_uri}")

        print("\nStep 3: Add attributes to several items at once")
        batch_rows = []
        for index, item in enumerate(files[1:]):
            split = "train" if index < 3 else "val"
            stage = "baseline" if index % 2 == 0 else "candidate"
            batch_rows.append(
                ItemAttributes(
                    item_id=item.uuid,
                    split=split,
                    labels={"solver": "simpleFoam", "stage": stage},
                    annotations={"note": f"batch update {index + 1}"},
                )
            )

        result = collection.set_item_attributes(batch_rows)
        print(
            "Updated "
            f"{result.get('success_count', 0)} of {result.get('total_count', len(batch_rows))} items"
        )

        print("\nStep 4: Read attributes back from one item")
        attributes = collection.get_item_attributes(baseline.uuid)
        print(attributes)

        print("\nStep 5: Filter items with ItemSelect")
        train_baseline = collection.list_items(
            include_dirs=False,
            sort_by="item_uri",
            select=ItemSelect(
                split="train",
                labels={"solver": "simpleFoam", "stage": "baseline"},
            ),
        )
        print_items(train_baseline, style="table", show_attributes=True)

        print("\nStep 6: Count selected items")
        train_count = collection.count_items(select=ItemSelect(split="train"))
        baseline_count = collection.count_items(
            select=ItemSelect(labels={"stage": "baseline"})
        )
        print(f"Train files: {train_count}")
        print(f"Baseline files: {baseline_count}")

        print("\nStep 7: Clear one field")
        collection.clear_item_attributes(baseline.uuid, keys=["annotations"])
        refreshed = collection.get_item_attributes(baseline.uuid)
        print(refreshed)


if __name__ == "__main__":
    main()

Recipe spec authoring and validation: transform.py

"""Build and validate transformation specs (converter and dataset generator).

Section 3 execution-layer demo: RecipeSpec authoring + ``validate_recipe``.
Pin admitted cases at ``deploy_recipe(..., cases=upload.cases)`` — not via a raw
``CaseGrouping`` on the spec. For deploy / conflict / update / run see
``recipe_lifecycle.py`` and ``3_describe_transformation.md``.
"""

from __future__ import annotations

from datetime import datetime, timezone

from miura import Nexus
from miura.display import print_capabilities, print_json, print_validation_result
from miura.nexus.recipe.sigma_converter import (
    RemoteSigmaConverter,
    SigmaClassRef,
    SigmaSetMethod,
)
from miura.nexus.recipe.sigma_handoff import TransformEntry, default_multipart_capability
from miura.sdk import Schema
from miura.sdk.exceptions import MissingOptionalDependencyError, ValidationError
from miura.sdk.recipe import (
    CaseAdmissionPolicy,
    CaseSelection,
    RecipeSpec,
)


def simple_vtk_converter() -> RemoteSigmaConverter:
    return RemoteSigmaConverter(
        required_extensions=[".vtk"],
        reader=SigmaClassRef(class_name="vtkDataSetReader"),
        writer=SigmaClassRef(class_name="vtkXdmfWriter"),
    )


def mapdl_thermal_converter() -> RemoteSigmaConverter:
    return RemoteSigmaConverter(
        required_extensions=[".rth"],
        reader=SigmaClassRef(
            class_name="vtkPymapdlReader",
            set_methods={
                "SetReaderProfile": SigmaSetMethod(args=("semantic_temperature",)),
            },
        ),
        writer=SigmaClassRef(
            class_name="vtkXdmfWriter",
            set_methods={
                "SetMeshStaticOverTime": SigmaSetMethod(args=(True,)),
            },
        ),
        custom_algorithm=SigmaClassRef(
            class_name="SplitHomogeneousMeshBlocks",
            options={"cell_types": [12]},
        ),
    )


def create_dataset_generator():
    from miura.sdk.sigma import sigma

    return sigma.DataSetGenerator(
        sigma.GlobalExtractionSchema(
            sealed_algorithm={"global_param_name": "param_1"},
        )
    )


def validate(project, label: str, spec: RecipeSpec) -> None:
    print(f"\nValidating {label}")
    print_validation_result(project.validate_recipe(spec))


def main():
    run_id = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S-%f")

    with Nexus() as nexus:
        project = nexus.create_project(f"epico-transform-{run_id}")
        schema = Schema.permissive()
        source_vtk = project.create_collection("source-vtk", schema=schema)
        target_xdmf = project.create_collection("target-xdmf", schema=schema)
        target_parquet = project.create_collection("target-parquet", schema=schema)

        print("Step 1: Transform files into other files (converter)")
        converter_spec = RecipeSpec.from_sigma_converter(
            source=source_vtk.name,
            target=target_xdmf.name,
            converter=simple_vtk_converter(),
        )
        print_json(
            converter_spec.to_payload()["execution_payload"]["data"],
            title="Converter payload:",
        )
        validate(project, "VTK converter", converter_spec)

        print("\nStep 2: Remote reader + custom algorithm")
        mapdl_spec = RecipeSpec.from_sigma_converter(
            source=source_vtk.name,
            target=target_xdmf.name,
            converter=mapdl_thermal_converter(),
        )
        print_json(
            mapdl_spec.to_payload()["execution_payload"]["data"]["custom_algorithm"],
            title="Custom algorithm:",
        )
        validate(project, "remote converter", mapdl_spec)

        print("\nStep 3: CaseSelection on the spec (pin admitted cases at deploy)")
        # Labels align with annotations.py (solver=simpleFoam, split=train).
        # Admit grouping on upload/plan; pin with deploy_recipe(..., cases=upload.cases).
        multi_case_spec = RecipeSpec.from_sigma_converter(
            source=source_vtk.name,
            target=target_xdmf.name,
            converter=simple_vtk_converter(),
            case_selection=CaseSelection.where(
                split="train",
                labels={"solver": "simpleFoam"},
            ),
            case_admission_policy=CaseAdmissionPolicy(
                skip_unchanged_before_transform=False,
                zero_admitted_is_success_noop=True,
            ),
        )
        orchestration = multi_case_spec.to_payload()["execution_payload"]["data_ref"][
            "orchestration"
        ]
        print_json(orchestration, title="Orchestration:")
        validate(project, "multi-case converter", multi_case_spec)

        print("\nStep 3b: Optional TransformEntry (modality handoff on the spec)")
        # Default converter path omits this — Sigma picks staged files by extension.
        # Use TransformEntry when the run must bind by published logical assets.
        handoff_spec = RecipeSpec.from_sigma_converter(
            source=source_vtk.name,
            target=target_xdmf.name,
            converter=simple_vtk_converter(),
            transform_entry=TransformEntry(
                primary_modality_id="field",
                required_companion_modalities=["mesh"],
            ),
            sigma_handoff_capability=default_multipart_capability(),
        )
        handoff_orch = handoff_spec.to_payload()["execution_payload"]["data_ref"][
            "orchestration"
        ]
        print_json(
            {
                "transform_entry": handoff_orch.get("transform_entry"),
                "sigma_handoff": handoff_orch.get("sigma_handoff"),
            },
            title="Modality handoff:",
        )
        validate(project, "transform-entry converter", handoff_spec)

        print("\nStep 4: Extract structured tables (dataset generator + writer)")
        capabilities = project.get_recipe_capabilities()
        print_capabilities(capabilities, reader_limit=5)
        print(f"available_writers: {list(project.available_writers)}")
        try:
            dataset_generator = create_dataset_generator()
        except MissingOptionalDependencyError as exc:
            print(f"Skipping dataset-generator spec (install miura-nexus[sigma]): {exc}")
            return

        try:
            project.validate_recipe(
                RecipeSpec.from_sigma_dataset_generator(
                    source=target_xdmf.name,
                    target=target_parquet.name,
                    dataset_generator=dataset_generator,
                    writer="parquet_tabluar",  # intentional typo
                )
            )
        except ValidationError as exc:
            print(f"bad writer rejected: {exc}")

        dataset_spec = RecipeSpec.from_sigma_dataset_generator(
            source=target_xdmf.name,
            target=target_parquet.name,
            dataset_generator=dataset_generator,
            writer="parquet_tabular",
            writer_options={
                "flush": {
                    "mode": "rows",
                    "rows_per_file": 1_000_000,
                    "target_row_group_rows": 1_000_000,
                }
            },
        )
        print_json(dataset_spec.to_payload()["writer"], title="DataSetGenerator writer:")
        validate(project, "dataset generator", dataset_spec)


if __name__ == "__main__":
    main()

Recipe lifecycle: recipe_lifecycle.py

"""Recipe lifecycle: deploy / conflict / update (ADR-0019 V2).

Preferred authoring path for versioned recipes:

1. ``project.deploy_recipe(name, spec, cases=…)`` — create, no-op, or revise
2. ``get_or_create_recipe`` raises ``RecipeSpecConflictError`` on mismatch
3. Typed rebind with ``recipe.update(cases=…)`` when you hold a handle
4. Stale handle after concurrent revise → ``RecipeConflictError``

Default ``recipe.run()`` / ``run_recipe(recipe)`` uses ``inputs="pinned"``. Automatic
``inputs="latest"`` (re-admit against collection head) is not wired yet —
after new membership, re-admit via ``plan(..., case_grouping=…)``, then
``recipe.update(cases=upload.cases)`` or ``deploy_recipe(..., cases=…)``, then run.
"""

from __future__ import annotations

from datetime import datetime, timezone

from miura import Nexus
from miura.display import print_json
from miura.nexus.case_grouping.admission import CaseGroupingResult, MembershipCommitment
from miura.nexus.recipe.sigma_converter import RemoteSigmaConverter, SigmaClassRef
from miura.sdk import Schema
from miura.sdk.exceptions import RecipeConflictError, RecipeSpecConflictError
from miura.sdk.recipe import RecipeSpec


def _vtk_converter() -> RemoteSigmaConverter:
    return RemoteSigmaConverter(
        required_extensions=[".vtk"],
        reader=SigmaClassRef(class_name="vtkDataSetReader"),
        writer=SigmaClassRef(class_name="vtkXdmfWriter"),
    )


def _demo_cgr(result_id: str) -> CaseGroupingResult:
    """Construct an admitted-shaped pin for lifecycle demos (not a live CS admit)."""
    digest = "sha256:" + ("a" * 64)
    return CaseGroupingResult(
        result_id=result_id,
        membership_commitment=MembershipCommitment(
            canonical_membership_schema_version="1",
            case_group_stream_sha256=digest,
            member_edge_stream_sha256=digest,
            case_group_id_set_sha256=digest,
            membership_merkle_root=digest,
        ),
    )


def main() -> None:
    run_id = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S-%f")
    name = f"lifecycle-demo-{run_id}"

    with Nexus() as nexus:
        project = nexus.create_project(f"epico-recipe-lifecycle-{run_id}")
        schema = Schema.permissive()
        source = project.create_collection("source-vtk", schema=schema)
        target = project.create_collection("target-xdmf", schema=schema)

        spec_a = RecipeSpec.from_sigma_converter(
            source=source.name,
            target=target.name,
            converter=_vtk_converter(),
        )
        # Production code gets this handle from upload.cases, plan.cases, or
        # collection.case_grouping.admit(...).
        initial_cases = source.admitted_cases(_demo_cgr(f"cgr_initial_{run_id}"))
        print("Step 1: deploy_recipe creates the first revision")
        recipe = project.deploy_recipe(name, spec_a, cases=initial_cases)
        print(f"  recipe={recipe.name} revision={recipe.current_revision_id}")

        print("\nStep 2: deploy_recipe is a no-op when the effective spec matches")
        again = project.deploy_recipe(name, spec_a, cases=initial_cases)
        print(f"  same revision={again.current_revision_id}")

        print("\nStep 3: get_or_create_recipe conflicts on a different executable spec")
        # Source/target names are not part of the Converter execution payload —
        # change the reader so the effective executable spec actually differs.
        spec_b = RecipeSpec.from_sigma_converter(
            source=source.name,
            target=target.name,
            converter=RemoteSigmaConverter(
                required_extensions=[".vtk"],
                reader=SigmaClassRef(class_name="vtkXMLUnstructuredGridReader"),
                writer=SigmaClassRef(class_name="vtkXdmfWriter"),
            ),
        )
        try:
            project.get_or_create_recipe(name, spec_b)
            raise SystemExit("FAIL: expected RecipeSpecConflictError")
        except RecipeSpecConflictError as exc:
            print(f"  caught {type(exc).__name__}: {exc}")

        print("\nStep 4: deploy_recipe revises intentionally")
        revised = project.deploy_recipe(name, spec_b, cases=initial_cases)
        print(f"  new revision={revised.current_revision_id}")

        print("\nStep 5: recipe.update(cases=…) typed rebind")
        # In production this is upload.cases / plan.cases straight from admission.
        cgr = _demo_cgr(f"cgr_demo_{run_id}")
        cases = source.admitted_cases(cgr)
        rebound = revised.update(cases=cases)
        print(f"  rebound revision={rebound.current_revision_id} cases={cases.result_id}")

        print("\nStep 6: stale handle after revise raises RecipeConflictError")
        try:
            revised.update(cases=source.admitted_cases(_demo_cgr(f"cgr_stale_{run_id}")))
            raise SystemExit("FAIL: expected RecipeConflictError")
        except RecipeConflictError as exc:
            print(f"  caught {type(exc).__name__}: {exc}")

        print_json(
            {
                "name": rebound.name,
                "current_revision_id": rebound.current_revision_id,
                "effective_spec_commitment": rebound.effective_spec_commitment,
                "case_grouping_result_id": cgr.result_id,
            },
            title="Current recipe:",
        )

        print("\nLifecycle OK (deploy → conflict → deploy revise → update → stale CAS).")


if __name__ == "__main__":
    main()

Browse admitted CaseSet: case_set.py

"""Browse an admitted CaseSet (cases, roles, logical assets).

Creates a small companion layout (field + mesh in each case folder), uploads
with ``case_grouping=`` and ``require=[...]``, then browses the admitted
universe — no transform entry on a recipe.

Requires a live Nexus deployment (``MIURA_NEXUS_API_KEY`` / ``miura-auth``).
"""

from __future__ import annotations

from datetime import datetime, timezone
from pathlib import Path
from tempfile import TemporaryDirectory

from miura import Nexus
from miura.display import print_upload_result
from miura.nexus.case_grouping import CaseGrouping, ValidationProfile, require_role
from miura.sdk import LocalDataSource, Schema


def _vtk_stub(*, title: str) -> str:
    return (
        "# vtk DataFile Version 3.0\n"
        f"{title}\n"
        "ASCII\n"
        "DATASET POLYDATA\n"
        "POINTS 3 float\n"
        "0 0 0\n"
        "1 0 0\n"
        "0 1 0\n"
        "POLYGONS 1 4\n"
        "3 0 1 2\n"
    )


def write_companion_cases(root: Path, *, case_count: int = 2) -> None:
    """Guide layout: ``case_000/case_000.vtk`` + ``case_000/case_000_mesh.vtk``."""
    for index in range(case_count):
        case_id = f"case_{index:03d}"
        case_dir = root / case_id
        case_dir.mkdir(parents=True, exist_ok=True)
        (case_dir / f"{case_id}.vtk").write_text(
            _vtk_stub(title=f"{case_id} field"),
            encoding="utf-8",
        )
        (case_dir / f"{case_id}_mesh.vtk").write_text(
            _vtk_stub(title=f"{case_id} mesh"),
            encoding="utf-8",
        )


def companion_case_grouping() -> CaseGrouping:
    # Processing validation rides on the projected grouping (not CaseStructure).
    profile = ValidationProfile.of(
        "companion_roles_present",
        require_role("boundary_solution"),
        require_role("mesh"),
    )
    return CaseGrouping.join_by_path_id(
        "case_id",
        {
            "boundary_solution": "{case_id}/{case_id}.vtk",
            "mesh": "{case_id}/{case_id}_mesh.vtk",
        },
        require=["boundary_solution", "mesh"],
    ).with_validation_profile(profile)


def main() -> None:
    run_id = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")

    with TemporaryDirectory(prefix="epico-caseset-") as tmp:
        data_path = Path(tmp)
        write_companion_cases(data_path)

        with Nexus() as nexus:
            project = nexus.create_project(f"epico-caseset-{run_id}")
            collection = project.create_collection(
                "companion-cases",
                schema=Schema.permissive(),
            )

            print("Step 1: Upload with companions required in each case")
            grouping = companion_case_grouping()
            profile = getattr(grouping.strategy, "validation_profile", None)
            print(
                f"  validation_profile={getattr(profile, 'profile_id', None)} "
                f"rules={len(getattr(profile, 'rules', ()) or ())}"
            )
            upload = collection.upload(
                LocalDataSource(data_path),
                case_grouping=grouping,
            )
            print_upload_result(upload)

            print("\nStep 2: Browse admitted cases and logical assets")
            case_set = collection.case_set(upload.cases)
            try:
                projection = case_set.get_projection()
            except Exception:
                projection = case_set.materialize_projection()
            print(
                f"projection: {projection.projection_id} "
                f"state={projection.state} "
                f"cases={projection.case_count} assets={projection.logical_asset_count}"
            )
            cases = case_set.list_cases()
            assets = case_set.list_logical_assets()
            print(f"cases: {len(cases.items)}")
            for case in cases.items:
                coords = ", ".join(f"{k}={v}" for k, v in sorted(case.coordinates.items()))
                print(f"  {case.case_group_id} ({coords})")
            print(f"logical_assets: {len(assets.items)}")
            for asset in assets.items:
                print(f"  {asset.role} → {asset.modality_id}")


if __name__ == "__main__":
    main()

Admit existing collection cases: existing_collection_case_grouping.py

"""Admit case grouping after files already exist in a collection.

This is the NEXUS-392 path: upload without ``case_grouping=``, then snapshot
the existing collection membership with ``collection.case_grouping.admit``.
Admission moves no bytes and creates no ingestion plan. The returned
``AdmittedCases`` binds directly to ``deploy_recipe(..., cases=...)``.

Requires a live Nexus deployment (``MIURA_NEXUS_API_KEY`` / ``miura-auth``).
"""

from __future__ import annotations

from datetime import datetime, timezone
from pathlib import Path
from tempfile import TemporaryDirectory

from miura import Nexus
from miura.nexus.case_grouping import CaseGrouping
from miura.nexus.recipe.sigma_converter import RemoteSigmaConverter, SigmaClassRef
from miura.sdk import LocalDataSource, Schema, UploadMode
from miura.sdk.recipe import RecipeSpec


def _vtk_stub(*, title: str) -> str:
    return (
        "# vtk DataFile Version 3.0\n"
        f"{title}\n"
        "ASCII\n"
        "DATASET POLYDATA\n"
        "POINTS 3 float\n"
        "0 0 0\n"
        "1 0 0\n"
        "0 1 0\n"
        "POLYGONS 1 4\n"
        "3 0 1 2\n"
    )


def write_existing_cases(root: Path) -> None:
    for case_id in ("case_001", "case_002"):
        case_dir = root / case_id
        case_dir.mkdir(parents=True, exist_ok=True)
        (case_dir / "input.vtk").write_text(
            _vtk_stub(title=case_id),
            encoding="utf-8",
        )


def _converter_spec(*, source: str, target: str) -> RecipeSpec:
    return RecipeSpec.from_sigma_converter(
        source=source,
        target=target,
        converter=RemoteSigmaConverter(
            required_extensions=[".vtk"],
            reader=SigmaClassRef(class_name="vtkDataSetReader"),
            writer=SigmaClassRef(class_name="vtkXdmfWriter"),
        ),
    )


def main() -> None:
    run_id = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S-%f")

    with TemporaryDirectory(prefix="epico-existing-cases-") as tmp:
        data_path = Path(tmp)
        write_existing_cases(data_path)

        with Nexus() as nexus:
            project = nexus.create_project(f"epico-existing-cases-{run_id}")
            source = project.create_collection("existing-cases", schema=Schema.permissive())
            target = project.create_collection("converted-cases", schema=Schema.permissive())

            print("Step 1: Upload files without case grouping")
            upload = source.upload(
                LocalDataSource(data_path),
                mode=UploadMode.INSERT,
            )
            if upload.status != "success":
                raise SystemExit(f"FAIL: upload status={upload.status}")
            before = source.count_items()
            print(f"  existing items={before}")

            print("\nStep 2: Admit grouping against frozen existing membership")
            cases = source.case_grouping.admit(
                CaseGrouping.root_path("/", depth=1),
                group_name="data",
                schema_extras="include",
            )
            result = cases.case_grouping_result
            after = source.count_items()
            if after != before:
                raise SystemExit(f"FAIL: admission changed membership ({before} → {after})")
            if not result.has_inventory_pins:
                raise SystemExit("FAIL: admission did not return complete inventory pins")
            print(f"  result_id={result.result_id}")
            print(f"  membership unchanged={before == after}")
            print(f"  snapshot={result.collection_snapshot_ref}")

            print("\nStep 3: Bind admitted cases to a recipe")
            recipe = project.deploy_recipe(
                "process-existing-cases",
                _converter_spec(source=source.name, target=target.name),
                cases=cases,
            )
            binding = recipe.recipe["execution_payload"]["data_ref"]["orchestration"]["case_grouping_binding"]
            if binding.get("version") != 2:
                raise SystemExit(f"FAIL: expected binding version 2, got {binding.get('version')!r}")
            print(f"  recipe={recipe.name}")
            print(f"  binding version={binding['version']}")
            print(f"  bound result={binding['result_id']}")


if __name__ == "__main__":
    main()

Collection Parquet upload and read: parquet.py

from __future__ import annotations

from datetime import datetime, timezone
from pathlib import Path
from tempfile import TemporaryDirectory

from miura import Nexus
from miura.adapters import DatasetSource
from miura.adapters.dataframe import to_dataframe
from miura.display import print_items, print_upload_result
from miura.sdk import LocalDataSource, Schema, UploadMode


def write_parquet_tree(root: Path) -> None:
    import pyarrow as pa
    import pyarrow.parquet as pq

    root.mkdir(parents=True, exist_ok=True)
    for shard in range(2):
        table = pa.table(
            {
                "case_id": [shard] * 5,
                "temperature": [20.0 + shard + i for i in range(5)],
                "pressure": [1010.0 + 2.0 * i for i in range(5)],
                "label": [int(i % 2 == 0) for i in range(5)],
            }
        )
        pq.write_table(table, root / f"part-{shard:04d}.parquet")


def main():
    run_id = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S-%f")

    with TemporaryDirectory(prefix="epico-parquet-") as tmp:
        data_root = Path(tmp)
        write_parquet_tree(data_root)

        with Nexus() as nexus:
            project = nexus.create_project(f"epico-parquet-{run_id}")

            print("Step 1: Upload generated Parquet files")
            collection = project.create_collection(
                collection_name="direct-parquet",
                schema=Schema.permissive(),
            )
            result = collection.upload(
                LocalDataSource(data_root),
                mode=UploadMode.REPLACE,
            )
            print_upload_result(result)

            print("\nStep 2: Inspect files")
            items = collection.list_items(
                path=None,
                include_dirs=False,
                sort_by="item_uri",
            )
            print_items(items)

            print("\nStep 3: Read through the DataFrame adapter")
            df = to_dataframe(
                collection,
                columns=["temperature", "pressure", "label"],
                limit_rows=6,
            )
            print(df.to_string(index=False))

            print("\nStep 4: Build a dataset wrapper for downstream adapters")
            source = DatasetSource.from_collection(collection, path="/")
            print(source)

            print("\nStep 5: Keep catalog reads for published recipe outputs")
            print(
                "Direct uploads are readable as files. Published recipe outputs can "
                "also expose catalog assets for batch-oriented reads."
            )

            # print(f"\nCleaning up project: {project.name}")
            # project.delete_project()


if __name__ == "__main__":
    main()

Catalog asset ref and batch preview: catalog.py

"""
Catalog read wiring without a live catalog asset.

Builds a CatalogAssetRef from a fresh project/collection, previews batch shape
locally, and wires DatasetSource + shard policy. For live Flight reads after a
published table exists, use catalog_stream.py.

Run:
  uv run python docs/examples/catalog.py
"""

from __future__ import annotations

from datetime import datetime, timezone

import pyarrow as pa

from miura import Nexus
from miura.adapters import DatasetSource
from miura.display import (
    print_catalog_read_batches,
    print_catalog_read_complete,
    print_catalog_read_scope,
    print_json,
)
from miura.nexus.catalog import CatalogAssetRef, CatalogReadShard
from miura.sdk import Schema

DEMO_COLUMNS = ("temperature", "pressure", "smoke_scalar")


def local_batches(columns: list[str]) -> list[pa.RecordBatch]:
    values = {
        "smoke_scalar": pa.array([0.1, 0.2, 0.3, 0.4]),
        "pressure": pa.array([1010.0, 1012.5, 1015.0, 1018.5]),
        "temperature": pa.array([293.0, 294.5, 296.0, 297.5]),
    }
    arrays = [values.get(name, pa.array([None, None, None, None])) for name in columns]
    return [pa.record_batch(arrays, names=columns)]


def main() -> None:
    run_id = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S-%f")
    columns = list(DEMO_COLUMNS)
    batch_size = 4096
    max_batches = 2

    with Nexus() as nexus:
        project = nexus.create_project(f"epico-catalog-{run_id}")
        collection = project.create_collection(
            collection_name="catalog-wiring-demo",
            schema=Schema.permissive(),
        )
        asset_ref = CatalogAssetRef(
            project_id=project.id,
            collection_id=collection.id,
            asset_id="demo-catalog-asset",
        )

        print("Step 1: Build a catalog asset ref from the current project")
        print_catalog_read_scope(
            project_id=project.id,
            collection_id=collection.id,
            asset_id=asset_ref.asset_id,
            tenant_id="demo",
            columns=columns,
        )

        print("Step 2: Preview the projected batch shape (local sample rows)")
        batches = local_batches(columns)
        batch_count = min(len(batches), max_batches)
        row_count = sum(batch.num_rows for batch in batches[:batch_count])
        first_batch = batches[0] if batches else None

        print_catalog_read_batches(
            batch_count=batch_count,
            row_count=row_count,
            column_names=first_batch.column_names if first_batch is not None else None,
            sample_batch=first_batch,
        )

        print("Step 3: Reuse the asset ref as an adapter source")
        source = DatasetSource.from_catalog_asset(
            asset_ref,
            columns=columns,
            catalog_batch_size=batch_size,
            shard_world_size=2,
        )
        print_json(
            {
                "readiness": source.readiness,
                "columns": source.columns,
                "batch_size": source.catalog_batch_size,
                "shard_world_size": source.shard_world_size,
                "asset_ref": source.asset_ref,
            },
            title="DatasetSource:",
        )

        print("\nStep 4: Plan one deterministic shard")
        shard = CatalogReadShard(strategy="rank_partition", rank=0, world_size=2)
        print_json(shard.as_policy(), title="Shard policy:")

        print_catalog_read_complete(
            label="catalog.py",
            batch_count=batch_count,
            row_count=row_count,
        )


if __name__ == "__main__":
    main()

Live catalog Flight reads: catalog_stream.py

"""Live catalog asset read (Flight-backed batches).

Uploads Parquet so the platform publishes a catalog asset, waits until the
collection binding is queryable, then streams rows through catalog read
(``to_dataframe_async`` / ``nexus.catalog.iter_batches``).

After a dataset-generator recipe, resolve the same way with
``target.catalog_asset()`` on the recipe target collection — see ``4_consume.md``.

Requires ``miura-nexus[pandas]``, ``pyarrow``, and a live Nexus deployment.
"""

from __future__ import annotations

import asyncio
from datetime import datetime, timezone
from pathlib import Path
from tempfile import TemporaryDirectory

import pyarrow as pa
import pyarrow.parquet as pq

from miura import Nexus
from miura.adapters import DatasetSource
from miura.adapters.dataframe import to_dataframe_async
from miura.api import AsyncNexus
from miura.display import (
    print_catalog_read_batches,
    print_catalog_read_complete,
    print_catalog_read_scope,
    print_json,
    print_upload_result,
)
from miura.nexus.catalog import CatalogReadShard
from miura.sdk import LocalDataSource, Schema, UploadMode

COLUMNS = ("temperature", "pressure", "smoke_scalar")


def write_parquet_tree(root: Path) -> None:
    root.mkdir(parents=True, exist_ok=True)
    table = pa.table(
        {
            "temperature": [293.0, 294.5, 296.0, 297.5, 299.0, 300.5],
            "pressure": [1010.0, 1012.5, 1015.0, 1018.5, 1020.0, 1022.5],
            "smoke_scalar": [0.10, 0.20, 0.30, 0.40, 0.50, 0.60],
        }
    )
    pq.write_table(table, root / "part-0000.parquet")


async def stream_batches(asset, *, columns: list[str], batch_size: int) -> tuple[int, int]:
    """Return (batch_count, row_count) from a live Flight-backed stream."""
    ref = asset.logical_ref
    batch_count = 0
    row_count = 0
    first_batch = None
    async with AsyncNexus() as nexus:
        async for batch in nexus.catalog.iter_batches(
            project_id=ref.project_id,
            collection_id=ref.collection_id,
            asset_id=ref.asset_id,
            columns=columns,
            batch_size=batch_size,
        ):
            if first_batch is None:
                first_batch = batch
            batch_count += 1
            row_count += batch.num_rows
    print_catalog_read_batches(
        batch_count=batch_count,
        row_count=row_count,
        column_names=list(first_batch.column_names) if first_batch is not None else None,
        sample_batch=first_batch,
    )
    return batch_count, row_count


async def stream_shard(asset, *, columns: list[str], batch_size: int) -> int:
    """One rank of a rank_partition world — returns row count for this shard."""
    ref = asset.logical_ref
    shard = CatalogReadShard(strategy="rank_partition", rank=0, world_size=2)
    rows = 0
    async with AsyncNexus() as nexus:
        async for batch in nexus.catalog.iter_batches(
            project_id=ref.project_id,
            collection_id=ref.collection_id,
            asset_id=ref.asset_id,
            columns=columns,
            batch_size=batch_size,
            shard=shard,
        ):
            rows += batch.num_rows
    return rows


def main() -> None:
    run_id = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S-%f")
    columns = list(COLUMNS)
    batch_size = 4096

    with TemporaryDirectory(prefix="epico-catalog-stream-") as tmp:
        data_root = Path(tmp)
        write_parquet_tree(data_root)

        with Nexus() as nexus:
            project = nexus.create_project(f"epico-catalog-stream-{run_id}")
            collection = project.create_collection(
                collection_name="published-tables",
                schema=Schema.permissive(),
            )

            print("Step 1: Upload Parquet (platform publishes the catalog asset)")
            upload = collection.upload(
                LocalDataSource(data_root),
                mode=UploadMode.REPLACE,
            )
            print_upload_result(upload)

            print("\nStep 2: Wait until the published table is queryable")
            # After a dataset-generator recipe: asset = target.catalog_asset(...)
            asset = collection.catalog_asset(timeout=180.0, poll_interval=2.0)
            print_catalog_read_scope(
                project_id=asset.logical_ref.project_id,
                collection_id=asset.logical_ref.collection_id,
                asset_id=asset.logical_ref.asset_id,
                columns=columns,
            )
            print_json(
                {
                    "asset_id": asset.logical_ref.asset_id,
                    "fileset_snapshot_id": asset.fileset_snapshot_id,
                    "catalog_registration_id": asset.catalog_registration_id,
                },
                title="Queryable asset:",
            )

            print("\nStep 3: Read as a pandas DataFrame (Flight-backed)")
            source = DatasetSource.from_catalog_asset(
                asset,
                columns=columns,
                catalog_batch_size=batch_size,
            )
            df = asyncio.run(to_dataframe_async(source, limit_rows=10))
            print(df.to_string(index=False))

            print("\nStep 4: Stream RecordBatches via nexus.catalog.iter_batches")
            batch_count, row_count = asyncio.run(stream_batches(asset, columns=columns, batch_size=batch_size))

            print("\nStep 5: Optional shard (rank 0 of world_size=2)")
            shard_rows = asyncio.run(stream_shard(asset, columns=columns, batch_size=batch_size))
            print(f"  shard rows (rank 0 / 2): {shard_rows}")

            print_catalog_read_complete(
                label="catalog_stream.py",
                batch_count=batch_count,
                row_count=row_count,
            )


if __name__ == "__main__":
    main()

Pandas / PyTorch collection adapters: adapters.py

from __future__ import annotations

from datetime import datetime, timezone
from pathlib import Path
from tempfile import TemporaryDirectory

from miura import Nexus
from miura.adapters import DatasetSource, ItemSource
from miura.adapters.dataframe import to_dataframe
from miura.api.annotations import ItemSelect
from miura.display import print_upload_result
from miura.sdk import LocalDataSource, Schema, UploadMode
from miura.sdk.exceptions import MissingOptionalDependencyError

try:
    from miura.adapters.torch import to_torch_dataloader
except MissingOptionalDependencyError:
    to_torch_dataloader = None  # type: ignore[assignment,misc]


def write_parquet_tree(root: Path) -> None:
    import pyarrow as pa
    import pyarrow.parquet as pq

    root.mkdir(parents=True, exist_ok=True)
    table = pa.table(
        {
            "temperature": [18.0, 21.5, 25.0, 27.0],
            "pressure": [1008.0, 1012.0, 1015.0, 1021.0],
            "label": [0, 0, 1, 1],
        }
    )
    pq.write_table(table, root / "part-0000.parquet")


def main():
    run_id = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S-%f")

    with TemporaryDirectory(prefix="epico-adapters-") as tmp:
        data_root = Path(tmp)
        write_parquet_tree(data_root)

        with Nexus() as nexus:
            project = nexus.create_project(f"epico-adapters-{run_id}")

            print("Step 1: Upload a small Parquet collection")
            collection = project.create_collection(
                collection_name="adapter-parquet",
                schema=Schema.permissive(),
            )
            result = collection.upload(
                LocalDataSource(data_root),
                mode=UploadMode.REPLACE,
            )
            print_upload_result(result)

            print("\nStep 2: Tag uploaded items with a train split")
            for item in collection.iter_items():
                item.set_attributes(split="train")
            train_count = collection.count_items(select=ItemSelect(split="train"))
            print(f"train items: {train_count}")

            print("\nStep 3: Build a DatasetSource wrapper")
            source = DatasetSource.from_collection(collection, path="/")
            print(source)

            print("\nStep 4: Read as a pandas DataFrame (via DatasetSource)")
            df = to_dataframe(
                source,
                columns=["temperature", "pressure", "label"],
                limit_rows=10,
            )
            print(df.to_string(index=False))

            print("\nStep 5: Peek raw bytes through ItemSource")
            item_source = ItemSource(collection, max_items=1)
            for item in item_source.iter_items():
                head = item.read_bytes(64)
                print(f"{item.ref.path}: {len(head)} byte preview")

            print("\nStep 6: Build a Torch DataLoader (train split, client shuffle)")
            if to_torch_dataloader is None:
                print("Skipping Torch DataLoader (install miura-nexus[torch])")
            else:
                loader = to_torch_dataloader(
                    collection,
                    select=ItemSelect(split="train"),
                    feature_columns=["temperature", "pressure"],
                    target_column="label",
                    batch_size=2,
                    limit=4,
                    shuffle=True,
                )
                first_batch = next(iter(loader))
                print(first_batch)

            # Live catalog Flight reads: docs/examples/catalog_stream.py
            # (collection.catalog_asset → iter_batches).

            # print(f"\nCleaning up project: {project.name}")
            # project.delete_project()


if __name__ == "__main__":
    main()

DatasetSource collection vs catalog: dataset.py

"""
Wire DatasetSource for collection files vs catalog assets.

Run:
  uv run python docs/examples/dataset.py
"""

from __future__ import annotations

from datetime import datetime, timezone
from pathlib import Path
from tempfile import TemporaryDirectory

import pyarrow as pa
import pyarrow.parquet as pq

from miura import Nexus
from miura.adapters import DatasetSource
from miura.display import print_json, print_upload_result
from miura.nexus.catalog.types import CatalogAssetRef
from miura.sdk import LocalDataSource, Schema, UploadMode


def write_parquet_tree(root: Path) -> None:
    root.mkdir(parents=True, exist_ok=True)
    table = pa.table(
        {
            "temperature": [18.0, 21.5, 25.0, 27.0],
            "pressure": [1008.0, 1012.0, 1015.0, 1021.0],
            "label": [0, 0, 1, 1],
        }
    )
    pq.write_table(table, root / "part-0000.parquet")


def print_dataset_source(source: DatasetSource) -> None:
    print(f"readiness: {source.readiness}")
    print(f"columns: {source.columns}")
    print(f"path: {source.path}")
    print(f"batch size: {source.catalog_batch_size}")


def main():
    run_id = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S-%f")

    with TemporaryDirectory(prefix="epico-dataset-") as tmp:
        data_root = Path(tmp)
        write_parquet_tree(data_root)

        with Nexus() as nexus:
            project = nexus.create_project(f"epico-dataset-{run_id}")

            print("Step 1: Wrap a collection-backed dataset")
            collection = project.create_collection(
                collection_name="parquet-files",
                schema=Schema.permissive(),
            )
            result = collection.upload(
                LocalDataSource(data_root),
                mode=UploadMode.REPLACE,
            )
            print_upload_result(result)

            collection_source = DatasetSource.from_collection(collection, path="/")
            print_dataset_source(collection_source)

            print("\nStep 2: Build a catalog-backed source from known ids")
            ref = CatalogAssetRef(
                asset_id="my-asset-id",
                collection_id=collection.id,
                project_id=project.id,
            )
            catalog_source = DatasetSource.from_catalog_asset(
                ref,
                columns=["temperature", "pressure"],
                catalog_batch_size=1024,
            )
            print_dataset_source(catalog_source)
            print_json(ref.as_kwargs(), title="CatalogAssetRef:")

            print("\nStep 3: Prepare catalog batch read parameters")
            params = catalog_source.catalog_stream_params()
            print_json(params.iter_batches_kwargs(), title="iter_batches kwargs:")

            print("\nStep 4: Keep the live read step separate")
            print(
                "Use collection-backed reads for uploaded Parquet files, or build a "
                "CatalogAssetRef after a recipe publishes a catalog asset."
            )

            # print(f"\nCleaning up project: {project.name}")
            # project.delete_project()


if __name__ == "__main__":
    main()

Connector import end to end: connector_import.py

from __future__ import annotations

import asyncio
from pathlib import Path
import sys

from miura.api import AsyncNexus
from miura.display import print_ingestion_plan, print_upload_result
from miura.sdk import ConnectorDataSource, ConnectorInventoryScope, UploadMode

_examples = Path(__file__).resolve().parent
if str(_examples) not in sys.path:
    sys.path.insert(0, str(_examples))

from connector_env import (
    COLLECTION_NAME,
    CONNECTOR_ID,
    PROJECT_NAME,
    SOURCE_PREFIX,
    TRANSFER_BINDING_ID,
    optional,
    required,
)


async def main():
    connector_id = required(CONNECTOR_ID)
    source_prefix = required(SOURCE_PREFIX)
    transfer_binding_id = required(TRANSFER_BINDING_ID)
    project_name = required(PROJECT_NAME)
    collection_name = required(COLLECTION_NAME)
    include_glob = optional("NEXUS_INCLUDE_GLOB", "**/*")

    # Connector inventory and transfer polling are async-only: sync Nexus() raises TypeError
    # for a ConnectorDataSource.
    async with AsyncNexus() as nexus:
        project = await nexus.get_project(project_name)
        collection = await project.get_collection(collection_name)

        print("Step 1: Describe what to import")
        # Routing travels on the datasource, which is what keeps apply() identical to a
        # local upload. It is only read at apply() time, so an inventory-and-review run
        # can leave it out entirely.
        datasource = ConnectorDataSource(
            connector_id=connector_id,
            inventory_scope=ConnectorInventoryScope(
                source_prefix=source_prefix,
                include_glob=include_glob,
            ),
            transfer_binding_id=transfer_binding_id,
            asset_metadata={"source": "connector_import_example"},
        )
        print(f"  connector {datasource.connector_id} · {source_prefix} · {datasource.run_type}")

        print("\nStep 2: Plan — inventory the prefix and validate, no bytes move")
        plan = await collection.plan(datasource, mode=UploadMode.INSERT)
        print_ingestion_plan(plan)

        print("\nStep 3: Read the record-level report before deciding")
        report = await plan.print_validation_report(max_issues=20)
        print(f"  {len(report.passed_records)} admitted, {len(report.failed_records)} rejected")

        # Review boundary. Everything above is a dry run; the next step copies bytes.
        if not plan.is_admissible:
            raise SystemExit(
                f"Plan {plan.ingestion_plan_id} is not admissible "
                f"({plan.blocking_failure_count} blocking failures) — nothing was imported.",
            )

        print("\nStep 4: Apply — run the transfer")
        result = await plan.apply()
        print_upload_result(result)

        print("\nStep 5: Read the imported items")
        print(f"  {await collection.count_items()} items in the collection")
        shown = 0
        async for item, _depth in collection.walk_items(path="/", include_dirs=False):
            print(f"  {item.item_uri}")
            shown += 1
            if shown >= 10:
                break


if __name__ == "__main__":
    asyncio.run(main())

Inventory scope, reuse, and replay: connector_inventory.py

from __future__ import annotations

import asyncio
from pathlib import Path
import sys

from miura.api import AsyncNexus
from miura.sdk import ConnectorDataSource, ConnectorInventoryScope

_examples = Path(__file__).resolve().parent
if str(_examples) not in sys.path:
    sys.path.insert(0, str(_examples))

from connector_env import (
    CONNECTOR_ID,
    SOURCE_PREFIX,
    TRANSFER_BINDING_ID,
    optional,
    required,
)


async def main():
    connector_id = required(CONNECTOR_ID)
    source_prefix = required(SOURCE_PREFIX)
    include_glob = optional("NEXUS_INCLUDE_GLOB", "**/*")

    async with AsyncNexus() as nexus:
        scope = ConnectorInventoryScope(
            source_prefix=source_prefix,
            include_glob=include_glob,
            max_keys=20_000,
        )

        print("Step 1: Start an inventory job")
        created = await nexus.connector_inventory.start(connector_id, scope)
        job_id = created["connector_inventory_job_id"]
        print(f"  {job_id}  state={created['state']}  reused={created.get('reused')}")

        print("\nStep 2: Wait for the manifest to be sealed")
        finished = await nexus.connector_inventory.wait(job_id)
        print(f"  state={finished['state']}  revision={finished['connector_revision']}")
        print(f"  scope hash {finished['scope']['scope_hash']}")
        print(f"  expires {finished['expires_at']}")

        print("\nStep 3: Read the sealed manifest")
        manifest = await nexus.connector_inventory.get_manifest(job_id)
        print(f"  objects      {manifest.object_count}")
        print(f"  schema       {manifest.manifest_schema_version}")
        print(f"  sha256       {manifest.manifest_sha256}")
        print(f"  provenance   {manifest.provenance.value}")

        print("\nStep 4: The same scope inside the reuse window returns an alias")
        again = await nexus.connector_inventory.start(connector_id, scope)
        print(f"  {again['connector_inventory_job_id']}  reused={again.get('reused')}")
        print(f"  reused_from  {again.get('reused_from_job_id')}")

        print("\nStep 5: force_refresh lists the prefix again")
        refreshed = await nexus.connector_inventory.start(
            connector_id,
            ConnectorInventoryScope(
                source_prefix=source_prefix,
                include_glob=include_glob,
                force_refresh=True,
            ),
        )
        print(f"  {refreshed['connector_inventory_job_id']}  reused={refreshed.get('reused')}")

        print("\nStep 6: Replay the sealed manifest without listing again")
        # with_manifest() carries transfer routing over, so the replayed datasource can still
        # apply(). Dropping it here would turn a replay into an apply() that fails for missing
        # routing the caller did supply.
        base = ConnectorDataSource(
            connector_id=connector_id,
            inventory_scope=scope,
            transfer_binding_id=required(TRANSFER_BINDING_ID),
        )
        replay = base.with_manifest(manifest)
        binding_id, _dag_id, metadata = replay.require_transfer_routing()
        print(f"  bound to {replay.candidate_manifest.inventory_job_id}")
        print(f"  routing  {binding_id} · {metadata}")

        print("\nStep 7: Plan the replayed manifest — no re-listing, same sealed file set")
        # The replayed datasource carries no inventory scope, so nothing here names the prefix.
        # plan() reads the canonical one off the inventory job the manifest is bound to. Planning
        # it is the point of a replay, and stopping at require_transfer_routing() is what let the
        # documented flow ship broken.
        project_name = optional("NEXUS_PROJECT_NAME", "")
        collection_name = optional("NEXUS_COLLECTION_NAME", "")
        if project_name and collection_name:
            from miura.sdk import UploadMode

            project = await nexus.get_project(project_name)
            collection = await project.get_collection(collection_name)
            plan = await collection.plan(replay, mode=UploadMode.INSERT)
            print(f"  {plan.ingestion_plan_id}  {plan.object_count} objects  admissible={plan.is_admissible}")
        else:
            print("  set NEXUS_PROJECT_NAME and NEXUS_COLLECTION_NAME to plan the replay here")

        print("\nStep 8: A datasource without routing refuses to apply, and says so")
        review_only = ConnectorDataSource(connector_id=connector_id, inventory_scope=scope)
        try:
            review_only.require_transfer_routing()
        except ValueError as exc:
            print(f"  {exc}")


if __name__ == "__main__":
    asyncio.run(main())

Connector types, registration, tests: connector_admin.py

from __future__ import annotations

import asyncio
import json
from pathlib import Path
import sys

from miura.api import AsyncNexus

_examples = Path(__file__).resolve().parent
if str(_examples) not in sys.path:
    sys.path.insert(0, str(_examples))

from connector_env import CONNECTOR_ID, flag, required


def show(label: str, value) -> None:
    print(f"--- {label} ---")
    print(json.dumps(value, indent=2, sort_keys=True, default=str))


async def main():
    connector_id = required(CONNECTOR_ID)

    async with AsyncNexus() as nexus:
        print("Step 1: List the connector types this deployment supports")
        for connector_type in await nexus.connectors.list_types():
            capabilities = ",".join(connector_type["capabilities"])
            print(f"  {connector_type['key']:<16} {connector_type['display_name']:<44} {capabilities}")

        print("\nStep 2: Read one type's contract before configuring against it")
        show("connector type", await nexus.connectors.get_type("rclone_s3"))

        print("\nStep 3: Inspect the connector an administrator registered")
        connector = await nexus.connectors.get_connector(connector_id)
        show("connector", connector)

        print("\nStep 4: The credentials route returns key material — do not print it")
        # get_credentials() returns the stored access_key_id and secret_access_key in plaintext,
        # so showing the response would put both in this script's output. The connector record
        # already carries the non-secret half.
        print(f"  secret_ref       {connector.get('secret_ref')}")

        print("\nStep 5: Test the connection before spending an inventory job on it")
        # DSS rejects an empty body with 422, so capabilities are always sent. list_objects is what
        # inventory needs; test_connection alone is the SDK default. read_files and write_files are
        # refused rather than tested — proving them means touching your data.
        status = await nexus.connectors.test_and_wait(
            connector_id,
            {"requested_capabilities": ["test_connection", "list_objects"]},
        )
        print(f"  status           {status['status']}")
        print(f"  pinned revision  {status['pinned_revision']}")
        # What the probe established — the subset of the request that passed.
        print(f"  verified         {', '.join(status['capabilities_verified'])}")

        if not flag("NEXUS_ALLOW_CONNECTOR_WRITES"):
            print("\nSet NEXUS_ALLOW_CONNECTOR_WRITES=1 to also run the registration steps.")
            return

        print("\nStep 6: Register a connector")
        # `secrets` is the only place credentials appear. DSS stores them and returns a
        # `secret_ref`; nothing readable comes back, here or from get_connector().
        created = await nexus.connectors.create_connector(
            {
                "name": "example-s3-connector",
                "type_key": "rclone_s3",
                "driver_kind": "rclone",
                "config": {
                    "provider": "Other",
                    "region": "gra",
                    "bucket": "customer-bucket",
                    "endpoint": "https://s3.gra.io.cloud.ovh.net",
                    "prefix": "for_dev",
                },
                "secrets": {
                    "access_key_id": required("NEXUS_CONNECTOR_ACCESS_KEY_ID"),
                    "secret_access_key": required("NEXUS_CONNECTOR_SECRET_ACCESS_KEY"),
                },
            }
        )
        new_id = created["connector_id"]
        print(f"  {new_id}  revision {created['revision']}")
        print(f"  secret_ref {created['secret_ref']}")

        print("\nStep 7: Update it — every change bumps the revision")
        updated = await nexus.connectors.update_connector(new_id, {"name": "example-s3-connector-renamed"})
        print(f"  revision {updated['revision']}")

        print("\nStep 8: Delete it")
        await nexus.connectors.delete_connector(new_id)
        print(f"  deleted {new_id}")


if __name__ == "__main__":
    asyncio.run(main())

Shared environment lookup: connector_env.py

"""Shared environment lookup for the connector example scripts.

Connector examples cannot generate their own fixtures the way the local-upload examples do:
they need a connector an administrator already registered, and platform routing values that
are specific to your deployment. Reading them here keeps the three scripts identical in how
they fail when something is missing — before any call goes out, with the variable named.
"""

from __future__ import annotations

import os

CONNECTOR_ID = "NEXUS_CONNECTOR_ID"
SOURCE_PREFIX = "NEXUS_SOURCE_PREFIX"
TRANSFER_BINDING_ID = "NEXUS_TRANSFER_BINDING_ID"
PROJECT_NAME = "NEXUS_PROJECT_NAME"
COLLECTION_NAME = "NEXUS_COLLECTION_NAME"


def required(name: str) -> str:
    """Return an environment variable, or raise naming the one that is missing."""
    value = os.environ.get(name, "").strip()
    if not value:
        raise RuntimeError(
            f"Required environment variable is missing: {name}. See the prerequisites at "
            "https://nexus.dev.miurasimulation.com/api/docs/examples/connector_import/"
            "#before-you-start",
        )
    return value


def optional(name: str, default: str) -> str:
    """Return an environment variable, falling back to a documented default."""
    value = os.environ.get(name, "").strip()
    return value or default


def flag(name: str, *, default: bool = False) -> bool:
    """Return a boolean environment variable using the usual truthy spellings."""
    raw = os.environ.get(name)
    if raw is None:
        return default
    return raw.strip().lower() in {"1", "true", "yes", "on"}

Shared case schemas: schemas.py

Reused by the upload and process walkthroughs rather than redefined per script, so a schema change lands in one place.

"""Structured collection schemas for ``examples`` bundled datasets."""

from __future__ import annotations

from miura.sdk import CaseStructure, File, Folder, Number, Schema, capture, one_or_more


def _polymesh_files() -> list:
    return [
        File("points"),
        File("faces"),
        File("owner"),
        File("neighbour"),
        File("boundary"),
    ]


def openfoam_quickstart_schema() -> Schema:
    """Minimal OpenFOAM stub for quick-start demos (``case_`` + 6 digits, one or more)."""
    case_id = Number(min_digits=6, max_digits=6)
    return Schema(
        roots=[
            Folder(
                "case_" + case_id,
                count=one_or_more(),
                children=[
                    Folder("system", children=[File("controlDict")]),
                    Folder("0", children=[File("U")]),
                ],
            )
        ]
    )


def vtk_case_structure() -> CaseStructure:
    """One ``field.vtk`` per ``case_`` + 6-digit folder."""
    case_id = capture("case_id", Number(min_digits=6, max_digits=6))
    return CaseStructure.folder(
        path="case_" + case_id,
        members={"result_field": File("field.vtk")},
    )


def vtk_case_schema() -> Schema:
    return vtk_case_structure().schema(count=one_or_more())


def xdmf_case_structure() -> CaseStructure:
    """One ``field.xdmf`` per ``case_`` + 6-digit folder (Getting Started / quick_start)."""
    case_id = capture("case_id", Number(min_digits=6, max_digits=6))
    return CaseStructure.folder(
        path="case_" + case_id,
        members={"result_field": File("field.xdmf")},
    )


def xdmf_case_schema() -> Schema:
    return xdmf_case_structure().schema(count=one_or_more())


def openfoam_case_schema() -> Schema:
    """``shared/data/openfoam``: serial OpenFOAM cases (``constant/``, ``system/``, ``0/``)."""
    case_id = Number(min_digits=6, max_digits=6)
    return Schema(
        roots=[
            Folder(
                "case_" + case_id,
                count=one_or_more(),
                children=[
                    Folder(
                        "constant",
                        children=[
                            Folder("polyMesh", children=_polymesh_files()),
                            File("transportProperties"),
                        ],
                    ),
                    Folder(
                        "system",
                        children=[
                            File("controlDict"),
                            File("fvSchemes"),
                            File("fvSolution"),
                        ],
                    ),
                    Folder(
                        "0",
                        children=[
                            File("U"),
                            File("p"),
                        ],
                    ),
                ],
            )
        ]
    )

Internal note — Not in this folder yet

Topics worth adding as new scripts under docs/examples/:

Topic Today Suggested script
Async AsyncNexus The three connector scripts use AsyncNexus; the rest are sync async_quickstart.py
End-to-end recipe run (extra scenarios) quick_start.py covers deploy → run → wait → dataframe companion converter run, async Nexus
Live catalog batch reads catalog_stream.py covers Flight iter_batches / to_dataframe_async —
Dask partitioned reads — dask_read.py
Multipart / very large uploads upload.py covers ingestion modes on small cases extend upload.py or add multipart_upload.py