Serving entry providers

httk-optimade is a generic implementation of the OPTIMADE protocol: it carries no knowledge of what it serves. Everything served — entry types, their properties, and the records — is supplied through the neutral httk.core.EntryProvider contract. This page shows how to write a provider, serve it, and query it. For the internals, see How it works.

Write a provider

A provider answers three questions: what entry types do you serve (each described by a first-class httk.core.EntryTypeDefinition), which record key holds each property (the property-key map must cover at least id and type, and every served name must be described by the definition), and what are the records (plain JSON-able mappings). Property definitions are generated from a compact description with PropertyDefinition.from_simple (or loaded from the vendored standards). A minimal provider serving a custom widgets entry type:

from collections.abc import Iterable, Mapping
from typing import Any

from httk.core import EntryProvider, EntryTypeDefinition, PropertyDefinition


class WidgetProvider(EntryProvider):
    """A minimal provider serving a custom ``widgets`` entry type."""

    def __init__(self, widgets: list[dict[str, Any]]) -> None:
        self._widgets = widgets

    def entry_types(self) -> Mapping[str, EntryTypeDefinition]:
        return {
            "widgets": EntryTypeDefinition(
                "widgets",
                "A widgets entry.",
                {
                    "id": PropertyDefinition.from_simple("id", description="The widget id.", required_response=True),
                    "type": PropertyDefinition.from_simple("type", description="The entry type.", required_response=True),
                    "cogs": PropertyDefinition.from_simple("cogs", description="Number of cogs.", fulltype="integer"),
                    "tags": PropertyDefinition.from_simple("tags", description="Tag labels.", fulltype="list of string"),
                },
            )
        }

    def property_keys(self, entry_type: str) -> Mapping[str, str]:
        return {"id": "__id", "type": "type", "cogs": "cogs", "tags": "tags"}

    def records(self, entry_type: str) -> Iterable[Mapping[str, Any]]:
        return self._widgets


provider = WidgetProvider(
    [
        {"__id": "w-1", "type": "widgets", "cogs": 3, "tags": ["red", "small"]},
        {"__id": "w-2", "type": "widgets", "cogs": 5, "tags": ["blue"]},
    ]
)

Each property’s type drives which filter operations the engine offers for it (comparisons for numbers, string matching for strings, HAS membership for lists) — no handler code is needed. Custom (database-specific) properties must carry a recognized prefix (_httk_/_omdb_) and be merged into a standard definition with EntryTypeDefinition.extended({...}).

Serve it

adapter_from_providers turns one or more providers into a fully wired backend adapter; serve runs a development server (or use create_asgi_app with any ASGI server):

from httk.optimade import adapter_from_providers, serve

serve(adapter_from_providers([provider]), port=8080)

The API is then live, e.g.:

curl 'http://localhost:8080/v1/widgets?filter=cogs=5'
{
 "data": [
  {"attributes": {"cogs": 5, "tags": ["blue"]}, "id": "w-2", "type": "widgets"}
 ],
 "links": {"next": null},
 "meta": {"api_version": "1.3.0", "data_available": 2, "data_returned": 1, "...": "..."}
}

Filters compose over the described properties: filter=cogs>3 AND tags HAS "blue", filter=id="w-1", and so on; /v1/info and /v1/info/widgets are generated from the provider’s descriptions. A runnable version of this example is in examples/provider_server/, and examples/demo_server/ shows the lower-level wiring (custom EntrySources, handler tables, and an OptimadeConfig with provider links) that adapter_from_providers automates.

Query programmatically

For tests or in-process use, skip HTTP and drive the engine directly:

from httk.optimade import adapter_from_providers
from httk.optimade.backend import execute_query
from httk.optimade.filter import parse_optimade_filter

adapter = adapter_from_providers([provider])
results = execute_query(
    adapter, ["widgets"], ["id", "cogs"], [], 100, 0,
    parse_optimade_filter('tags HAS "red"'),
)
print([r.values["id"] for r in results])  # ['w-1']

Relationships, include, and relationship filtering

