Serving a database as an OPTIMADE API¶
The last step of the chain: a store full of frozen dataclasses becomes an
OPTIMADE API. StoreEntryProvider implements httk-core’s neutral
httk.core.EntryProvider contract over an SqlStore; httk-serve’s
adapter_from_providers consumes that contract. httk-store does not depend on
httk-serve. The provider handoff uses the httk-core contract, while
httk-serve also consumes httk-store’s neutral query and store APIs.
What the provider derives from the schema¶
StoreEntryProvider(store, {"books": Book, "writers": Writer}) needs nothing
but the mapping from entry-type name to storable class. From each class’s
storage schema it derives:
an
EntryTypeDefinitionwith one OPTIMADE property per servable stored field, named with a registered database-specific prefix (_httk_by default), plus the mandatoryidandtype. You may pass your owndefinitions=instead, as long as they describe everything served.JSON-able records: rationals are served as their nearest floats (the exact values stay in the database), datetimes as ISO text, fixed-shape tensors as nested lists, and
stored_propertyvalues as ordinary properties.relationships, for reference fields and
list[Storable]fields whose target class is also served. ARelatedmarker on the field adds the OPTIMADErole/descriptionmetadata;StorageInfo(links=...)lets a separate join class contribute relationships without being served itself.
Fields with no OPTIMADE value representation — bytes, custom codecs — are
simply not served, and a reference field whose target class is not served
becomes neither a property nor a relationship.
Running a query¶
adapter_from_providers([provider]) builds a backend adapter: it reads the
definitions, loads the records, and wires the filter handlers and the
relationship id fields. execute_query(adapter, entries, response_fields, ...)
is then the whole OPTIMADE query pipeline minus HTTP — the same call an
OPTIMADE web endpoint makes after parsing a request. It takes the filter as an
already-parsed AST from parse_optimade_filter, so this example goes
end to end from a filter string to served records.
Note the difference from the OPTIMADE filters example: there the filter runs as SQL against the store and yields dataclass instances; here it runs through the serving layer and yields records — id, type and the selected response fields — which is what an API actually returns.
Ids¶
By default an entry is identified as "<entry type>-<sid>", using the row’s
integer sid: books-1, writers-3. An id_of callback replaces that scheme
everywhere at once, including inside relationships.
import datetime
from dataclasses import dataclass
from fractions import Fraction
from typing import Annotated, Any
from httk.core.optimade import parse_optimade_filter
from httk.core.storage import Related, stored_property
from httk.store.db import Database, SqlStore, StoreEntryProvider
HTTK_EXAMPLE_REQUIRES = ["sqlalchemy", "httk.serve.optimade"]
@dataclass(frozen=True)
class Writer:
name: str
born: int
@dataclass(frozen=True)
class Book:
title: str
pages: int
price: Fraction # exact in the database, served as a float
in_print: bool
published: datetime.datetime # served as ISO text
cover: bytes # no OPTIMADE representation: not served
keywords: list[str]
author: Annotated[Writer | None, Related(role="author", description="Wrote the book")] = None
@stored_property
def nkeywords(self) -> int:
return len(self.keywords)
ADA = Writer("Ada", 1815)
BOOLE = Writer("Boole", 1815)
CARA = Writer("Cara", 1820)
BOOKS = [
Book(
title="Analytical Engines",
pages=350,
price=Fraction(1, 3),
in_print=True,
published=datetime.datetime(2026, 7, 24, 12, 30, 0), # noqa: DTZ001 (naive datetime is the storage contract)
cover=b"\x00\xff",
keywords=["computing", "history"],
author=ADA,
),
Book(
title="Laws of Thought",
pages=420,
price=Fraction(7, 2),
in_print=True,
published=datetime.datetime(2024, 3, 1, 9, 0, 0), # noqa: DTZ001 (naive datetime is the storage contract)
cover=b"",
keywords=["logic"],
author=BOOLE,
),
Book(
title="Silence",
pages=120,
price=Fraction(-7, 5),
in_print=False,
published=datetime.datetime(2020, 1, 1, 0, 0, 0), # noqa: DTZ001 (naive datetime is the storage contract)
cover=b"",
keywords=[],
),
]
#: OPTIMADE filter strings to run through the serving layer, with the fields to return.
QUERIES: list[tuple[str, list[str], str]] = [
("books", ["id", "_httk_custom_title", "_httk_custom_pages"], "_httk_custom_pages > 200"),
("books", ["id", "_httk_custom_price"], "_httk_custom_price < 0"),
("books", ["id", "_httk_custom_keywords"], '_httk_custom_keywords HAS "history"'),
("books", ["id", "_httk_custom_title"], '_httk_custom_in_print = TRUE AND _httk_custom_title CONTAINS "o"'),
("writers", ["id", "_httk_custom_name"], "_httk_custom_born = 1820"),
]
def populate() -> SqlStore:
store = SqlStore(Database.sqlite(), entry_records={})
with store.transaction():
for writer in (ADA, BOOLE, CARA): # saved first, so their sids are 1, 2, 3
store.save(writer)
for book in BOOKS:
store.save(book)
return store
def show_provider(provider: StoreEntryProvider) -> None:
"""The provider's answers to the entry-provider contract, derived from the schema."""
print("== Auto-generated entry-type definitions ==")
for entry_type, definition in sorted(provider.entry_types().items()):
print(f" {entry_type}: {', '.join(sorted(definition.properties))}")
books = provider.entry_types()["books"].properties
print(
f" _httk_custom_price is served as: {books['_httk_custom_price'].optimade_type} (a Fraction in the database)"
)
print(f" _httk_custom_published is served as: {books['_httk_custom_published'].optimade_type}")
print(f" _httk_custom_nkeywords is served as: {books['_httk_custom_nkeywords'].optimade_type} (a stored_property)")
print(" '_httk_custom_cover' (bytes) is absent: no OPTIMADE value representation.")
print(" '_httk_custom_author' is absent as a property: references surface as relationships.")
print()
print("== Records, keyed through property_keys() ==")
property_keys = provider.property_keys("books")
for record in provider.records("books"):
served = {name: record[key] for name, key in property_keys.items()}
print(
f" {served['id']}: title={served['_httk_custom_title']!r}, price={served['_httk_custom_price']}, "
f"published={served['_httk_custom_published']!r}, keywords={served['_httk_custom_keywords']}"
)
print()
print("== Relationships ==")
for entry_id, related in provider.relationships("books").items():
for entry in related:
print(f" {entry_id} -> {entry.id} (type {entry.entry_type}, role={entry.role}, {entry.description})")
print(" 'Silence' has no author, so it declares no relationships.")
print()
def show_queries(provider: StoreEntryProvider) -> None:
"""Hand the provider to httk-serve and run OPTIMADE queries against it."""
from httk.serve.optimade import adapter_from_providers
from httk.serve.optimade.backend import execute_query
adapter = adapter_from_providers([provider])
print("== The adapter serves ==")
print(f" {sorted(adapter.schema.all_entries)}")
print()
print("== OPTIMADE queries, executed against the database ==")
for entry_type, response_fields, filter_string in QUERIES:
results = list(
execute_query(
adapter,
[entry_type],
response_fields,
[], # unknown response fields: none
100, # response limit
0, # response offset
parse_optimade_filter(filter_string),
)
)
print(f" {entry_type}?filter={filter_string}")
for result in results:
rendered: dict[str, Any] = {name: result.values[name] for name in response_fields}
print(f" {rendered}")
if not results:
print(" (no matches)")
print()
def show_custom_ids(store: SqlStore) -> None:
"""`id_of` replaces the default '<entry type>-<sid>' scheme everywhere."""
def id_of(entry_type: str, sid: int, obj: Any) -> str:
label = getattr(obj, "title", None) or obj.name
return f"{entry_type}/{label.lower().replace(' ', '-')}"
provider = StoreEntryProvider(store, {"books": Book, "writers": Writer}, id_of=id_of)
print("== Custom ids ==")
property_keys = provider.property_keys("books")
for record in provider.records("books"):
print(f" {record[property_keys['id']]}")
for entry_id, related in provider.relationships("books").items():
for entry in related:
print(f" {entry_id} -> {entry.id} (type {entry.entry_type})")
def main() -> None:
store = populate()
provider = StoreEntryProvider(store, {"books": Book, "writers": Writer})
show_provider(provider)
show_queries(provider)
show_custom_ids(store)
if __name__ == "__main__":
main()