How it works¶
httk-serve implements an OPTIMADE server as a pipeline of small, independently testable layers. The package was ported from the OPTIMADE implementation in httk v1 and reorganized around two explicit seams: a web-framework seam and a storage-backend seam.
Request flow¶
ASGI request
└─ runtime/asgi.py — adapts the HTTP request to a RawRequest
└─ engine/processing.py — process(): dispatches to an endpoint
├─ engine/validate.py — validates URL, version, and query parameters
├─ filter/ — parses the OPTIMADE filter language to an AST
├─ query_function — the backend seam (executes entry queries)
└─ endpoints/ — builds the JSON:API reply documents
Runtime (
httk.serve.optimade.runtime): a Starlette application with a single catch-all GET route. It builds aRawRequest(base URL, representation, query string) and renders the resultingEndpointResponse. All errors are converted to OPTIMADE JSON:API error documents.Engine (
httk.serve.optimade.engine):validate_optimade_request()resolves the endpoint (including versioned base URLs like/v1.0.0/structures), validates query parameters, and computes the response fields.process()routes the validated request to the matching endpoint reply generator, parsing thefilter=parameter when present.Filter parsing (
httk.serve.optimade.filter): the OPTIMADE filter grammar (shipped asoptimade_filter_grammar.ebnf) is parsed with a vendored LR(1) parser into a nested-tuple abstract syntax tree.Backend (
httk.serve.optimade.backend): the filter AST is translated into backend search expressions and executed. This is the storage seam.
The backend seam¶
process() never touches storage directly; it calls a QueryFunction
callback. The standard implementation is provided by BackendAdapter:
Store/Searcherprotocols (backend/protocols.py) describe the query interface a storage backend must implement (mirroring the httk v1httk.dbsearcher API:variable(),add(),count(), set operations likehas_any, literal string matching (contains/startswith/endswith, which carry no pattern syntax), and the comparison operators).EntrySourcepairs a queryable target (e.g. a table or type) with a mapping from OPTIMADE response fields to row extractors. An entry endpoint can be backed by several sources; results are concatenated and offset/limit are redistributed across them.Field handlers (
backend/handlers.py) translate filter operations on OPTIMADE properties into search expressions over backend fields. A backend supplies a handler table per entry type;simple_property_handlers()derives one generically from a property-key map and the entry’s property types. When aBackendAdapteris given no handlers, it derives them from its schema.
httk-serve is a generic implementation of the OPTIMADE protocol: it carries
no materials-science knowledge of its own. In a durable deployment, configured
entry families are discovered from EntryStore and queried lazily through
adapter_from_store; see Serving directly from an entry
store. A record saved after application construction is
visible without rebuilding an adapter. EntryProvider is the separate
in-memory ingestion path for generated and compatibility datasets.
Entry providers¶
For a worked usage guide with runnable examples, see Serving entry providers.
The materials-science mapping lives outside httk-serve, behind the neutral
httk.core.EntryProvider contract (defined in httk-core, so neither package
depends on the other). A provider describes its entry types (as first-class
httk.core.EntryTypeDefinition objects — vendored standards or definitions built
with from_optimade/from_simple), states how each served property maps to a
record key, and yields the records:
entry_types()→ entry-type name to anEntryTypeDefinition;property_keys(entry_type)→ served-property to record-key map (at leastid/type; every served name must be described by the definition);records(entry_type)→ an iterable of plain JSON-able record dicts.
adapter_from_providers([...]) (backend/providers.py) turns one or more
providers into a fully wired BackendAdapter over an in-memory store: it builds
the ServedSchema from the definitions (validating each served property
against the definition), the filter handlers from the property keys, and the
response-field extractors from the property keys, then loads the records.
adapter_from_store(store) (backend/stores.py) instead reads the store’s
declared entry layout, ignores families without an OPTIMADE definition, and
builds a StoredBackendAdapter. Its query callback delegates filtering,
sorting, counting, pagination, and record hydration to durable entry
federations. It never calls EntryProvider.records() and never copies the
database into an InMemoryStore.
The materials provider itself lives in httk-atomistic
(httk.atomistic.entries.structures.StructureEntryProvider), which serves
OPTIMADE structures — species, species_at_sites, lattice_vectors,
cartesian_site_positions, … — from httk.atomistic.UnitcellStructure objects.
Providers self-register a factory via httk.core.register_entry_provider, so
providers_from_registry() can enumerate the installed ones (applications
instantiate them with their data).
OPTIMADE version support¶
The served API version is 1.3.0 (versioned base URLs /v1, /v1.3, /v1.3.0).
Relative to OPTIMADE v1.0.0, the implementation includes:
boolean values (
TRUE/FALSE) in the filter language (v1.2),the v1.2 entry listing info format: top-level
id/typeand properties presented as OPTIMADE Property Definitions (the property-definition model and generator live in httk-core,httk.core.property_definitions),extended
metainformation:implementationwithsource_urlandissue_tracker, and the optionalschema,database, andrequest_delayfields viaOptimadeConfig,the structures properties added in v1.2 (space-group symmetry fields) and v1.3 (
fractional_site_positions,site_coordinate_span,optimization_type,wyckoff_positions, …) — recognized in filters and as response fields; they are served asnulluntil a backend implements them,sorting of entry listings (the
sortquery parameter),relationships between entries and the
includequery parameter (compound documents with a top-levelincludedfield), with per-identifier relationshipmeta(descriptionand the v1.3role),filtering on relationships:
<type>.id HAS ...and depth-1 relationship-property filters such asreferences.doi CONTAINS "10.1"(resolved by a two-phase semi-join over the related entry type; each dotted filter node is resolved independently, matching the reference implementation’s semantics),the
references,files, andtrajectoriesentry types,per-property metadata (
meta.property_metadataand thex-optimade-metadata-definitionin property definitions),the partial data protocol (JSON Lines format and the
dimension_slicesquery parameter) with the compact list representation for trajectories,the
license,available_licenses, andavailable_licenses_for_entriesbase-info attributes, and thewarnings,last_id, andlinks.describedbyresponse fields viaOptimadeConfig.
Optional parts of the specification that are not implemented: cross-source sort
merging, filtering on relationship paths nested deeper than one level
(references.structures.x), on relationship meta
(.description/.role), and dotted LENGTH filters, the sparse JSON Lines
layout, transaction mechanisms, and rejection of unrecognized query parameters.
Index meta-databases and composition¶
An index is configured separately from a normal backend-backed OPTIMADE
service. Its links contain one root and any child, external, or
providers entries; default_link_id optionally selects a child:
from httk.serve import ASGIAppMount, compose_asgi_apps
from httk.serve.optimade import OptimadeIndexConfig, create_index_asgi_app
index = create_index_asgi_app(
OptimadeIndexConfig(links=[root_link, amdb_link], default_link_id="amdb"),
baseurl="https://example.org/optimade/index/",
)
app = compose_asgi_apps(
[ASGIAppMount("/optimade/index", index), ASGIAppMount("/optimade/amdb", amdb_app)],
root=ASGIAppMount("/", website_app),
)
compose_asgi_apps orders nested mounts from most specific to least specific
and coordinates the Starlette lifespan of every child. The index has no
backend adapter: its /info, /links, and unversioned /versions responses
are produced by the same request, version, rendering, reporting, and CORS
pipeline as an ordinary service.
Serving additional entry types¶
The served entry types and their properties are described by a ServedSchema
(schema/served.py), built with build_served_schema(). A backend registers
extra entry types by passing them in and wiring an EntrySource for each:
build_served_schema(definitions, served)derives the endpoint/field tables from a mapping of entry type toEntryTypeDefinitionand the per-entry list of served property names (defaulting to every described property). Thetrajectoriesentry type is generated by frame-wrapping thestructuresproperties (schema/trajectories.py, which takes the structuresEntryTypeDefinition) and turning the result into a definition viaentry_type_definition_from_simple(inschema.served).BackendAdapter(schema=..., sources={entry: (EntrySource(...),)})binds each served entry type to a queryable target and its field extractors.
The examples/optimade/demo_server/ backend registers references, files, and
trajectories this way alongside structures and calculations.
Large properties and slicing¶
Field extractors may return a PartialValue (backend/partial.py) instead of
a concrete value for large, dimensioned properties (e.g. a trajectory’s
cartesian_site_positions). In a normal reply such a value is served as null
with a meta.partial_data_links entry pointing at
partial_data/<type>/<id>/<property>, which streams the data in the JSON Lines
format. A client may instead request an inline slice with the
dimension_slices=name[start:stop:step] query parameter (single-entry
endpoints only); note that stop is inclusive per the specification. Values
that are constant across a dimension (e.g. nelements across a trajectory’s
frames) are served as single-item constant compact lists, which is legal
because those axes are declared compactable.
Differences from the httk v1 implementation¶
The stdlib
httk.httkweb.webserverbinding was replaced by a Starlette ASGI app + uvicorn development server (serve()), andcreate_asgi_app()supports production ASGI deployment.serve()takes aBackendAdapterinstead of a rawhttk.dbstore.The client-side
validation/subpackage (stale at OPTIMADE 0.9.5) was not ported; use the officialoptimade-validatorinstead.Several latent bugs were fixed (query-string derivation, caller-supplied endpoints, offsets beyond the result set) — see the regression tests in
tests/.Timestamps in
metaare now UTC.