diff --git a/.github/utils/end2end_test.py b/.github/utils/end2end_test.py index 617d9f3..108f72d 100755 --- a/.github/utils/end2end_test.py +++ b/.github/utils/end2end_test.py @@ -54,7 +54,7 @@ def main(oteapi_url: str) -> None: from otelib import OTEClient from pydantic import ValidationError - from oteapi_optimade.models import OPTIMADEResourceSession + from oteapi_optimade.models import OPTIMADEResourceResult client = OTEClient(oteapi_url) @@ -66,24 +66,25 @@ def main(oteapi_url: str) -> None: } source = client.create_dataresource( + resourceType="OPTIMADE/structures", accessService="OPTIMADE", accessUrl=OPTIMADE_URL, configuration=config, ) - session = source.get() + output = source.get() error_message = "Could not parse returned session as an OPTIMADEResourceStrategy." try: - session = OPTIMADEResourceSession(**json.loads(session)) + output = OPTIMADEResourceResult(**json.loads(output)) except ValidationError as exc_: raise RuntimeError(error_message) from exc_ - assert session.optimade_resource_model == f"{Structure.__module__}:Structure" - assert len(session.optimade_resources) == 2 + assert output.optimade_resource_model == f"{Structure.__module__}:Structure" + assert len(output.optimade_resources) == 2 - for resource in tuple(session.optimade_resources): + for resource in tuple(output.optimade_resources): parsed_resource = Structure(resource) assert parsed_resource.id in ["mpf_1", "mpf_110"] @@ -96,18 +97,18 @@ def main(oteapi_url: str) -> None: ) pipeline = query >> source - session = pipeline.get() + output = pipeline.get() try: - # Should be an OPTIMADEResourceSession because `source` is last in the pipeline - session = OPTIMADEResourceSession(**json.loads(session)) + # Should be an OPTIMADEResourceResult because `source` is last in the pipeline + output = OPTIMADEResourceResult(**json.loads(output)) except ValidationError as exc_: raise RuntimeError(error_message) from exc_ - assert session.optimade_resource_model == f"{Structure.__module__}:Structure" - assert len(session.optimade_resources) == 4 + assert output.optimade_resource_model == f"{Structure.__module__}:Structure" + assert len(output.optimade_resources) == 4 - for resource in tuple(session.optimade_resources): + for resource in tuple(output.optimade_resources): parsed_resource = Structure(resource) assert parsed_resource.id in [ "mpf_1", @@ -132,11 +133,11 @@ def main(oteapi_url: str) -> None: # Configuration PORT = os.getenv("OTEAPI_PORT", "8080") OTEAPI_SERVICE_URL = f"http://localhost:{PORT}" - OTEAPI_PREFIX = os.getenv("OTEAPI_prefix", "/api/v1") # noqa: SIM112 + OTEAPI_PREFIX = os.getenv("OTEAPI_PREFIX", "/api/v1") OPTIMADE_URL = f"http://{os.getenv('OPTIMADE_HOST', 'localhost')}:{os.getenv('OPTIMADE_PORT', '5000')}/" - if "OTEAPI_prefix" not in os.environ: + if "OTEAPI_PREFIX" not in os.environ: # Set environment variables - os.environ["OTEAPI_prefix"] = OTEAPI_PREFIX # noqa: SIM112 + os.environ["OTEAPI_PREFIX"] = OTEAPI_PREFIX try: _check_service_availability(service_url=OTEAPI_SERVICE_URL) diff --git a/.github/utils/requirements_upload.txt b/.github/utils/requirements_upload.txt new file mode 100644 index 0000000..4c18ece --- /dev/null +++ b/.github/utils/requirements_upload.txt @@ -0,0 +1 @@ +entities-service[cli] @ git+https://github.com/SINTEF/entities-service.git@v0.7.0 diff --git a/.github/workflows/cd_upload_entities.yml b/.github/workflows/cd_upload_entities.yml new file mode 100644 index 0000000..fcf48ec --- /dev/null +++ b/.github/workflows/cd_upload_entities.yml @@ -0,0 +1,65 @@ +name: CD - Upload Entities + +on: + push: + branches: [main] + pull_request: + +jobs: + update-public-entities: + name: Update public entities + runs-on: ubuntu-latest + if: github.repository_owner == 'SINTEF' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -U setuptools wheel + pip install -r .github/utils/requirements_upload.txt + + - name: Check 'entities-service' CLI version + run: entities-service --version + + - name: Gather Entities + id: gather_entities + run: | + if [ "${{ github.event_name }}" == "push" ]; then + SHA_BEFORE="${{ github.event.before }}" + else + SHA_BEFORE="${{ github.event.pull_request.base.sha }}" + fi + + git diff --name-only ${SHA_BEFORE} | grep -E '^oteapi_optimade/dlite/entities/.*\.ya?ml$' > entities.txt ||: + + if [ -s entities.txt ]; then + echo "relevant_entities=true" >> $GITHUB_OUTPUT + + echo "Relevant Entities:" + cat entities.txt + else + echo "relevant_entities=false" >> $GITHUB_OUTPUT + + echo "No entities to validate (and upload)." + exit 0 + fi + + - name: Validate Entities + if: steps.gather_entities.outputs.relevant_entities == 'true' + run: cat entities.txt | entities-service validate --strict --format=yaml - + + - name: Upload Entities + if: steps.gather_entities.outputs.relevant_entities == 'true' && github.event_name == 'push' && github.ref_name == 'main' + run: cat entities.txt | entities-service upload --auto-confirm --strict --format=yaml - + env: + ENTITIES_SERVICE_ACCESS_TOKEN: ${{ secrets.ENTITIES_SERVICE_ACCESS_TOKEN }} diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index 2fa55c4..922c8d3 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -47,7 +47,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11"] + python-version: ["3.9", "3.10", "3.11", "3.12"] os: - ["ubuntu-latest", "linux"] - ["windows-latest", "windows"] @@ -90,7 +90,7 @@ jobs: env: OTEAPI_PORT: 8080 OPTIMADE_PORT: 5000 - OTEAPI_prefix: "/api/v1" + OTEAPI_PREFIX: "/api/v1" services: redis: @@ -129,7 +129,7 @@ jobs: --env "OTEAPI_REDIS_TYPE=redis" \ --env "OTEAPI_REDIS_HOST=localhost" \ --env "OTEAPI_REDIS_PORT=6379" \ - --env "OTEAPI_prefix=${OTEAPI_prefix}" \ + --env "OTEAPI_PREFIX=${OTEAPI_PREFIX}" \ --network "host" \ --volume "${PWD}:/oteapi-optimade" \ --entrypoint "" \ @@ -139,11 +139,7 @@ jobs: .github/utils/wait_for_it.sh localhost:${OTEAPI_PORT} -t 240 sleep 5 env: - # Use version 1.20240228.345 until - # https://github.com/SINTEF/oteapi-optimade/issues/213 has been resolved. - # See also - # https://github.com/EMMC-ASBL/oteapi-services/tree/8306d7212419764fb87e5cefdb5a869db9c68ef7?tab=readme-ov-file#open-translation-environment-ote-api - DOCKER_OTEAPI_VERSION: '1.20240228.345' + DOCKER_OTEAPI_VERSION: 'latest' - name: Run end-2-end tests run: python .github/utils/end2end_test.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fedc49f..614eda3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ ci: autoupdate_branch: 'main' autoupdate_commit_msg: '[pre-commit.ci] pre-commit autoupdate' autoupdate_schedule: 'weekly' - skip: [] + skip: [validate-entities] # doesn't seem to work. Also, this is checked with dedicated CD workflow submodules: false # hooks @@ -26,6 +26,7 @@ repos: - id: debug-statements - id: end-of-file-fixer - id: mixed-line-ending + exclude: ^CHANGELOG.md$ - id: name-tests-test args: ["--pytest-test-first"] - id: trailing-whitespace @@ -78,3 +79,13 @@ repos: - "--package-dir=oteapi_optimade" - "--full-docs-folder=models" - id: docs-landing-page + + # entities-service can validate SOFT/DLite entities + # More information can be found in the repository README: + # https://github.com/SINTEF/entities-service?tab=readme-ov-file#readme + - repo: https://github.com/SINTEF/entities-service + rev: v0.7.0 + hooks: + - id: validate-entities + additional_dependencies: [".[cli]"] + files: ^oteapi_optimade/dlite/entities/.*\.ya?ml$ diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..bcf434c --- /dev/null +++ b/compose.yaml @@ -0,0 +1,44 @@ +services: + oteapi: + image: ghcr.io/emmc-asbl/oteapi:${DOCKER_OTEAPI_VERSION:-latest} + ports: + - "${OTEAPI_PORT:-8080}:8080" + environment: + OTEAPI_REDIS_TYPE: redis + OTEAPI_REDIS_HOST: redis + OTEAPI_REDIS_PORT: 6379 + OTEAPI_PREFIX: "${OTEAPI_PREFIX:-/api/v1}" + PATH_TO_OTEAPI_CORE: + OTEAPI_PLUGIN_PACKAGES: "-v -e /oteapi-optimade" + depends_on: + - redis + networks: + - otenet + volumes: + - "${PATH_TO_OTEAPI_CORE:-/dev/null}:/oteapi-core" + - "${PWD}:/oteapi-optimade" + entrypoint: | + /bin/bash -c "if [ \"${PATH_TO_OTEAPI_CORE}\" != \"/dev/null\" ] && [ -n \"${PATH_TO_OTEAPI_CORE}\" ]; then \ + pip install -U --force-reinstall -e /oteapi-core; fi && ./entrypoint.sh --reload --debug --log-level debug" + stop_grace_period: 1s + + redis: + image: redis:latest + volumes: + - redis-persist:/data + networks: + - otenet + + optimade: + image: ghcr.io/materials-consortia/optimade:develop + ports: + - "${OPTIMADE_PORT:-5000}:5000" + networks: + - otenet + stop_grace_period: 1s + +volumes: + redis-persist: + +networks: + otenet: diff --git a/mkdocs.yml b/mkdocs.yml index be66b84..ee74cb0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -93,6 +93,7 @@ plugins: allow_errors: false execute_ignore: - "**/examples/otelib.ipynb" + - "**/examples/dlite.ipynb" kernel_name: python3 nav: diff --git a/oteapi_optimade/dlite/entities/JSONAPIResourceLinks.yaml b/oteapi_optimade/dlite/entities/JSONAPIResourceLinks.yaml index c566982..7ba4e01 100644 --- a/oteapi_optimade/dlite/entities/JSONAPIResourceLinks.yaml +++ b/oteapi_optimade/dlite/entities/JSONAPIResourceLinks.yaml @@ -1,7 +1,7 @@ uri: http://onto-ns.com/meta/1.0/JSONAPIResourceLinks meta: http://onto-ns.com/meta/0.3/EntitySchema description: A Resource Links object. -dimensions: [] +dimensions: {} properties: self: type: string diff --git a/oteapi_optimade/dlite/entities/OPTIMADERelationships.yaml b/oteapi_optimade/dlite/entities/OPTIMADERelationships.yaml index db72979..b57d50a 100644 --- a/oteapi_optimade/dlite/entities/OPTIMADERelationships.yaml +++ b/oteapi_optimade/dlite/entities/OPTIMADERelationships.yaml @@ -1,7 +1,7 @@ uri: http://onto-ns.com/meta/1.0/OPTIMADERelationships meta: http://onto-ns.com/meta/0.3/EntitySchema description: This model wraps the JSON API Relationships to include type-specific top level keys. -dimensions: [] +dimensions: {} properties: references: type: ref diff --git a/oteapi_optimade/dlite/entities/OPTIMADEStructure.yaml b/oteapi_optimade/dlite/entities/OPTIMADEStructure.yaml index c37dbe5..768e99f 100644 --- a/oteapi_optimade/dlite/entities/OPTIMADEStructure.yaml +++ b/oteapi_optimade/dlite/entities/OPTIMADEStructure.yaml @@ -1,14 +1,14 @@ -uri: http://onto-ns.com/meta/1.0/OPTIMADEStructure +uri: http://onto-ns.com/meta/1.0.1/OPTIMADEStructure meta: http://onto-ns.com/meta/0.3/EntitySchema description: An OPTIMADE structure. -dimensions: [] +dimensions: {} properties: type: type: string description: The name of the type of an entry. Must always be 'structures'. attributes: type: ref - $ref: http://onto-ns.com/meta/1.0/OPTIMADEStructureAttributes + $ref: http://onto-ns.com/meta/1.0.1/OPTIMADEStructureAttributes description: The attributes used to represent a structure, e.g. unit cell, atoms, positions. id: type: str diff --git a/oteapi_optimade/dlite/entities/OPTIMADEStructureAttributes.yaml b/oteapi_optimade/dlite/entities/OPTIMADEStructureAttributes.yaml index fce0a49..4f89ca3 100644 --- a/oteapi_optimade/dlite/entities/OPTIMADEStructureAttributes.yaml +++ b/oteapi_optimade/dlite/entities/OPTIMADEStructureAttributes.yaml @@ -1,4 +1,4 @@ -uri: http://onto-ns.com/meta/1.0/OPTIMADEStructureAttributes +uri: http://onto-ns.com/meta/1.0.1/OPTIMADEStructureAttributes meta: http://onto-ns.com/meta/0.3/EntitySchema description: The attributes used to represent a structure, e.g. unit cell, atoms, positions. dimensions: @@ -46,7 +46,7 @@ properties: description: Cartesian positions of each site in the structure. A site is usually used to describe positions of atoms; what atoms can be encountered at a given site is conveyed by the `species_at_sites` property, and the species themselves are described in the `species` property. species: type: ref - $ref: http://onto-ns.com/meta/1.0/OPTIMADEStructureSpecies + $ref: http://onto-ns.com/meta/1.0.1/OPTIMADEStructureSpecies shape: [nspecies] description: A list describing the species of the sites of this structure. Species can represent pure chemical elements, virtual-crystal atoms representing a statistical occupation of a given site by multiple chemical elements, and/or a location to which there are attached atoms, i.e., atoms whose precise location are unknown beyond that they are attached to that position (frequently used to indicate hydrogen atoms attached to another element, e.g., a carbon with three attached hydrogens might represent a methyl group, -CH3). species_at_sites: diff --git a/oteapi_optimade/dlite/entities/OPTIMADEStructureSpecies.yaml b/oteapi_optimade/dlite/entities/OPTIMADEStructureSpecies.yaml index 584fb3c..d650dbb 100644 --- a/oteapi_optimade/dlite/entities/OPTIMADEStructureSpecies.yaml +++ b/oteapi_optimade/dlite/entities/OPTIMADEStructureSpecies.yaml @@ -1,4 +1,4 @@ -uri: http://onto-ns.com/meta/1.0/OPTIMADEStructureSpecies +uri: http://onto-ns.com/meta/1.0.1/OPTIMADEStructureSpecies meta: http://onto-ns.com/meta/0.3/EntitySchema description: Species can represent pure chemical elements, virtual-crystal atoms representing a statistical occupation of a given site by multiple chemical elements, and/or a location to which there are attached atoms, i.e., atoms whose precise location are unknown beyond that they are attached to that position (frequently used to indicate hydrogen atoms attached to another element, e.g., a carbon with three attached hydrogens might represent a methyl group, -CH3). dimensions: diff --git a/oteapi_optimade/dlite/parse.py b/oteapi_optimade/dlite/parse.py index 166dee6..bb087d8 100644 --- a/oteapi_optimade/dlite/parse.py +++ b/oteapi_optimade/dlite/parse.py @@ -17,21 +17,20 @@ StructureResponseOne, Success, ) -from oteapi.models import SessionUpdate from oteapi_dlite.models import DLiteSessionUpdate from oteapi_dlite.utils import get_collection, update_collection from pydantic import BaseModel, ValidationError from pydantic.dataclasses import dataclass from oteapi_optimade.exceptions import OPTIMADEParseError -from oteapi_optimade.models import OPTIMADEDLiteParseConfig, OPTIMADEParseSession +from oteapi_optimade.models import OPTIMADEDLiteParseConfig, OPTIMADEParseResult from oteapi_optimade.strategies.parse import OPTIMADEParseStrategy if TYPE_CHECKING: # pragma: no cover from typing import Any -LOGGER = logging.getLogger("oteapi_optimade.dlite") +LOGGER = logging.getLogger(__name__) @dataclass @@ -40,36 +39,30 @@ class OPTIMADEDLiteParseStrategy: **Implements strategies**: - - `("mediaType", "application/vnd.optimade+dlite")` - - `("mediaType", "application/vnd.OPTIMADE+dlite")` - - `("mediaType", "application/vnd.OPTiMaDe+dlite")` - - `("mediaType", "application/vnd.optimade+DLite")` - - `("mediaType", "application/vnd.OPTIMADE+DLite")` - - `("mediaType", "application/vnd.OPTiMaDe+DLite")` + - `("parserType", "parser/OPTIMADE/DLite")` """ parse_config: OPTIMADEDLiteParseConfig - def initialize(self, session: dict[str, Any] | None = None) -> DLiteSessionUpdate: + def initialize(self) -> 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. """ - return DLiteSessionUpdate(collection_id=get_collection(session).uuid) + return DLiteSessionUpdate( + collection_id=get_collection( + collection_id=self.parse_config.configuration.collection_id + ).uuid + ) - def get( - self, session: SessionUpdate | dict[str, Any] | None = None - ) -> OPTIMADEParseSession: + def get(self) -> OPTIMADEParseResult: """Request and parse an OPTIMADE response using OPT. This method will be called through the strategy-specific endpoint of the @@ -89,9 +82,6 @@ def get( meaning the most nested data structures must first be parsed, and then the ones 1 layer up and so on until the most upper layer can be parsed. - 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. @@ -99,12 +89,21 @@ def get( """ generic_parse_config = self.parse_config.model_copy( update={ - "mediaType": self.parse_config.mediaType.lower().replace( - "+dlite", "+json" - ) + "parserType": self.parse_config.parserType.lower().replace( + "/dlite", "" + ), + "configuration": self.parse_config.configuration.model_copy( + update={ + "mediaType": self.parse_config.configuration.get( + "mediaType", "" + ) + .lower() + .replace("+dlite", "+json") + } + ), } - ).model_dump() - session = OPTIMADEParseStrategy(generic_parse_config).get(session) + ).model_dump(exclude_unset=True, exclude_defaults=True) + generic_parse_result = OPTIMADEParseStrategy(generic_parse_config).get() entities_path = Path(__file__).resolve().parent.resolve() / "entities" @@ -127,7 +126,8 @@ def get( ) if not all( - _ in session for _ in ("optimade_response", "optimade_response_model") + _ in generic_parse_result + for _ in ("optimade_response", "optimade_response_model") ): base_error_message = ( "Could not retrieve response from OPTIMADE parse strategy." @@ -138,16 +138,16 @@ def get( "optimade_response_model=%r\n" "session fields=%r", base_error_message, - session.get("optimade_response"), - session.get("optimade_response_model"), - list(session.keys()), + generic_parse_result.get("optimade_response"), + generic_parse_result.get("optimade_response_model"), + list(generic_parse_result.keys()), ) raise OPTIMADEParseError(base_error_message) - optimade_response_model_module, optimade_response_model_name = session.get( - "optimade_response_model" + optimade_response_model_module, optimade_response_model_name = ( + generic_parse_result.get("optimade_response_model") ) - optimade_response_dict = session.get("optimade_response") + optimade_response_dict = generic_parse_result.get("optimade_response") error_message_supporting_only_structures = ( "The DLite OPTIMADE Parser currently only supports structures entities." @@ -235,7 +235,9 @@ def get( raise OPTIMADEParseError(error_message_supporting_only_structures) # DLite-fy OPTIMADE structures - dlite_collection = get_collection(session) + dlite_collection = get_collection( + collection_id=self.parse_config.configuration.collection_id + ) for structure in structures: new_structure_attributes: dict[str, Any] = {} @@ -299,7 +301,9 @@ def get( # Attributes new_structure_attributes.update( structure.attributes.model_dump( - exclude={"species", "assemblies", "nelements", "nsites"} + exclude={"species", "assemblies", "nelements", "nsites"}, + exclude_unset=True, + exclude_defaults=True, ) ) for key in list(new_structure_attributes): @@ -339,4 +343,4 @@ def get( update_collection(collection=dlite_collection) - return session + return generic_parse_result diff --git a/oteapi_optimade/models/__init__.py b/oteapi_optimade/models/__init__.py index 7929007..3223192 100644 --- a/oteapi_optimade/models/__init__.py +++ b/oteapi_optimade/models/__init__.py @@ -5,19 +5,19 @@ from .strategies import ( OPTIMADEDLiteParseConfig, OPTIMADEFilterConfig, - OPTIMADEFilterSession, + OPTIMADEFilterResult, OPTIMADEParseConfig, - OPTIMADEParseSession, + OPTIMADEParseResult, OPTIMADEResourceConfig, - OPTIMADEResourceSession, + OPTIMADEResourceResult, ) __all__ = ( "OPTIMADEDLiteParseConfig", "OPTIMADEFilterConfig", - "OPTIMADEFilterSession", + "OPTIMADEFilterResult", "OPTIMADEParseConfig", - "OPTIMADEParseSession", + "OPTIMADEParseResult", "OPTIMADEResourceConfig", - "OPTIMADEResourceSession", + "OPTIMADEResourceResult", ) diff --git a/oteapi_optimade/models/config.py b/oteapi_optimade/models/config.py index 17e2a20..7202598 100644 --- a/oteapi_optimade/models/config.py +++ b/oteapi_optimade/models/config.py @@ -5,8 +5,9 @@ from typing import Annotated, Literal, Optional from oteapi.models import AttrDict, DataCacheConfig -from pydantic import Field, field_validator +from pydantic import BeforeValidator, Field, field_validator +from oteapi_optimade.models.custom_types import OPTIMADEUrl from oteapi_optimade.models.query import OPTIMADEQueryParameters DEFAULT_CACHE_CONFIG_VALUES = { @@ -16,9 +17,30 @@ """Set the `expireTime` and `tag` to default values for the data cache.""" -class OPTIMADEConfig(AttrDict): # type: ignore[misc] +class OPTIMADEConfig(AttrDict): """OPTIMADE configuration.""" + # OTEAPI-specific attributes + downloadUrl: Annotated[ + Optional[OPTIMADEUrl], + Field(description="Either a base OPTIMADE URL or a full OPTIMADE URL."), + ] = None + + mediaType: Annotated[ + Optional[Literal["application/vnd.optimade+json", "application/vnd.optimade"]], + BeforeValidator(lambda x: x.lower() if isinstance(x, str) else x), + Field( + description="The registered strategy name for OPTIMADEParseStrategy.", + ), + ] = None + + # OPTIMADE parse result attributes + optimade_config: Annotated[ + Optional[OPTIMADEConfig], + Field(description="A pre-existing instance of this OPTIMADE configuration."), + ] = None + + # OPTIMADE-specific attributes version: Annotated[ str, Field( @@ -26,24 +48,28 @@ class OPTIMADEConfig(AttrDict): # type: ignore[misc] pattern=r"^v[0-9]+(\.[0-9]+){0,2}$", ), ] = "v1" + endpoint: Annotated[ Literal["references", "structures"], Field( description="Supported OPTIMADE entry resource endpoint.", ), ] = "structures" + query_parameters: Annotated[ Optional[OPTIMADEQueryParameters], Field( description="URL query parameters to be used in the OPTIMADE query.", ), ] = None + datacache_config: Annotated[ DataCacheConfig, Field( description="Configuration options for the local data cache.", ), ] = DataCacheConfig(**DEFAULT_CACHE_CONFIG_VALUES) + use_dlite: Annotated[ bool, Field( @@ -53,7 +79,7 @@ class OPTIMADEConfig(AttrDict): # type: ignore[misc] @field_validator("datacache_config", mode="after") @classmethod - def default_datacache_config( + def _default_datacache_config( cls, datacache_config: DataCacheConfig ) -> DataCacheConfig: """Use default values for `DataCacheConfig` if not supplied.""" @@ -75,3 +101,22 @@ def default_datacache_config( } ) return datacache_config + + +class OPTIMADEDLiteConfig(OPTIMADEConfig): + """OPTIMADE configuration when using the DLite-specific strategies.""" + + # OTEAPI-specific attributes + mediaType: Annotated[ + Optional[Literal["application/vnd.optimade+dlite"]], + BeforeValidator(lambda x: x.lower() if isinstance(x, str) else x), + Field( + description="The registered strategy name for OPTIMADEDLiteParseStrategy.", + ), + ] = None # type: ignore[assignment] + + # Dlite specific attributes + collection_id: Annotated[ + Optional[str], + Field(description="A reference to a DLite Collection."), + ] = None diff --git a/oteapi_optimade/models/custom_types.py b/oteapi_optimade/models/custom_types.py index 273e9eb..d2e93c9 100644 --- a/oteapi_optimade/models/custom_types.py +++ b/oteapi_optimade/models/custom_types.py @@ -62,7 +62,7 @@ class OPTIMADEParts(TypedDict, total=False): r"|calculations|extensions)(?:/[^\s?#]*)?))?$" ) -LOGGER = logging.getLogger("oteapi_optimade.models") +LOGGER = logging.getLogger(__name__) class OPTIMADEUrl(str): diff --git a/oteapi_optimade/models/query.py b/oteapi_optimade/models/query.py index 534e16f..fbdfa60 100644 --- a/oteapi_optimade/models/query.py +++ b/oteapi_optimade/models/query.py @@ -31,19 +31,25 @@ class OPTIMADEQueryParameters(BaseModel, validate_assignment=True): Field( description=QUERY_PARAMETERS["annotations"]["filter"].description, ), - ] = QUERY_PARAMETERS["defaults"].filter + ] = ( + QUERY_PARAMETERS["defaults"].filter or None + ) response_format: Annotated[ Optional[str], Field( description=QUERY_PARAMETERS["annotations"]["response_format"].description, ), - ] = QUERY_PARAMETERS["defaults"].response_format + ] = ( + QUERY_PARAMETERS["defaults"].response_format or None + ) email_address: Annotated[ Optional[EmailStr], Field( description=QUERY_PARAMETERS["annotations"]["email_address"].description, ), - ] = QUERY_PARAMETERS["defaults"].email_address + ] = ( + QUERY_PARAMETERS["defaults"].email_address or None + ) response_fields: Annotated[ Optional[str], Field( @@ -52,28 +58,36 @@ class OPTIMADEQueryParameters(BaseModel, validate_assignment=True): .metadata[0] .pattern, ), - ] = QUERY_PARAMETERS["defaults"].response_fields + ] = ( + QUERY_PARAMETERS["defaults"].response_fields or None + ) sort: Annotated[ Optional[str], Field( description=QUERY_PARAMETERS["annotations"]["sort"].description, pattern=QUERY_PARAMETERS["annotations"]["sort"].metadata[0].pattern, ), - ] = QUERY_PARAMETERS["defaults"].sort + ] = ( + QUERY_PARAMETERS["defaults"].sort or None + ) page_limit: Annotated[ Optional[int], Field( description=QUERY_PARAMETERS["annotations"]["page_limit"].description, ge=QUERY_PARAMETERS["annotations"]["page_limit"].metadata[0].ge, ), - ] = QUERY_PARAMETERS["defaults"].page_limit + ] = ( + QUERY_PARAMETERS["defaults"].page_limit or None + ) page_offset: Annotated[ Optional[int], Field( description=QUERY_PARAMETERS["annotations"]["page_offset"].description, ge=QUERY_PARAMETERS["annotations"]["page_offset"].metadata[0].ge, ), - ] = QUERY_PARAMETERS["defaults"].page_offset + ] = ( + QUERY_PARAMETERS["defaults"].page_offset or None + ) page_number: Annotated[ Optional[int], Field( @@ -82,32 +96,42 @@ class OPTIMADEQueryParameters(BaseModel, validate_assignment=True): # This constraint is only 'RECOMMENDED' in the specification, so should not # be included here or in the OpenAPI schema. ), - ] = QUERY_PARAMETERS["defaults"].page_number + ] = ( + QUERY_PARAMETERS["defaults"].page_number or None + ) page_cursor: Annotated[ Optional[int], Field( description=QUERY_PARAMETERS["annotations"]["page_cursor"].description, ge=QUERY_PARAMETERS["annotations"]["page_cursor"].metadata[0].ge, ), - ] = QUERY_PARAMETERS["defaults"].page_cursor + ] = ( + QUERY_PARAMETERS["defaults"].page_cursor or None + ) page_above: Annotated[ Optional[int], Field( description=QUERY_PARAMETERS["annotations"]["page_above"].description, ), - ] = QUERY_PARAMETERS["defaults"].page_above + ] = ( + QUERY_PARAMETERS["defaults"].page_above or None + ) page_below: Annotated[ Optional[int], Field( description=QUERY_PARAMETERS["annotations"]["page_below"].description, ), - ] = QUERY_PARAMETERS["defaults"].page_below + ] = ( + QUERY_PARAMETERS["defaults"].page_below or None + ) include: Annotated[ Optional[str], Field( description=QUERY_PARAMETERS["annotations"]["include"].description, ), - ] = QUERY_PARAMETERS["defaults"].include + ] = ( + QUERY_PARAMETERS["defaults"].include or None + ) # api_hint is not yet initialized in `EntryListingQueryParams`. # These values are copied verbatim from `optimade==0.16.10`. api_hint: Annotated[ diff --git a/oteapi_optimade/models/strategies/__init__.py b/oteapi_optimade/models/strategies/__init__.py index 9cbbbbf..ce332ae 100644 --- a/oteapi_optimade/models/strategies/__init__.py +++ b/oteapi_optimade/models/strategies/__init__.py @@ -2,16 +2,16 @@ from __future__ import annotations -from .filter import OPTIMADEFilterConfig, OPTIMADEFilterSession -from .parse import OPTIMADEDLiteParseConfig, OPTIMADEParseConfig, OPTIMADEParseSession -from .resource import OPTIMADEResourceConfig, OPTIMADEResourceSession +from .filter import OPTIMADEFilterConfig, OPTIMADEFilterResult +from .parse import OPTIMADEDLiteParseConfig, OPTIMADEParseConfig, OPTIMADEParseResult +from .resource import OPTIMADEResourceConfig, OPTIMADEResourceResult __all__ = ( "OPTIMADEDLiteParseConfig", "OPTIMADEFilterConfig", - "OPTIMADEFilterSession", + "OPTIMADEFilterResult", "OPTIMADEParseConfig", - "OPTIMADEParseSession", + "OPTIMADEParseResult", "OPTIMADEResourceConfig", - "OPTIMADEResourceSession", + "OPTIMADEResourceResult", ) diff --git a/oteapi_optimade/models/strategies/filter.py b/oteapi_optimade/models/strategies/filter.py index 39d78b8..9ad4d23 100644 --- a/oteapi_optimade/models/strategies/filter.py +++ b/oteapi_optimade/models/strategies/filter.py @@ -4,13 +4,13 @@ from typing import Annotated, Any, Literal, Optional -from oteapi.models import FilterConfig, SessionUpdate -from pydantic import ConfigDict, Field +from oteapi.models import AttrDict, FilterConfig +from pydantic import BeforeValidator, ConfigDict, Field from oteapi_optimade.models.config import OPTIMADEConfig -class OPTIMADEFilterConfig(FilterConfig): # type: ignore[misc] +class OPTIMADEFilterConfig(FilterConfig): """OPTIMADE-specific filter strategy config. Note: @@ -19,7 +19,8 @@ class OPTIMADEFilterConfig(FilterConfig): # type: ignore[misc] """ filterType: Annotated[ - Literal["optimade", "OPTIMADE", "OPTiMaDe"], + Literal["optimade"], + BeforeValidator(lambda x: x.lower() if isinstance(x, str) else x), Field( description="The registered strategy name for OPTIMADEFilterStrategy.", ), @@ -60,7 +61,7 @@ class OPTIMADEFilterConfig(FilterConfig): # type: ignore[misc] ] = OPTIMADEConfig() -class OPTIMADEFilterSession(SessionUpdate): # type: ignore[misc] +class OPTIMADEFilterResult(AttrDict): """OPTIMADE session for the filter strategy.""" model_config = ConfigDict(validate_assignment=True, arbitrary_types_allowed=True) diff --git a/oteapi_optimade/models/strategies/parse.py b/oteapi_optimade/models/strategies/parse.py index 07d192f..7953b65 100644 --- a/oteapi_optimade/models/strategies/parse.py +++ b/oteapi_optimade/models/strategies/parse.py @@ -4,38 +4,28 @@ from typing import Annotated, Any, Literal, Optional -from oteapi.models import ResourceConfig, SessionUpdate -from pydantic import ConfigDict, Field +from oteapi.models import AttrDict, ParserConfig +from pydantic import AnyHttpUrl, BeforeValidator, ConfigDict, Field, field_validator -from oteapi_optimade.models.config import OPTIMADEConfig -from oteapi_optimade.models.custom_types import OPTIMADEUrl +from oteapi_optimade.models.config import OPTIMADEConfig, OPTIMADEDLiteConfig -class OPTIMADEParseConfig(ResourceConfig): # type: ignore[misc] +class OPTIMADEParseConfig(ParserConfig): """OPTIMADE-specific parse strategy config.""" - downloadUrl: Annotated[ - OPTIMADEUrl, - Field( - description="Either a base OPTIMADE URL or a full OPTIMADE URL.", - ), - ] - mediaType: Annotated[ - Literal[ - "application/vnd.optimade+json", - "application/vnd.OPTIMADE+json", - "application/vnd.OPTiMaDe+json", - "application/vnd.optimade+JSON", - "application/vnd.OPTIMADE+JSON", - "application/vnd.OPTiMaDe+JSON", - "application/vnd.optimade", - "application/vnd.OPTIMADE", - "application/vnd.OPTiMaDe", - ], + entity: Annotated[ + AnyHttpUrl, + Field(description=ParserConfig.model_fields["entity"].description), + ] = AnyHttpUrl("http://onto-ns.com/meta/1.0.1/OPTIMADEStructure") + + parserType: Annotated[ + Literal["parser/optimade"], + BeforeValidator(lambda x: x.lower() if isinstance(x, str) else x), Field( - description="The registered strategy name for OPTIMADEParseStrategy.", + description=ParserConfig.model_fields["parserType"].description, ), ] + configuration: Annotated[ OPTIMADEConfig, Field( @@ -46,9 +36,20 @@ class OPTIMADEParseConfig(ResourceConfig): # type: ignore[misc] ), ] = OPTIMADEConfig() + @field_validator("entity", mode="after") + def _validate_entity(cls, value: AnyHttpUrl) -> AnyHttpUrl: + """Validate entity.""" + supported_entities = {"http://onto-ns.com/meta/1.0.1/OPTIMADEStructure"} + if value not in (AnyHttpUrl(_) for _ in supported_entities): + raise ValueError( + f"Unsupported entity: {value}. Supported entities: {supported_entities}" + ) + + return value + -class OPTIMADEParseSession(SessionUpdate): # type: ignore[misc] - """OPTIMADE session for the parse strategy.""" +class OPTIMADEParseResult(AttrDict): + """OPTIMADE parse strategy result.""" model_config = ConfigDict(validate_assignment=True, arbitrary_types_allowed=True) @@ -80,18 +81,20 @@ class OPTIMADEParseSession(SessionUpdate): # type: ignore[misc] class OPTIMADEDLiteParseConfig(OPTIMADEParseConfig): - """OPTIMADE-specific parse strategy config.""" + """OPTIMADE-specific parse strategy config when using DLite.""" + + parserType: Annotated[ # type: ignore[assignment] + Literal["parser/optimade/dlite"], + BeforeValidator(lambda x: x.lower() if isinstance(x, str) else x), + Field(description=ParserConfig.model_fields["parserType"].description), + ] - mediaType: Annotated[ # type: ignore[assignment] - Literal[ - "application/vnd.optimade+dlite", - "application/vnd.OPTIMADE+dlite", - "application/vnd.OPTiMaDe+dlite", - "application/vnd.optimade+DLite", - "application/vnd.OPTIMADE+DLite", - "application/vnd.OPTiMaDe+DLite", - ], + configuration: Annotated[ # type: ignore[assignment] + OPTIMADEDLiteConfig, Field( - description="The registered strategy name for OPTIMADEDLiteParseStrategy.", + description=( + "OPTIMADE configuration when using the DLite-specific strategies. " + "Contains relevant information necessary to perform OPTIMADE queries." + ), ), - ] + ] = OPTIMADEDLiteConfig() diff --git a/oteapi_optimade/models/strategies/resource.py b/oteapi_optimade/models/strategies/resource.py index 9853097..ae49ea1 100644 --- a/oteapi_optimade/models/strategies/resource.py +++ b/oteapi_optimade/models/strategies/resource.py @@ -2,42 +2,37 @@ from __future__ import annotations -from typing import Annotated, Any, Literal, Optional +from typing import Annotated, Any, Literal, Optional, Union -from oteapi.models import ResourceConfig, SessionUpdate -from pydantic import ConfigDict, Field +from oteapi.models import AttrDict, ResourceConfig +from pydantic import BeforeValidator, ConfigDict, Field -from oteapi_optimade.models.config import OPTIMADEConfig +from oteapi_optimade.models.config import OPTIMADEConfig, OPTIMADEDLiteConfig from oteapi_optimade.models.custom_types import OPTIMADEUrl -class OPTIMADEResourceConfig(ResourceConfig): # type: ignore[misc] +class OPTIMADEResourceConfig(ResourceConfig): """OPTIMADE-specific resource strategy config.""" + resourceType: Annotated[ + # later OPTIMADE/references and more should be added and other resources + Literal["optimade/structures"], + BeforeValidator(lambda x: x.lower() if isinstance(x, str) else x), + Field(description=ResourceConfig.model_fields["resourceType"].description), + ] accessUrl: Annotated[ OPTIMADEUrl, - Field( - description="Either a base OPTIMADE URL or a full OPTIMADE URL.", - ), + Field(description="Either a base OPTIMADE URL or a full OPTIMADE URL."), ] accessService: Annotated[ - Literal[ - "optimade", - "OPTIMADE", - "OPTiMaDe", - "optimade+dlite", - "OPTIMADE+dlite", - "OPTiMaDe+dlite", - "optimade+DLite", - "OPTIMADE+DLite", - "OPTiMaDe+DLite", - ], + Literal["optimade", "optimade+dlite"], + BeforeValidator(lambda x: x.lower() if isinstance(x, str) else x), Field( description="The registered strategy name for OPTIMADEResourceStrategy.", ), ] configuration: Annotated[ - OPTIMADEConfig, + Union[OPTIMADEConfig | OPTIMADEDLiteConfig], Field( description=( "OPTIMADE configuration. Contains relevant information necessary to " @@ -47,7 +42,7 @@ class OPTIMADEResourceConfig(ResourceConfig): # type: ignore[misc] ] = OPTIMADEConfig() -class OPTIMADEResourceSession(SessionUpdate): # type: ignore[misc] +class OPTIMADEResourceResult(AttrDict): """OPTIMADE session for the resource strategy.""" model_config = ConfigDict(validate_assignment=True, arbitrary_types_allowed=True) diff --git a/oteapi_optimade/strategies/filter.py b/oteapi_optimade/strategies/filter.py index 124cecd..99ea91a 100644 --- a/oteapi_optimade/strategies/filter.py +++ b/oteapi_optimade/strategies/filter.py @@ -3,19 +3,14 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING -from oteapi.models import SessionUpdate +from oteapi.models import AttrDict from pydantic.dataclasses import dataclass -from oteapi_optimade.models import OPTIMADEFilterConfig, OPTIMADEFilterSession +from oteapi_optimade.models import OPTIMADEFilterConfig, OPTIMADEFilterResult from oteapi_optimade.models.query import OPTIMADEQueryParameters -if TYPE_CHECKING: # pragma: no cover - from typing import Any - - -LOGGER = logging.getLogger("oteapi_optimade.strategies") +LOGGER = logging.getLogger(__name__) @dataclass @@ -25,16 +20,12 @@ class OPTIMADEFilterStrategy: **Implements strategies**: - `("filterType", "OPTIMADE")` - - `("filterType", "optimade")` - - `("filterType", "OPTiMaDe")` """ filter_config: OPTIMADEFilterConfig - def initialize( - self, session: SessionUpdate | dict[str, Any] | None = None - ) -> OPTIMADEFilterSession: + def initialize(self) -> OPTIMADEFilterResult: """Initialize strategy. This method will be called through the `/initialize` endpoint of the OTE-API @@ -48,27 +39,17 @@ def initialize( 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: + if self.filter_config.configuration.optimade_config: self.filter_config.configuration.update( - session.optimade_config.model_dump( - exclude_defaults=True, exclude_unset=True + self.filter_config.configuration.optimade_config.model_dump( + exclude_defaults=True, + exclude_unset=True, + exclude={"optimade_config", "downloadUrl", "mediaType"}, ) ) @@ -85,34 +66,23 @@ def initialize( 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, - ) - } - ) - }, + return OPTIMADEFilterResult( + optimade_config=optimade_config.model_dump( + exclude={"optimade_config", "downloadUrl", "mediaType"}, + exclude_unset=True, + exclude_defaults=True, + ) ) - def get( - self, - session: dict[str, Any] | None = None, # noqa: ARG002 - ) -> SessionUpdate: + def get(self) -> AttrDict: """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() + return AttrDict() diff --git a/oteapi_optimade/strategies/parse.py b/oteapi_optimade/strategies/parse.py index 53ad4cc..9f6f20c 100644 --- a/oteapi_optimade/strategies/parse.py +++ b/oteapi_optimade/strategies/parse.py @@ -8,20 +8,19 @@ from optimade.models import ErrorResponse, Success from oteapi.datacache import DataCache -from oteapi.models import SessionUpdate +from oteapi.models import AttrDict from oteapi.plugins import create_strategy -from oteapi.plugins.entry_points import StrategyType from pydantic import ValidationError from pydantic.dataclasses import dataclass from oteapi_optimade.exceptions import OPTIMADEParseError -from oteapi_optimade.models import OPTIMADEParseConfig, OPTIMADEParseSession +from oteapi_optimade.models import OPTIMADEParseConfig, OPTIMADEParseResult if TYPE_CHECKING: # pragma: no cover from typing import Any -LOGGER = logging.getLogger("oteapi_optimade.strategies") +LOGGER = logging.getLogger(__name__) @dataclass @@ -30,42 +29,26 @@ class OPTIMADEParseStrategy: **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")` + - `("parserType", "parser/OPTIMADE")` """ parse_config: OPTIMADEParseConfig - def initialize( - self, - session: dict[str, Any] | None = None, # noqa: ARG002 - ) -> SessionUpdate: + def initialize(self) -> AttrDict: """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() + return AttrDict() - def get( - self, session: SessionUpdate | dict[str, Any] | None = None - ) -> OPTIMADEParseSession: + def get(self) -> OPTIMADEParseResult: """Request and parse an OPTIMADE response using OPT. This method will be called through the strategy-specific endpoint of the @@ -79,33 +62,24 @@ def get( 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 - ) + if ( + self.parse_config.configuration.downloadUrl is None + or self.parse_config.configuration.mediaType is None + ): + raise OPTIMADEParseError( + "Missing downloadUrl or mediaType in configuration." ) 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) + if self.parse_config.configuration.downloadUrl in cache: + response: dict[str, Any] = cache.get( + self.parse_config.configuration.downloadUrl + ) elif ( self.parse_config.configuration.datacache_config.accessKey and self.parse_config.configuration.datacache_config.accessKey in cache @@ -114,19 +88,9 @@ def 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")))} + download_config = self.parse_config.configuration.model_copy() + download_output = create_strategy("download", download_config).get() + response = {"json": json.loads(cache.get(download_output.pop("key")))} if ( not response.get("ok", True) @@ -150,7 +114,9 @@ def get( raise OPTIMADEParseError(error_message) from exc else: # Successful response - response_model = self.parse_config.downloadUrl.response_model() + response_model = ( + self.parse_config.configuration.downloadUrl.response_model() + ) LOGGER.debug("response_model=%r", response_model) if response_model: if not isinstance(response_model, tuple): @@ -168,7 +134,7 @@ def get( LOGGER.error( "%s\nURL=%r\n" "response_models=%r\nresponse=%s", error_message, - self.parse_config.downloadUrl, + self.parse_config.configuration.downloadUrl, response_model, response, ) @@ -185,25 +151,31 @@ def get( "URL=%r\nendpoint=%r\nresponse_model=%r\nresponse=%s", error_message, exc, - self.parse_config.downloadUrl, - self.parse_config.downloadUrl.endpoint, + self.parse_config.configuration.downloadUrl, + self.parse_config.configuration.downloadUrl.endpoint, response_model, response, ) raise OPTIMADEParseError(error_message) from exc - session.optimade_response_model = ( - response_object.__class__.__module__, - response_object.__class__.__name__, + result = OPTIMADEParseResult( + model_config=self.parse_config.configuration.model_dump(), + optimade_response_model=( + response_object.__class__.__module__, + response_object.__class__.__name__, + ), + optimade_response=response_object.model_dump(exclude_unset=True), ) - session.optimade_response = response_object.model_dump(exclude_unset=True) - if session.optimade_config and session.optimade_config.query_parameters: - session = session.model_copy( + if ( + self.parse_config.configuration.optimade_config + and self.parse_config.configuration.optimade_config.query_parameters + ): + result = result.model_copy( update={ - "optimade_config": session.optimade_config.model_copy( + "optimade_config": self.parse_config.configuration.optimade_config.model_copy( update={ - "query_parameters": session.optimade_config.query_parameters.model_dump( + "query_parameters": self.parse_config.configuration.optimade_config.query_parameters.model_dump( exclude_defaults=True, exclude_unset=True, ) @@ -212,7 +184,4 @@ def get( } ) - if TYPE_CHECKING: # pragma: no cover - assert isinstance(session, OPTIMADEParseSession) # nosec - - return session + return result diff --git a/oteapi_optimade/strategies/resource.py b/oteapi_optimade/strategies/resource.py index 0f7201a..1e294c5 100644 --- a/oteapi_optimade/strategies/resource.py +++ b/oteapi_optimade/strategies/resource.py @@ -18,9 +18,8 @@ StructureResponseOne, ) from oteapi.datacache import DataCache -from oteapi.models import SessionUpdate +from oteapi.models import AttrDict from oteapi.plugins import create_strategy -from oteapi.plugins.entry_points import StrategyType from pydantic import ValidationError from pydantic.dataclasses import dataclass @@ -32,17 +31,24 @@ oteapi_dlite_version = None from oteapi_optimade.exceptions import MissingDependency, OPTIMADEParseError -from oteapi_optimade.models import OPTIMADEResourceConfig, OPTIMADEResourceSession +from oteapi_optimade.models import OPTIMADEResourceConfig, OPTIMADEResourceResult from oteapi_optimade.models.custom_types import OPTIMADEUrl from oteapi_optimade.models.query import OPTIMADEQueryParameters if TYPE_CHECKING: # pragma: no cover - from typing import Any + from typing import Any, TypedDict from optimade.models import Response as OPTIMADEResponse + class ParseConfigDict(TypedDict): + """Type definition for the `parse_config` dictionary.""" -LOGGER = logging.getLogger("oteapi_optimade.strategies") + entity: str + parserType: str + configuration: dict[str, Any] + + +LOGGER = logging.getLogger(__name__) def use_dlite(access_service: str, use_dlite_flag: bool) -> bool: @@ -76,31 +82,19 @@ class OPTIMADEResourceStrategy: **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: + def initialize(self) -> AttrDict | 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. @@ -110,12 +104,16 @@ def initialize( self.resource_config.accessService, self.resource_config.configuration.use_dlite, ): - return DLiteSessionUpdate(collection_id=get_collection(session).uuid) - return SessionUpdate() + collection_id = self.resource_config.configuration.get( + "collection_id", None + ) + return DLiteSessionUpdate( + collection_id=get_collection(collection_id=collection_id).uuid + ) - def get( - self, session: SessionUpdate | dict[str, Any] | None = None - ) -> OPTIMADEResourceSession: + return AttrDict() + + def get(self) -> OPTIMADEResourceResult: """Execute an OPTIMADE query to `accessUrl`. This method will be called through the strategy-specific endpoint of the @@ -132,27 +130,17 @@ def get( 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: + if self.resource_config.configuration.optimade_config: self.resource_config.configuration.update( - session.optimade_config.model_dump( - exclude_defaults=True, exclude_unset=True + self.resource_config.configuration.optimade_config.model_dump( + exclude_defaults=True, + exclude_unset=True, + exclude={"optimade_config", "downloadUrl", "mediaType"}, ) ) @@ -213,38 +201,41 @@ def get( self.resource_config.configuration.use_dlite, ) + parse_parserType = "parser/OPTIMADE" parse_mediaType = ( "application/vnd." f"{self.resource_config.accessService.split('+', maxsplit=1)[0]}" ) if parse_with_dlite: + parse_parserType += "/DLite" parse_mediaType += "+DLite" elif optimade_query.response_format: parse_mediaType += f"+{optimade_query.response_format}" - parse_config = { - "downloadUrl": optimade_url, - "mediaType": parse_mediaType, + parse_config: ParseConfigDict = { + "entity": "http://onto-ns.com/meta/1.0.1/OPTIMADEStructure", + "parserType": parse_parserType, "configuration": { - "datacache_config": self.resource_config.configuration.datacache_config, + "datacache_config": self.resource_config.configuration.datacache_config.model_copy(), + "downloadUrl": str(optimade_url), + "mediaType": parse_mediaType, + "optimade_config": self.resource_config.configuration.model_dump( + exclude={"optimade_config", "downloadUrl", "mediaType"}, + exclude_unset=True, + exclude_defaults=True, + ), }, } 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) - ) + parse_config["configuration"].update( + create_strategy("parse", parse_config).initialize() ) + parse_result = create_strategy("parse", parse_config).get() if not all( - _ in session for _ in ("optimade_response", "optimade_response_model") + _ in parse_result for _ in ("optimade_response", "optimade_response_model") ): base_error_message = ( "Could not retrieve response from OPTIMADE parse strategy." @@ -255,16 +246,16 @@ def get( "optimade_response_model=%r\n" "session fields=%r", base_error_message, - session.get("optimade_response"), - session.get("optimade_response_model"), - list(session.keys()), + parse_result.get("optimade_response"), + parse_result.get("optimade_response_model"), + list(parse_result.keys()), ) raise OPTIMADEParseError(base_error_message) - optimade_response_model_module, optimade_response_model_name = session.pop( + optimade_response_model_module, optimade_response_model_name = parse_result.pop( "optimade_response_model" ) - optimade_response_dict = session.pop("optimade_response") + optimade_response_dict = parse_result.pop("optimade_response") # Parse response using the provided model try: @@ -300,11 +291,11 @@ def get( ) raise OPTIMADEParseError(base_error_message) from exc + result = OPTIMADEResourceResult() + if isinstance(optimade_response, ErrorResponse): optimade_resources = optimade_response.errors - session.optimade_resource_model = ( - f"{OptimadeError.__module__}:OptimadeError" - ) + result.optimade_resource_model = f"{OptimadeError.__module__}:OptimadeError" elif isinstance(optimade_response, ReferenceResponseMany): optimade_resources = [ ( @@ -314,7 +305,7 @@ def get( ) for entry in optimade_response.data ] - session.optimade_resource_model = f"{Reference.__module__}:Reference" + result.optimade_resource_model = f"{Reference.__module__}:Reference" elif isinstance(optimade_response, ReferenceResponseOne): optimade_resources = [ ( @@ -323,7 +314,7 @@ def get( else Reference(optimade_response.data.model_dump()).as_dict ) ] - session.optimade_resource_model = f"{Reference.__module__}:Reference" + result.optimade_resource_model = f"{Reference.__module__}:Reference" elif isinstance(optimade_response, StructureResponseMany): optimade_resources = [ ( @@ -333,7 +324,7 @@ def get( ) for entry in optimade_response.data ] - session.optimade_resource_model = f"{Structure.__module__}:Structure" + result.optimade_resource_model = f"{Structure.__module__}:Structure" elif isinstance(optimade_response, StructureResponseOne): optimade_resources = [ ( @@ -342,7 +333,7 @@ def get( else Structure(optimade_response.data.model_dump()).as_dict ) ] - session.optimade_resource_model = f"{Structure.__module__}:Structure" + result.optimade_resource_model = f"{Structure.__module__}:Structure" else: LOGGER.error( "Could not parse response as errors, references or structures. " @@ -357,17 +348,20 @@ def get( ) raise OPTIMADEParseError(error_message) - session.optimade_resources = [ + result.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( + if ( + self.resource_config.configuration.optimade_config + and self.resource_config.configuration.optimade_config.query_parameters + ): + result = result.model_copy( update={ - "optimade_config": session.optimade_config.model_copy( + "optimade_config": self.resource_config.configuration.optimade_config.model_copy( update={ - "query_parameters": session.optimade_config.query_parameters.model_dump( + "query_parameters": self.resource_config.configuration.optimade_config.query_parameters.model_dump( exclude_defaults=True, exclude_unset=True, ) @@ -376,7 +370,4 @@ def get( } ) - if TYPE_CHECKING: # pragma: no cover - assert isinstance(session, OPTIMADEResourceSession) # nosec - - return session + return result diff --git a/pyproject.toml b/pyproject.toml index 61cc3f2..351ff61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,27 +17,32 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", # "Framework :: OTEAPI", "Environment :: Plugins", "Natural Language :: English", "Operating System :: OS Independent", ] keywords = ["OTE", "OPTIMADE", "OTE-API"] -requires-python = ">=3.9,<3.12" # DLite does not support Python 3.12 and above +requires-python = ">=3.9,<3.13" dynamic = ["version"] dependencies = [ - "DLite-Python >=0.4.5,<1", + "DLite-Python >=0.5.16,<1", + "eval-type-backport ~=0.2.0", + "numpy <2", # Required by DLite-Python "optimade[server] ~=1.1", - "oteapi-core ~=0.6.1", - "oteapi-dlite >=0.2.0,<1", + "oteapi-core ~=0.7.0.dev2", + # "oteapi-dlite >=0.2.0,<1", + "oteapi-dlite-mod @ git+https://github.com/SINTEF/oteapi-dlite-Mod.git@master#egg=oteapi-dlite-mod", "requests ~=2.32", + "typing-extensions ~=4.12; python_version < '3.10'", ] [project.optional-dependencies] examples = [ "jupyter ~=1.1", - "otelib ~=0.4.1", + "otelib ~=0.5.0.dev0", ] doc = [ "mike ~=2.1", @@ -45,7 +50,7 @@ doc = [ "mkdocs-awesome-pages-plugin ~=2.9", "mkdocs-jupyter ~=0.24.8", "mkdocs-material >=9.5.5,<10", - "mkdocstrings[python-legacy] ~=0.26.0", + "mkdocstrings[python] ~=0.26.0", "oteapi-optimade[examples]", ] test = [ @@ -71,32 +76,20 @@ Package = "https://pypi.org/project/oteapi-optimade" "oteapi_optimade.OPTiMaDe" = "oteapi_optimade.strategies.filter:OPTIMADEFilterStrategy" [project.entry-points."oteapi.parse"] -"oteapi_optimade.application/vnd.optimade+json" = "oteapi_optimade.strategies.parse:OPTIMADEParseStrategy" -"oteapi_optimade.application/vnd.OPTIMADE+json" = "oteapi_optimade.strategies.parse:OPTIMADEParseStrategy" -"oteapi_optimade.application/vnd.OPTiMaDe+json" = "oteapi_optimade.strategies.parse:OPTIMADEParseStrategy" -"oteapi_optimade.application/vnd.optimade+JSON" = "oteapi_optimade.strategies.parse:OPTIMADEParseStrategy" -"oteapi_optimade.application/vnd.OPTIMADE+JSON" = "oteapi_optimade.strategies.parse:OPTIMADEParseStrategy" -"oteapi_optimade.application/vnd.OPTiMaDe+JSON" = "oteapi_optimade.strategies.parse:OPTIMADEParseStrategy" -"oteapi_optimade.application/vnd.optimade" = "oteapi_optimade.strategies.parse:OPTIMADEParseStrategy" -"oteapi_optimade.application/vnd.OPTIMADE" = "oteapi_optimade.strategies.parse:OPTIMADEParseStrategy" -"oteapi_optimade.application/vnd.OPTiMaDe" = "oteapi_optimade.strategies.parse:OPTIMADEParseStrategy" -"oteapi_optimade.application/vnd.optimade+dlite" = "oteapi_optimade.dlite.parse:OPTIMADEDLiteParseStrategy" -"oteapi_optimade.application/vnd.OPTIMADE+dlite" = "oteapi_optimade.dlite.parse:OPTIMADEDLiteParseStrategy" -"oteapi_optimade.application/vnd.OPTiMaDe+dlite" = "oteapi_optimade.dlite.parse:OPTIMADEDLiteParseStrategy" -"oteapi_optimade.application/vnd.optimade+DLite" = "oteapi_optimade.dlite.parse:OPTIMADEDLiteParseStrategy" -"oteapi_optimade.application/vnd.OPTIMADE+DLite" = "oteapi_optimade.dlite.parse:OPTIMADEDLiteParseStrategy" -"oteapi_optimade.application/vnd.OPTiMaDe+DLite" = "oteapi_optimade.dlite.parse:OPTIMADEDLiteParseStrategy" +"oteapi_optimade.parser/optimade" = "oteapi_optimade.strategies.parse:OPTIMADEParseStrategy" +"oteapi_optimade.parser/OPTIMADE" = "oteapi_optimade.strategies.parse:OPTIMADEParseStrategy" +"oteapi_optimade.parser/OPTiMaDe" = "oteapi_optimade.strategies.parse:OPTIMADEParseStrategy" +"oteapi_optimade.parser/optimade/dlite" = "oteapi_optimade.dlite.parse:OPTIMADEDLiteParseStrategy" +"oteapi_optimade.parser/OPTIMADE/dlite" = "oteapi_optimade.dlite.parse:OPTIMADEDLiteParseStrategy" +"oteapi_optimade.parser/OPTiMaDe/dlite" = "oteapi_optimade.dlite.parse:OPTIMADEDLiteParseStrategy" +"oteapi_optimade.parser/optimade/DLite" = "oteapi_optimade.dlite.parse:OPTIMADEDLiteParseStrategy" +"oteapi_optimade.parser/OPTIMADE/DLite" = "oteapi_optimade.dlite.parse:OPTIMADEDLiteParseStrategy" +"oteapi_optimade.parser/OPTiMaDe/DLite" = "oteapi_optimade.dlite.parse:OPTIMADEDLiteParseStrategy" [project.entry-points."oteapi.resource"] -"oteapi_optimade.optimade" = "oteapi_optimade.strategies.resource:OPTIMADEResourceStrategy" -"oteapi_optimade.OPTIMADE" = "oteapi_optimade.strategies.resource:OPTIMADEResourceStrategy" -"oteapi_optimade.OPTiMaDe" = "oteapi_optimade.strategies.resource:OPTIMADEResourceStrategy" -"oteapi_optimade.optimade+dlite" = "oteapi_optimade.strategies.resource:OPTIMADEResourceStrategy" -"oteapi_optimade.OPTIMADE+dlite" = "oteapi_optimade.strategies.resource:OPTIMADEResourceStrategy" -"oteapi_optimade.OPTiMaDe+dlite" = "oteapi_optimade.strategies.resource:OPTIMADEResourceStrategy" -"oteapi_optimade.optimade+DLite" = "oteapi_optimade.strategies.resource:OPTIMADEResourceStrategy" -"oteapi_optimade.OPTIMADE+DLite" = "oteapi_optimade.strategies.resource:OPTIMADEResourceStrategy" -"oteapi_optimade.OPTiMaDe+DLite" = "oteapi_optimade.strategies.resource:OPTIMADEResourceStrategy" +"oteapi_optimade.optimade/structures" = "oteapi_optimade.strategies.resource:OPTIMADEResourceStrategy" +"oteapi_optimade.OPTIMADE/structures" = "oteapi_optimade.strategies.resource:OPTIMADEResourceStrategy" +"oteapi_optimade.OPTiMaDe/structures" = "oteapi_optimade.strategies.resource:OPTIMADEResourceStrategy" [tool.flit.module] name = "oteapi_optimade" @@ -117,7 +110,6 @@ warn_unused_configs = true show_error_codes = true allow_redefinition = true enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] -strict = true warn_unreachable = true plugins = ["pydantic.mypy"] @@ -145,12 +137,14 @@ extend-select = [ ] ignore = [ "PLR", # Design related pylint codes + "EM101", # Using a literal string in error messages + "EM102", # Using f-strings in error messages ] isort.required-imports = ["from __future__ import annotations"] [tool.ruff.lint.per-file-ignores] "docs/examples/**" = [ - "I002", # required imports + "I002", # required imports (e.g., from __future__ import annotations) "T20", # print statements ] "tests/**" = [ @@ -163,14 +157,4 @@ addopts = "-rs --cov=oteapi_optimade --cov-report=term-missing:skip-covered --no filterwarnings = [ # Fail on any warning "error", - - # Except the following: - - # reset_field() from oteapi.models.AttrDict implements the previous behaviour of - # __delattr__(). - "ignore:.*reset_field().*:DeprecationWarning", - - # Python 3.10 deprecation warning coming from oteapi-core - # To follow the solution, see this issue: https://github.com/EMMC-ASBL/oteapi-core/issues/395 - "ignore:.*SelectableGroups dict interface is deprecated.*:DeprecationWarning", ] diff --git a/tests/dlite/test_parse.py b/tests/dlite/test_parse.py index cea260e..dd8a8b5 100644 --- a/tests/dlite/test_parse.py +++ b/tests/dlite/test_parse.py @@ -30,9 +30,10 @@ def test_parse(static_files: Path) -> None: '?filter=elements HAS ALL "Si","O"&sort=nelements&page_limit=2' ) config = { - "mediaType": "application/vnd.OPTIMADE+DLite", - "downloadUrl": url, + "parserType": "parser/OPTIMADE/DLite", "configuration": { + "mediaType": "application/vnd.OPTIMADE+DLite", + "downloadUrl": url, "datacache_config": { "expireTime": 60 * 60 * 24, "tag": "optimade", @@ -52,11 +53,12 @@ def test_parse(static_files: Path) -> None: } ) - session = OPTIMADEDLiteParseStrategy(config).initialize({}) - session = OPTIMADEDLiteParseStrategy(config).get(session) + config["configuration"].update(OPTIMADEDLiteParseStrategy(config).initialize()) + config["configuration"].update(OPTIMADEDLiteParseStrategy(config).get()) - dlite_collection = get_collection(session) - assert dlite_collection + dlite_collection = get_collection( + collection_id=config["configuration"]["collection_id"] + ) assert len(list(dlite_collection.get_labels())) == len(response_json["data"]) diff --git a/tests/strategies/test_resource.py b/tests/strategies/test_resource.py index b8d878c..da24961 100644 --- a/tests/strategies/test_resource.py +++ b/tests/strategies/test_resource.py @@ -9,8 +9,14 @@ import pytest if TYPE_CHECKING: + import sys from pathlib import Path + if sys.version_info >= (3, 10): + from typing import Literal + else: + from typing_extensions import Literal + from requests_mock import Mocker @@ -18,6 +24,7 @@ def resource_config() -> dict[str, str]: """A resource config dictionary for test purposes.""" return { + "resourceType": "optimade/structures", "accessService": "optimade", "accessUrl": ( "https://example.org/some/base/v0.1/optimade/v1/structures" @@ -27,92 +34,86 @@ def resource_config() -> dict[str, str]: @pytest.mark.parametrize( - "session", [None, {"optimade_config": "content"}], ids=["None", "dict"] + "session", [None, {"optimade_config": {"version": "v1"}}], ids=["None", "dict"] ) def test_initialize(session: dict | None, resource_config: dict[str, str]) -> None: """Test the `initialize()` method.""" - from oteapi.models import SessionUpdate + from oteapi.models import AttrDict from oteapi_optimade.strategies.resource import OPTIMADEResourceStrategy - session_update = OPTIMADEResourceStrategy(resource_config).initialize(session) + if session: + resource_config["configuration"] = session + + output = OPTIMADEResourceStrategy(resource_config).initialize() - assert isinstance(session_update, SessionUpdate) - assert {**session_update} == {} + assert isinstance(output, AttrDict) + assert {**output} == {} def test_get_no_session( resource_config: dict[str, str], static_files: Path, requests_mock: Mocker ) -> None: - """Test the `get()` method - session is `None`.""" + """Test the `get()` method - no previous strategies have run + (i.e., no session info has been added to `configuration`).""" from optimade.adapters import Structure - from oteapi_optimade.models.strategies.resource import OPTIMADEResourceSession + from oteapi_optimade.models.strategies.resource import OPTIMADEResourceResult from oteapi_optimade.strategies.resource import OPTIMADEResourceStrategy sample_file = static_files / "optimade_response.json" requests_mock.get(resource_config["accessUrl"], content=sample_file.read_bytes()) - session = OPTIMADEResourceStrategy(resource_config).get() + output = OPTIMADEResourceStrategy(resource_config).get() - assert isinstance(session, OPTIMADEResourceSession) - assert session.optimade_config is None - assert session.optimade_resource_model == f"{Structure.__module__}:Structure" - assert session.optimade_resources - for resource in session.optimade_resources: + assert isinstance(output, OPTIMADEResourceResult) + assert output.optimade_config is None + assert output.optimade_resource_model == f"{Structure.__module__}:Structure" + assert output.optimade_resources + for resource in output.optimade_resources: assert Structure(resource) -@pytest.mark.parametrize( - ("accessService", "use_dlite"), - [ - ("optimade+dlite", False), - ("OPTIMADE+dlite", False), - ("OPTiMaDe+dlite", False), - ("optimade+DLite", False), - ("OPTIMADE+DLite", False), - ("OPTiMaDe+DLite", False), - ("optimade", True), - ("OPTIMADE", True), - ("OPTiMaDe", True), - ("optimade+dlite", True), - ("OPTIMADE+dlite", True), - ("OPTiMaDe+dlite", True), - ("optimade+DLite", True), - ("OPTIMADE+DLite", True), - ("OPTiMaDe+DLite", True), - ], -) +@pytest.mark.parametrize("use_dlite", [True, False]) +@pytest.mark.parametrize("accessService_root", ["optimade", "OPTIMADE", "OPTiMaDe"]) +@pytest.mark.parametrize("accessService_appendix", ["", "+dlite", "+DLite"]) def test_use_dlite( resource_config: dict[str, str], static_files: Path, requests_mock: Mocker, - accessService: str, - use_dlite: bool, + accessService_root: Literal["optimade", "OPTIMADE", "OPTiMaDe"], + accessService_appendix: Literal["+dlite", "+DLite"], + use_dlite: Literal[True, False], ) -> None: - """Test the `get()` method - session is `None` and use_dlite is True.""" + """Test the `get()` method when `use_dlite` is set and for different valid accessService values.""" from optimade.adapters import Structure from oteapi_dlite.utils import get_collection - from oteapi_optimade.models.strategies.resource import OPTIMADEResourceSession + from oteapi_optimade.models.strategies.resource import OPTIMADEResourceResult from oteapi_optimade.strategies.resource import OPTIMADEResourceStrategy sample_file = static_files / "optimade_response.json" requests_mock.get(resource_config["accessUrl"], content=sample_file.read_bytes()) - resource_config["accessService"] = accessService + resource_config["accessService"] = accessService_root + accessService_appendix resource_config["configuration"] = {"use_dlite": use_dlite} - session = OPTIMADEResourceStrategy(resource_config).initialize() - session = OPTIMADEResourceStrategy(resource_config).get(session) + resource_config["configuration"].update( + OPTIMADEResourceStrategy(resource_config).initialize() + ) + output = OPTIMADEResourceStrategy(resource_config).get() - assert isinstance(session, OPTIMADEResourceSession) - assert session.optimade_config is None - assert session.optimade_resource_model == f"{Structure.__module__}:Structure" - assert session.optimade_resources - for resource in session.optimade_resources: + assert isinstance(output, OPTIMADEResourceResult) + assert output.optimade_config is None + assert output.optimade_resource_model == f"{Structure.__module__}:Structure" + assert output.optimade_resources + for resource in output.optimade_resources: assert Structure(resource) - assert "collection_id" in session - dlite_collection = get_collection(session) - assert dlite_collection + if use_dlite or "+dlite" in resource_config["accessService"].lower(): + assert "collection_id" in resource_config["configuration"] + assert get_collection( + collection_id=resource_config["configuration"]["collection_id"] + ).get_labels() + else: + assert "collection_id" not in resource_config["configuration"]