Skip to content

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 use_dlite configuration option.

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