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.