A provider can declare related entries by overriding the optional EntryProvider.relationships(entry_type) hook, which maps each entry id to a flat tuple of httk.core.RelatedEntry objects (each naming the related entry type and id, optionally with a per-identifier description and v1.3 role). adapter_from_providers wires that into the served OPTIMADE relationships block automatically (grouped by related type, metadata rendered as the JSON:API meta object), so an include=<type> request embeds the related resources (resolved from whichever provider serves that related type). Filtering is auto-wired too: references.id HAS "ref-1" matches over the declared ids, and depth-1 relationship-property filters such as references.title CONTAINS "study" are resolved by filtering the related entry type’s own properties (each dotted filter node independently). A provider that does not override the hook is unaffected — no relationships block is emitted.

from collections.abc import Iterable, Mapping
from typing import Any

from httk.core import EntryProvider, EntryTypeDefinition, PropertyDefinition, RelatedEntry, standard_entry_type
from httk.optimade import adapter_from_providers
from httk.optimade.backend import execute_query
from httk.optimade.filter import parse_optimade_filter


class LinkedProvider(EntryProvider):
    def entry_types(self) -> Mapping[str, EntryTypeDefinition]:
        structures = EntryTypeDefinition(
            "structures",
            "Structures.",
            {
                "id": PropertyDefinition.from_simple("id", description="id", required_response=True),
                "type": PropertyDefinition.from_simple("type", description="type", required_response=True),
            },
        )
        return {"structures": structures, "references": standard_entry_type("references")}

    def property_keys(self, entry_type: str) -> Mapping[str, str]:
        if entry_type == "structures":
            return {"id": "__id", "type": "type"}
        return {"id": "__id", "type": "type", "title": "title"}

    def records(self, entry_type: str) -> Iterable[Mapping[str, Any]]:
        if entry_type == "structures":
            return [{"__id": "s-1", "type": "structures"}]
        return [{"__id": "ref-1", "type": "references", "title": "A study"}]

    def relationships(self, entry_type: str) -> Mapping[str, tuple[RelatedEntry, ...]]:
        if entry_type == "structures":
            return {"s-1": (RelatedEntry("references", "ref-1", description="Cited for this structure"),)}
        return {}


adapter = adapter_from_providers([LinkedProvider()])
(row,) = list(execute_query(adapter, ["structures"], ["id", "type"], [], 100, 0))
assert row.relationships == {"references": [{"id": "ref-1", "description": "Cited for this structure"}]}

# Relationship filtering works without any handler wiring:
(row,) = list(
    execute_query(
        adapter, ["structures"], ["id"], [], 100, 0,
        parse_optimade_filter('references.title CONTAINS "study"'),
    )
)
assert row.values["id"] == "s-1"

A runnable server showcasing declared relationships (serving, include, and both kinds of relationship filtering) is in examples/provider_server/.

Discover registered providers

Provider packages can self-register a factory under httk.handlers.* via httk.core.register_entry_provider. For example, httk-data registers in-memory providers for the standard references/files/calculations entry types (as data-references/data-files/data-calculations), and httk-atomistic registers atomistic-structures. providers_from_registry resolves everything registered in the current environment; since providers need data, you instantiate them:

from httk.optimade import adapter_from_providers, providers_from_registry

factories = providers_from_registry()
provider = factories["atomistic-structures"](my_structures)  # requires httk-atomistic
adapter = adapter_from_providers([provider])

Serving crystal structures (via httk-atomistic)

The materials mapping lives in httk-atomistic, not here: its StructureEntryProvider maps Structure objects to an OPTIMADE structures entry type (species, species_at_sites, lattice_vectors, cartesian_site_positions, nsites, elements, nelements, structure_features). With httk-atomistic installed:

from httk.atomistic import Structure, StructureEntryProvider
from httk.optimade import adapter_from_providers, serve

nacl = Structure(
    cell=(5.64, 5.64, 5.64, 90.0, 90.0, 90.0),
    sites=[[0.0, 0.0, 0.0], [0.5, 0.5, 0.5]],
    species=[
        {"name": "Na", "chemical_symbols": ["Na"], "concentration": [1.0]},
        {"name": "Cl", "chemical_symbols": ["Cl"], "concentration": [1.0]},
    ],
    species_at_sites=["Na", "Cl"],
)

serve(adapter_from_providers([StructureEntryProvider({"nacl": nacl})]))
curl 'http://localhost:8080/v1/structures?filter=elements HAS "Na"'

Neither package imports the other — the contract in httk-core is the only coupling, which is why the two install and evolve independently.