OTE-API OPTIMADE Strategies¶
This page provides documentation for the oteapi_optimade.strategies
submodule, where all the OTE-API OPTIMADE strategies are located.
These strategies will be available when setting up a server in an environment with oteapi-optimade installed.
filter
¶
Demo filter strategy.
OPTIMADEFilterStrategy
dataclass
¶
Filter Strategy.
Implements strategies:
("filterType", "OPTIMADE")
("filterType", "optimade")
("filterType", "OPTiMaDe")
Source code in oteapi_optimade/strategies/filter.py
@dataclass
class OPTIMADEFilterStrategy:
"""Filter Strategy.
**Implements strategies**:
- `("filterType", "OPTIMADE")`
- `("filterType", "optimade")`
- `("filterType", "OPTiMaDe")`
"""
filter_config: OPTIMADEFilterConfig
def initialize(
self, session: SessionUpdate | dict[str, Any] | None = None
) -> OPTIMADEFilterSession:
"""Initialize strategy.
This method will be called through the `/initialize` endpoint of the OTE-API
Services.
Configuration values, specifically URL query parameters, can be provided to the
OPTIMADE resource strategy through this filter strategy.
Workflow:
1. Compile received information.
2. Update session with compiled information.
Parameters:
session: A session-specific dictionary context.
Returns:
An update model of key/value-pairs to be stored in the
session-specific context from services.
"""
if session and isinstance(session, dict):
session = OPTIMADEFilterSession(**session)
elif session and isinstance(session, SessionUpdate):
session = OPTIMADEFilterSession(
**session.model_dump(exclude_defaults=True, exclude_unset=True)
)
else:
session = OPTIMADEFilterSession()
if session.optimade_config:
self.filter_config.configuration.update(
session.optimade_config.model_dump(
exclude_defaults=True, exclude_unset=True
)
)
optimade_config = self.filter_config.configuration.model_copy()
if not optimade_config.query_parameters:
optimade_config.query_parameters = OPTIMADEQueryParameters()
if self.filter_config.query:
LOGGER.debug("Setting filter from query.")
optimade_config.query_parameters.filter = self.filter_config.query
if self.filter_config.limit:
LOGGER.debug("Setting page_limit from limit.")
optimade_config.query_parameters.page_limit = self.filter_config.limit
return session.model_copy( # type: ignore[no-any-return]
update={
"optimade_config": optimade_config.model_copy(
update={
"query_parameters": optimade_config.query_parameters.model_dump(
exclude_defaults=True,
exclude_unset=True,
)
}
)
},
)
def get(
self,
session: dict[str, Any] | None = None, # noqa: ARG002
) -> SessionUpdate:
"""Execute the strategy.
This method will be called through the strategy-specific endpoint of the
OTE-API Services.
Parameters:
session: A session-specific dictionary context.
Returns:
An update model of key/value-pairs to be stored in the
session-specific context from services.
"""
return SessionUpdate()
get(self, session=None)
¶
Execute the strategy.
This method will be called through the strategy-specific endpoint of the OTE-API Services.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
session |
dict[str, Any] | None |
A session-specific dictionary context. |
None |
Returns:
Type | Description |
---|---|
SessionUpdate |
An update model of key/value-pairs to be stored in the session-specific context from services. |
Source code in oteapi_optimade/strategies/filter.py
def get(
self,
session: dict[str, Any] | None = None, # noqa: ARG002
) -> SessionUpdate:
"""Execute the strategy.
This method will be called through the strategy-specific endpoint of the
OTE-API Services.
Parameters:
session: A session-specific dictionary context.
Returns:
An update model of key/value-pairs to be stored in the
session-specific context from services.
"""
return SessionUpdate()
initialize(self, session=None)
¶
Initialize strategy.
This method will be called through the /initialize
endpoint of the OTE-API
Services.
Configuration values, specifically URL query parameters, can be provided to the OPTIMADE resource strategy through this filter strategy.
Workflow:
- Compile received information.
- Update session with compiled information.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
session |
SessionUpdate | dict[str, Any] | None |
A session-specific dictionary context. |
None |
Returns:
Type | Description |
---|---|
OPTIMADEFilterSession |
An update model of key/value-pairs to be stored in the session-specific context from services. |
Source code in oteapi_optimade/strategies/filter.py
def initialize(
self, session: SessionUpdate | dict[str, Any] | None = None
) -> OPTIMADEFilterSession:
"""Initialize strategy.
This method will be called through the `/initialize` endpoint of the OTE-API
Services.
Configuration values, specifically URL query parameters, can be provided to the
OPTIMADE resource strategy through this filter strategy.
Workflow:
1. Compile received information.
2. Update session with compiled information.
Parameters:
session: A session-specific dictionary context.
Returns:
An update model of key/value-pairs to be stored in the
session-specific context from services.
"""
if session and isinstance(session, dict):
session = OPTIMADEFilterSession(**session)
elif session and isinstance(session, SessionUpdate):
session = OPTIMADEFilterSession(
**session.model_dump(exclude_defaults=True, exclude_unset=True)
)
else:
session = OPTIMADEFilterSession()
if session.optimade_config:
self.filter_config.configuration.update(
session.optimade_config.model_dump(
exclude_defaults=True, exclude_unset=True
)
)
optimade_config = self.filter_config.configuration.model_copy()
if not optimade_config.query_parameters:
optimade_config.query_parameters = OPTIMADEQueryParameters()
if self.filter_config.query:
LOGGER.debug("Setting filter from query.")
optimade_config.query_parameters.filter = self.filter_config.query
if self.filter_config.limit:
LOGGER.debug("Setting page_limit from limit.")
optimade_config.query_parameters.page_limit = self.filter_config.limit
return session.model_copy( # type: ignore[no-any-return]
update={
"optimade_config": optimade_config.model_copy(
update={
"query_parameters": optimade_config.query_parameters.model_dump(
exclude_defaults=True,
exclude_unset=True,
)
}
)
},
)
parse
¶
Demo strategy class for text/json.
OPTIMADEParseStrategy
dataclass
¶
Parse strategy for JSON.
Implements strategies:
("mediaType", "application/vnd.optimade+json")
("mediaType", "application/vnd.OPTIMADE+json")
("mediaType", "application/vnd.OPTiMaDe+json")
("mediaType", "application/vnd.optimade+JSON")
("mediaType", "application/vnd.OPTIMADE+JSON")
("mediaType", "application/vnd.OPTiMaDe+JSON")
("mediaType", "application/vnd.optimade")
("mediaType", "application/vnd.OPTIMADE")
("mediaType", "application/vnd.OPTiMaDe")
Source code in oteapi_optimade/strategies/parse.py
@dataclass
class OPTIMADEParseStrategy:
"""Parse strategy for JSON.
**Implements strategies**:
- `("mediaType", "application/vnd.optimade+json")`
- `("mediaType", "application/vnd.OPTIMADE+json")`
- `("mediaType", "application/vnd.OPTiMaDe+json")`
- `("mediaType", "application/vnd.optimade+JSON")`
- `("mediaType", "application/vnd.OPTIMADE+JSON")`
- `("mediaType", "application/vnd.OPTiMaDe+JSON")`
- `("mediaType", "application/vnd.optimade")`
- `("mediaType", "application/vnd.OPTIMADE")`
- `("mediaType", "application/vnd.OPTiMaDe")`
"""
parse_config: OPTIMADEParseConfig
def initialize(
self,
session: dict[str, Any] | None = None, # noqa: ARG002
) -> SessionUpdate:
"""Initialize strategy.
This method will be called through the `/initialize` endpoint of the OTE-API
Services.
Parameters:
session: A session-specific dictionary context.
Returns:
An update model of key/value-pairs to be stored in the session-specific
context from services.
"""
return SessionUpdate()
def get(
self, session: SessionUpdate | dict[str, Any] | None = None
) -> OPTIMADEParseSession:
"""Request and parse an OPTIMADE response using OPT.
This method will be called through the strategy-specific endpoint of the
OTE-API Services.
Configuration values provided in `resource_config.configuration` take
precedence over the derived values from `downloadUrl`.
Workflow:
1. Request OPTIMADE response.
2. Parse as an OPTIMADE Python tools (OPT) pydantic response model.
Parameters:
session: A session-specific dictionary-like context.
Returns:
An update model of key/value-pairs to be stored in the session-specific
context from services.
"""
if session and isinstance(session, dict):
session = OPTIMADEParseSession(**session)
elif session and isinstance(session, SessionUpdate):
session = OPTIMADEParseSession(
**session.model_dump(exclude_defaults=True, exclude_unset=True)
)
else:
session = OPTIMADEParseSession()
if session.optimade_config:
self.parse_config.configuration.update(
session.optimade_config.model_dump(
exclude_defaults=True, exclude_unset=True
)
)
cache = DataCache(self.parse_config.configuration.datacache_config)
if self.parse_config.downloadUrl in cache:
response: dict[str, Any] = cache.get(self.parse_config.downloadUrl)
elif (
self.parse_config.configuration.datacache_config.accessKey
and self.parse_config.configuration.datacache_config.accessKey in cache
):
response = cache.get(
self.parse_config.configuration.datacache_config.accessKey
)
else:
download_config = self.parse_config.model_copy()
session.update(
create_strategy(StrategyType.DOWNLOAD, download_config).initialize(
session.model_dump(exclude_defaults=True, exclude_unset=True)
)
)
session.update(
create_strategy(StrategyType.DOWNLOAD, download_config).get(
session.model_dump(exclude_defaults=True, exclude_unset=True)
)
)
response = {"json": json.loads(cache.get(session.pop("key")))}
if (
not response.get("ok", True)
or (
response.get("status_code", 200) < 200
or response.get("status_code", 200) >= 300
)
or "errors" in response.get("json", {})
):
# Error response
try:
response_object = ErrorResponse(**response.get("json", {}))
except ValidationError as exc:
error_message = "Could not validate an error response."
LOGGER.error(
"%s\nValidationError: " "%s\nresponse=%r",
error_message,
exc,
response,
)
raise OPTIMADEParseError(error_message) from exc
else:
# Successful response
response_model = self.parse_config.downloadUrl.response_model()
LOGGER.debug("response_model=%r", response_model)
if response_model:
if not isinstance(response_model, tuple):
response_model = (response_model,)
for model_cls in response_model:
try:
response_object = model_cls(**response.get("json", {}))
except ValidationError:
pass
else:
break
else:
error_message = "Could not validate for an expected response model."
LOGGER.error(
"%s\nURL=%r\n" "response_models=%r\nresponse=%s",
error_message,
self.parse_config.downloadUrl,
response_model,
response,
)
raise OPTIMADEParseError(error_message)
else:
# No "endpoint" or unknown
LOGGER.debug("No response_model, using Success response model.")
try:
response_object = Success(**response.get("json", {}))
except ValidationError as exc:
error_message = "Unknown or unparseable endpoint."
LOGGER.error(
"%s\nValidatonError: %s\n"
"URL=%r\nendpoint=%r\nresponse_model=%r\nresponse=%s",
error_message,
exc,
self.parse_config.downloadUrl,
self.parse_config.downloadUrl.endpoint,
response_model,
response,
)
raise OPTIMADEParseError(error_message) from exc
session.optimade_response_model = (
response_object.__class__.__module__,
response_object.__class__.__name__,
)
session.optimade_response = response_object.model_dump(exclude_unset=True)
if session.optimade_config and session.optimade_config.query_parameters:
session = session.model_copy(
update={
"optimade_config": session.optimade_config.model_copy(
update={
"query_parameters": session.optimade_config.query_parameters.model_dump(
exclude_defaults=True,
exclude_unset=True,
)
}
)
}
)
if TYPE_CHECKING: # pragma: no cover
assert isinstance(session, OPTIMADEParseSession) # nosec
return session
get(self, session=None)
¶
Request and parse an OPTIMADE response using OPT.
This method will be called through the strategy-specific endpoint of the OTE-API Services.
Configuration values provided in resource_config.configuration
take
precedence over the derived values from downloadUrl
.
Workflow:
- Request OPTIMADE response.
- Parse as an OPTIMADE Python tools (OPT) pydantic response model.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
session |
SessionUpdate | dict[str, Any] | None |
A session-specific dictionary-like context. |
None |
Returns:
Type | Description |
---|---|
OPTIMADEParseSession |
An update model of key/value-pairs to be stored in the session-specific context from services. |
Source code in oteapi_optimade/strategies/parse.py
def get(
self, session: SessionUpdate | dict[str, Any] | None = None
) -> OPTIMADEParseSession:
"""Request and parse an OPTIMADE response using OPT.
This method will be called through the strategy-specific endpoint of the
OTE-API Services.
Configuration values provided in `resource_config.configuration` take
precedence over the derived values from `downloadUrl`.
Workflow:
1. Request OPTIMADE response.
2. Parse as an OPTIMADE Python tools (OPT) pydantic response model.
Parameters:
session: A session-specific dictionary-like context.
Returns:
An update model of key/value-pairs to be stored in the session-specific
context from services.
"""
if session and isinstance(session, dict):
session = OPTIMADEParseSession(**session)
elif session and isinstance(session, SessionUpdate):
session = OPTIMADEParseSession(
**session.model_dump(exclude_defaults=True, exclude_unset=True)
)
else:
session = OPTIMADEParseSession()
if session.optimade_config:
self.parse_config.configuration.update(
session.optimade_config.model_dump(
exclude_defaults=True, exclude_unset=True
)
)
cache = DataCache(self.parse_config.configuration.datacache_config)
if self.parse_config.downloadUrl in cache:
response: dict[str, Any] = cache.get(self.parse_config.downloadUrl)
elif (
self.parse_config.configuration.datacache_config.accessKey
and self.parse_config.configuration.datacache_config.accessKey in cache
):
response = cache.get(
self.parse_config.configuration.datacache_config.accessKey
)
else:
download_config = self.parse_config.model_copy()
session.update(
create_strategy(StrategyType.DOWNLOAD, download_config).initialize(
session.model_dump(exclude_defaults=True, exclude_unset=True)
)
)
session.update(
create_strategy(StrategyType.DOWNLOAD, download_config).get(
session.model_dump(exclude_defaults=True, exclude_unset=True)
)
)
response = {"json": json.loads(cache.get(session.pop("key")))}
if (
not response.get("ok", True)
or (
response.get("status_code", 200) < 200
or response.get("status_code", 200) >= 300
)
or "errors" in response.get("json", {})
):
# Error response
try:
response_object = ErrorResponse(**response.get("json", {}))
except ValidationError as exc:
error_message = "Could not validate an error response."
LOGGER.error(
"%s\nValidationError: " "%s\nresponse=%r",
error_message,
exc,
response,
)
raise OPTIMADEParseError(error_message) from exc
else:
# Successful response
response_model = self.parse_config.downloadUrl.response_model()
LOGGER.debug("response_model=%r", response_model)
if response_model:
if not isinstance(response_model, tuple):
response_model = (response_model,)
for model_cls in response_model:
try:
response_object = model_cls(**response.get("json", {}))
except ValidationError:
pass
else:
break
else:
error_message = "Could not validate for an expected response model."
LOGGER.error(
"%s\nURL=%r\n" "response_models=%r\nresponse=%s",
error_message,
self.parse_config.downloadUrl,
response_model,
response,
)
raise OPTIMADEParseError(error_message)
else:
# No "endpoint" or unknown
LOGGER.debug("No response_model, using Success response model.")
try:
response_object = Success(**response.get("json", {}))
except ValidationError as exc:
error_message = "Unknown or unparseable endpoint."
LOGGER.error(
"%s\nValidatonError: %s\n"
"URL=%r\nendpoint=%r\nresponse_model=%r\nresponse=%s",
error_message,
exc,
self.parse_config.downloadUrl,
self.parse_config.downloadUrl.endpoint,
response_model,
response,
)
raise OPTIMADEParseError(error_message) from exc
session.optimade_response_model = (
response_object.__class__.__module__,
response_object.__class__.__name__,
)
session.optimade_response = response_object.model_dump(exclude_unset=True)
if session.optimade_config and session.optimade_config.query_parameters:
session = session.model_copy(
update={
"optimade_config": session.optimade_config.model_copy(
update={
"query_parameters": session.optimade_config.query_parameters.model_dump(
exclude_defaults=True,
exclude_unset=True,
)
}
)
}
)
if TYPE_CHECKING: # pragma: no cover
assert isinstance(session, OPTIMADEParseSession) # nosec
return session
initialize(self, session=None)
¶
Initialize strategy.
This method will be called through the /initialize
endpoint of the OTE-API
Services.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
session |
dict[str, Any] | None |
A session-specific dictionary context. |
None |
Returns:
Type | Description |
---|---|
SessionUpdate |
An update model of key/value-pairs to be stored in the session-specific context from services. |
Source code in oteapi_optimade/strategies/parse.py
def initialize(
self,
session: dict[str, Any] | None = None, # noqa: ARG002
) -> SessionUpdate:
"""Initialize strategy.
This method will be called through the `/initialize` endpoint of the OTE-API
Services.
Parameters:
session: A session-specific dictionary context.
Returns:
An update model of key/value-pairs to be stored in the session-specific
context from services.
"""
return SessionUpdate()
resource
¶
OPTIMADE resource strategy.
OPTIMADEResourceStrategy
dataclass
¶
OPTIMADE Resource Strategy.
Implements strategies:
("accessService", "optimade")
("accessService", "OPTIMADE")
("accessService", "OPTiMaDe")
("accessService", "optimade+dlite")
("accessService", "OPTIMADE+dlite")
("accessService", "OPTiMaDe+dlite")
("accessService", "optimade+DLite")
("accessService", "OPTIMADE+DLite")
("accessService", "OPTiMaDe+DLite")
Source code in oteapi_optimade/strategies/resource.py
@dataclass
class OPTIMADEResourceStrategy:
"""OPTIMADE Resource Strategy.
**Implements strategies**:
- `("accessService", "optimade")`
- `("accessService", "OPTIMADE")`
- `("accessService", "OPTiMaDe")`
- `("accessService", "optimade+dlite")`
- `("accessService", "OPTIMADE+dlite")`
- `("accessService", "OPTiMaDe+dlite")`
- `("accessService", "optimade+DLite")`
- `("accessService", "OPTIMADE+DLite")`
- `("accessService", "OPTiMaDe+DLite")`
"""
resource_config: OPTIMADEResourceConfig
def initialize(
self, session: dict[str, Any] | None = None
) -> SessionUpdate | DLiteSessionUpdate:
"""Initialize strategy.
This method will be called through the `/initialize` endpoint of the OTE-API
Services.
Parameters:
session: A session-specific dictionary context.
Returns:
An update model of key/value-pairs to be stored in the session-specific
context from services.
"""
if use_dlite(
self.resource_config.accessService,
self.resource_config.configuration.use_dlite,
):
return DLiteSessionUpdate(collection_id=get_collection(session).uuid)
return SessionUpdate()
def get(
self, session: SessionUpdate | dict[str, Any] | None = None
) -> OPTIMADEResourceSession:
"""Execute an OPTIMADE query to `accessUrl`.
This method will be called through the strategy-specific endpoint of the
OTE-API Services.
Configuration values provided in `resource_config.configuration` take
precedence over the derived values from `accessUrl`.
Workflow:
1. Update configuration according to session.
2. Deconstruct `accessUrl` (done partly by
`oteapi_optimade.models.custom_types.OPTIMADEUrl`).
3. Reconstruct the complete query URL.
4. Send query.
5. Store result in data cache.
Parameters:
session: A session-specific dictionary-like context.
Returns:
An update model of key/value-pairs to be stored in the session-specific
context from services.
"""
if session and isinstance(session, dict):
session = OPTIMADEResourceSession(**session)
elif session and isinstance(session, SessionUpdate):
session = OPTIMADEResourceSession(
**session.model_dump(exclude_defaults=True, exclude_unset=True)
)
else:
session = OPTIMADEResourceSession()
if session.optimade_config:
self.resource_config.configuration.update(
session.optimade_config.model_dump(
exclude_defaults=True, exclude_unset=True
)
)
optimade_endpoint = self.resource_config.accessUrl.endpoint or "structures"
optimade_query = (
self.resource_config.configuration.query_parameters
or OPTIMADEQueryParameters()
)
LOGGER.debug("resource_config: %r", self.resource_config)
if self.resource_config.accessUrl.query:
parsed_query = parse_qs(self.resource_config.accessUrl.query)
for field, value in parsed_query.items():
# Only use the latest defined value for any parameter
if field not in optimade_query.model_fields_set:
LOGGER.debug(
"Setting %r from accessUrl (value=%r)", field, value[-1]
)
setattr(optimade_query, field, value[-1])
LOGGER.debug("optimade_query after update: %r", optimade_query)
optimade_url = OPTIMADEUrl(
f"{self.resource_config.accessUrl.base_url}"
f"/{self.resource_config.accessUrl.version or 'v1'}"
f"/{optimade_endpoint}?{optimade_query.generate_query_string()}"
)
LOGGER.debug("OPTIMADE URL to be requested: %s", optimade_url)
# Set cache access key to the full OPTIMADE URL.
self.resource_config.configuration.datacache_config.accessKey = optimade_url
# Perform query
response = requests.get(
optimade_url,
allow_redirects=True,
timeout=(3, 27), # timeout in seconds (connect, read)
)
if optimade_query.response_format and optimade_query.response_format != "json":
error_message = (
"Can only handle JSON responses for now. Requested response format: "
f"{optimade_query.response_format!r}"
)
raise NotImplementedError(error_message)
cache = DataCache(config=self.resource_config.configuration.datacache_config)
cache.add(
{
"status_code": response.status_code,
"ok": response.ok,
"json": response.json(),
}
)
parse_with_dlite = use_dlite(
self.resource_config.accessService,
self.resource_config.configuration.use_dlite,
)
parse_mediaType = (
"application/vnd."
f"{self.resource_config.accessService.split('+', maxsplit=1)[0]}"
)
if parse_with_dlite:
parse_mediaType += "+DLite"
elif optimade_query.response_format:
parse_mediaType += f"+{optimade_query.response_format}"
parse_config = {
"downloadUrl": optimade_url,
"mediaType": parse_mediaType,
"configuration": {
"datacache_config": self.resource_config.configuration.datacache_config,
},
}
LOGGER.debug("parse_config: %r", parse_config)
session.update(
create_strategy(StrategyType.PARSE, parse_config).initialize(
session.model_dump(exclude_defaults=True, exclude_unset=True)
)
)
session.update(
create_strategy(StrategyType.PARSE, parse_config).get(
session.model_dump(exclude_defaults=True, exclude_unset=True)
)
)
if not all(
_ in session for _ in ("optimade_response", "optimade_response_model")
):
base_error_message = (
"Could not retrieve response from OPTIMADE parse strategy."
)
LOGGER.error(
"%s\n"
"optimade_response=%r\n"
"optimade_response_model=%r\n"
"session fields=%r",
base_error_message,
session.get("optimade_response"),
session.get("optimade_response_model"),
list(session.keys()),
)
raise OPTIMADEParseError(base_error_message)
optimade_response_model_module, optimade_response_model_name = session.pop(
"optimade_response_model"
)
optimade_response_dict = session.pop("optimade_response")
# Parse response using the provided model
try:
optimade_response_model: type[OPTIMADEResponse] = getattr(
importlib.import_module(optimade_response_model_module),
optimade_response_model_name,
)
optimade_response = optimade_response_model(**optimade_response_dict)
except (ImportError, AttributeError) as exc:
base_error_message = "Could not import the response model."
LOGGER.error(
"%s\n"
"ImportError: %s\n"
"optimade_response_model_module=%r\n"
"optimade_response_model_name=%r",
base_error_message,
exc,
optimade_response_model_module,
optimade_response_model_name,
)
raise OPTIMADEParseError(base_error_message) from exc
except ValidationError as exc:
base_error_message = "Could not validate the response model."
LOGGER.error(
"%s\n"
"ValidationError: %s\n"
"optimade_response_model_module=%r\n"
"optimade_response_model_name=%r",
base_error_message,
exc,
optimade_response_model_module,
optimade_response_model_name,
)
raise OPTIMADEParseError(base_error_message) from exc
if isinstance(optimade_response, ErrorResponse):
optimade_resources = optimade_response.errors
session.optimade_resource_model = (
f"{OptimadeError.__module__}:OptimadeError"
)
elif isinstance(optimade_response, ReferenceResponseMany):
optimade_resources = [
(
Reference(entry).as_dict
if isinstance(entry, dict)
else Reference(entry.model_dump()).as_dict
)
for entry in optimade_response.data
]
session.optimade_resource_model = f"{Reference.__module__}:Reference"
elif isinstance(optimade_response, ReferenceResponseOne):
optimade_resources = [
(
Reference(optimade_response.data).as_dict
if isinstance(optimade_response.data, dict)
else Reference(optimade_response.data.model_dump()).as_dict
)
]
session.optimade_resource_model = f"{Reference.__module__}:Reference"
elif isinstance(optimade_response, StructureResponseMany):
optimade_resources = [
(
Structure(entry).as_dict
if isinstance(entry, dict)
else Structure(entry.model_dump()).as_dict
)
for entry in optimade_response.data
]
session.optimade_resource_model = f"{Structure.__module__}:Structure"
elif isinstance(optimade_response, StructureResponseOne):
optimade_resources = [
(
Structure(optimade_response.data).as_dict
if isinstance(optimade_response.data, dict)
else Structure(optimade_response.data.model_dump()).as_dict
)
]
session.optimade_resource_model = f"{Structure.__module__}:Structure"
else:
LOGGER.error(
"Could not parse response as errors, references or structures. "
"Response:\n%r",
optimade_response,
)
error_message = (
"Could not retrieve errors, references or structures from response "
f"from {optimade_url}. It could be a valid OPTIMADE API response, "
"however it may not be supported by OTEAPI-OPTIMADE. It may also be an "
"invalid response completely."
)
raise OPTIMADEParseError(error_message)
session.optimade_resources = [
resource if isinstance(resource, dict) else resource.model_dump()
for resource in optimade_resources
]
if session.optimade_config and session.optimade_config.query_parameters:
session = session.model_copy(
update={
"optimade_config": session.optimade_config.model_copy(
update={
"query_parameters": session.optimade_config.query_parameters.model_dump(
exclude_defaults=True,
exclude_unset=True,
)
}
)
}
)
if TYPE_CHECKING: # pragma: no cover
assert isinstance(session, OPTIMADEResourceSession) # nosec
return session
get(self, session=None)
¶
Execute an OPTIMADE query to accessUrl
.
This method will be called through the strategy-specific endpoint of the OTE-API Services.
Configuration values provided in resource_config.configuration
take
precedence over the derived values from accessUrl
.
Workflow:
1. Update configuration according to session.
2. Deconstruct accessUrl
(done partly by
oteapi_optimade.models.custom_types.OPTIMADEUrl
).
3. Reconstruct the complete query URL.
4. Send query.
5. Store result in data cache.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
session |
SessionUpdate | dict[str, Any] | None |
A session-specific dictionary-like context. |
None |
Returns:
Type | Description |
---|---|
OPTIMADEResourceSession |
An update model of key/value-pairs to be stored in the session-specific context from services. |
Source code in oteapi_optimade/strategies/resource.py
def get(
self, session: SessionUpdate | dict[str, Any] | None = None
) -> OPTIMADEResourceSession:
"""Execute an OPTIMADE query to `accessUrl`.
This method will be called through the strategy-specific endpoint of the
OTE-API Services.
Configuration values provided in `resource_config.configuration` take
precedence over the derived values from `accessUrl`.
Workflow:
1. Update configuration according to session.
2. Deconstruct `accessUrl` (done partly by
`oteapi_optimade.models.custom_types.OPTIMADEUrl`).
3. Reconstruct the complete query URL.
4. Send query.
5. Store result in data cache.
Parameters:
session: A session-specific dictionary-like context.
Returns:
An update model of key/value-pairs to be stored in the session-specific
context from services.
"""
if session and isinstance(session, dict):
session = OPTIMADEResourceSession(**session)
elif session and isinstance(session, SessionUpdate):
session = OPTIMADEResourceSession(
**session.model_dump(exclude_defaults=True, exclude_unset=True)
)
else:
session = OPTIMADEResourceSession()
if session.optimade_config:
self.resource_config.configuration.update(
session.optimade_config.model_dump(
exclude_defaults=True, exclude_unset=True
)
)
optimade_endpoint = self.resource_config.accessUrl.endpoint or "structures"
optimade_query = (
self.resource_config.configuration.query_parameters
or OPTIMADEQueryParameters()
)
LOGGER.debug("resource_config: %r", self.resource_config)
if self.resource_config.accessUrl.query:
parsed_query = parse_qs(self.resource_config.accessUrl.query)
for field, value in parsed_query.items():
# Only use the latest defined value for any parameter
if field not in optimade_query.model_fields_set:
LOGGER.debug(
"Setting %r from accessUrl (value=%r)", field, value[-1]
)
setattr(optimade_query, field, value[-1])
LOGGER.debug("optimade_query after update: %r", optimade_query)
optimade_url = OPTIMADEUrl(
f"{self.resource_config.accessUrl.base_url}"
f"/{self.resource_config.accessUrl.version or 'v1'}"
f"/{optimade_endpoint}?{optimade_query.generate_query_string()}"
)
LOGGER.debug("OPTIMADE URL to be requested: %s", optimade_url)
# Set cache access key to the full OPTIMADE URL.
self.resource_config.configuration.datacache_config.accessKey = optimade_url
# Perform query
response = requests.get(
optimade_url,
allow_redirects=True,
timeout=(3, 27), # timeout in seconds (connect, read)
)
if optimade_query.response_format and optimade_query.response_format != "json":
error_message = (
"Can only handle JSON responses for now. Requested response format: "
f"{optimade_query.response_format!r}"
)
raise NotImplementedError(error_message)
cache = DataCache(config=self.resource_config.configuration.datacache_config)
cache.add(
{
"status_code": response.status_code,
"ok": response.ok,
"json": response.json(),
}
)
parse_with_dlite = use_dlite(
self.resource_config.accessService,
self.resource_config.configuration.use_dlite,
)
parse_mediaType = (
"application/vnd."
f"{self.resource_config.accessService.split('+', maxsplit=1)[0]}"
)
if parse_with_dlite:
parse_mediaType += "+DLite"
elif optimade_query.response_format:
parse_mediaType += f"+{optimade_query.response_format}"
parse_config = {
"downloadUrl": optimade_url,
"mediaType": parse_mediaType,
"configuration": {
"datacache_config": self.resource_config.configuration.datacache_config,
},
}
LOGGER.debug("parse_config: %r", parse_config)
session.update(
create_strategy(StrategyType.PARSE, parse_config).initialize(
session.model_dump(exclude_defaults=True, exclude_unset=True)
)
)
session.update(
create_strategy(StrategyType.PARSE, parse_config).get(
session.model_dump(exclude_defaults=True, exclude_unset=True)
)
)
if not all(
_ in session for _ in ("optimade_response", "optimade_response_model")
):
base_error_message = (
"Could not retrieve response from OPTIMADE parse strategy."
)
LOGGER.error(
"%s\n"
"optimade_response=%r\n"
"optimade_response_model=%r\n"
"session fields=%r",
base_error_message,
session.get("optimade_response"),
session.get("optimade_response_model"),
list(session.keys()),
)
raise OPTIMADEParseError(base_error_message)
optimade_response_model_module, optimade_response_model_name = session.pop(
"optimade_response_model"
)
optimade_response_dict = session.pop("optimade_response")
# Parse response using the provided model
try:
optimade_response_model: type[OPTIMADEResponse] = getattr(
importlib.import_module(optimade_response_model_module),
optimade_response_model_name,
)
optimade_response = optimade_response_model(**optimade_response_dict)
except (ImportError, AttributeError) as exc:
base_error_message = "Could not import the response model."
LOGGER.error(
"%s\n"
"ImportError: %s\n"
"optimade_response_model_module=%r\n"
"optimade_response_model_name=%r",
base_error_message,
exc,
optimade_response_model_module,
optimade_response_model_name,
)
raise OPTIMADEParseError(base_error_message) from exc
except ValidationError as exc:
base_error_message = "Could not validate the response model."
LOGGER.error(
"%s\n"
"ValidationError: %s\n"
"optimade_response_model_module=%r\n"
"optimade_response_model_name=%r",
base_error_message,
exc,
optimade_response_model_module,
optimade_response_model_name,
)
raise OPTIMADEParseError(base_error_message) from exc
if isinstance(optimade_response, ErrorResponse):
optimade_resources = optimade_response.errors
session.optimade_resource_model = (
f"{OptimadeError.__module__}:OptimadeError"
)
elif isinstance(optimade_response, ReferenceResponseMany):
optimade_resources = [
(
Reference(entry).as_dict
if isinstance(entry, dict)
else Reference(entry.model_dump()).as_dict
)
for entry in optimade_response.data
]
session.optimade_resource_model = f"{Reference.__module__}:Reference"
elif isinstance(optimade_response, ReferenceResponseOne):
optimade_resources = [
(
Reference(optimade_response.data).as_dict
if isinstance(optimade_response.data, dict)
else Reference(optimade_response.data.model_dump()).as_dict
)
]
session.optimade_resource_model = f"{Reference.__module__}:Reference"
elif isinstance(optimade_response, StructureResponseMany):
optimade_resources = [
(
Structure(entry).as_dict
if isinstance(entry, dict)
else Structure(entry.model_dump()).as_dict
)
for entry in optimade_response.data
]
session.optimade_resource_model = f"{Structure.__module__}:Structure"
elif isinstance(optimade_response, StructureResponseOne):
optimade_resources = [
(
Structure(optimade_response.data).as_dict
if isinstance(optimade_response.data, dict)
else Structure(optimade_response.data.model_dump()).as_dict
)
]
session.optimade_resource_model = f"{Structure.__module__}:Structure"
else:
LOGGER.error(
"Could not parse response as errors, references or structures. "
"Response:\n%r",
optimade_response,
)
error_message = (
"Could not retrieve errors, references or structures from response "
f"from {optimade_url}. It could be a valid OPTIMADE API response, "
"however it may not be supported by OTEAPI-OPTIMADE. It may also be an "
"invalid response completely."
)
raise OPTIMADEParseError(error_message)
session.optimade_resources = [
resource if isinstance(resource, dict) else resource.model_dump()
for resource in optimade_resources
]
if session.optimade_config and session.optimade_config.query_parameters:
session = session.model_copy(
update={
"optimade_config": session.optimade_config.model_copy(
update={
"query_parameters": session.optimade_config.query_parameters.model_dump(
exclude_defaults=True,
exclude_unset=True,
)
}
)
}
)
if TYPE_CHECKING: # pragma: no cover
assert isinstance(session, OPTIMADEResourceSession) # nosec
return session
initialize(self, session=None)
¶
Initialize strategy.
This method will be called through the /initialize
endpoint of the OTE-API
Services.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
session |
dict[str, Any] | None |
A session-specific dictionary context. |
None |
Returns:
Type | Description |
---|---|
SessionUpdate | DLiteSessionUpdate |
An update model of key/value-pairs to be stored in the session-specific context from services. |
Source code in oteapi_optimade/strategies/resource.py
def initialize(
self, session: dict[str, Any] | None = None
) -> SessionUpdate | DLiteSessionUpdate:
"""Initialize strategy.
This method will be called through the `/initialize` endpoint of the OTE-API
Services.
Parameters:
session: A session-specific dictionary context.
Returns:
An update model of key/value-pairs to be stored in the session-specific
context from services.
"""
if use_dlite(
self.resource_config.accessService,
self.resource_config.configuration.use_dlite,
):
return DLiteSessionUpdate(collection_id=get_collection(session).uuid)
return SessionUpdate()
use_dlite(access_service, use_dlite_flag)
¶
Determine whether DLite should be utilized in the Resource strategy.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
access_service |
str |
The accessService value from the resource's configuration. |
required |
use_dlite_flag |
bool |
The strategy-specific |
required |
Returns:
Type | Description |
---|---|
bool |
Based on the accessService value, then whether DLite should be used or not. |
Source code in oteapi_optimade/strategies/resource.py
def use_dlite(access_service: str, use_dlite_flag: bool) -> bool:
"""Determine whether DLite should be utilized in the Resource strategy.
Parameters:
access_service: The accessService value from the resource's configuration.
use_dlite_flag: The strategy-specific `use_dlite` configuration option.
Returns:
Based on the accessService value, then whether DLite should be used or not.
"""
if (
any(dlite_form in access_service for dlite_form in ["DLite", "dlite"])
or use_dlite_flag
):
if oteapi_dlite_version is None:
error_message = (
"OTEAPI-DLite is not found on the system. This is required to use "
"DLite with the OTEAPI-OPTIMADE strategies."
)
raise MissingDependency(error_message)
return True
return False