Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add adapters - Base design + 'structures' (+ 'references'... sort of) #241

Merged
merged 32 commits into from
Apr 22, 2020
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
fbe83dc
Add adapters - First adapter is for 'structures'
CasperWA Mar 28, 2020
b906860
Rename pymatgen converter function
CasperWA Mar 30, 2020
0effe72
Add some debug logging to config (for config path)
CasperWA Mar 30, 2020
109496c
Add base adapter
CasperWA Mar 30, 2020
a6c4b18
Add Reference adapter
CasperWA Mar 30, 2020
a8efec2
Tests for Structure adapter
CasperWA Mar 30, 2020
39137a1
Update dependencies with client deps
CasperWA Mar 30, 2020
741e5b1
Update dependencies
CasperWA Mar 30, 2020
0aed01e
Update test to recognize if ase is installed
CasperWA Mar 30, 2020
572a277
Install only "testing" extra for CI pytest run
CasperWA Mar 30, 2020
ef420ff
Use pytest instead of unittest for writing tests
CasperWA Mar 30, 2020
e882b8b
Testing conversion functions
CasperWA Mar 30, 2020
97bdefb
Install all dependencies for CI testing
CasperWA Apr 6, 2020
2c71a11
Set up AiiDA environment for CI testing
CasperWA Apr 7, 2020
c7ccc28
Expand CI testing for adapter conversions
CasperWA Apr 7, 2020
ad45225
Properly handle vacancies for converting to AiiDA
CasperWA Apr 7, 2020
68cc650
Update eager CI tests
CasperWA Apr 7, 2020
7fd4cf5
Invoke postgres using GitHub services
CasperWA Apr 7, 2020
6704c0b
Add tests for references adapter
CasperWA Apr 7, 2020
2c7aa92
Remove `formatting` from get_cif
CasperWA Apr 7, 2020
f419565
Tests for special species for adapter converters
CasperWA Apr 7, 2020
322ab54
Apply suggestions from code review
CasperWA Apr 16, 2020
2deae37
Better logic for handling lattice vectors
CasperWA Apr 16, 2020
50d7cd8
Add common converters for all adapter classes
CasperWA Apr 16, 2020
7cee376
Restructure testing of adapters
CasperWA Apr 16, 2020
3ad0e28
Search also in resource.attributes
CasperWA Apr 17, 2020
b71b2ce
Make sure using getattr() also works for nesting
CasperWA Apr 17, 2020
71e69ca
Use `as_` instead of `get_`
CasperWA Apr 20, 2020
ca6f4ae
Limit namespace in adapters from models
CasperWA Apr 20, 2020
6baf226
For CIF: Convert to fract. positions (if 3D)
CasperWA Apr 20, 2020
f2f2b4d
Add SCALE(n) to PDB
CasperWA Apr 21, 2020
f88df44
Open usage of PDBx/mmCIF format
CasperWA Apr 22, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/aiida/profile.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
profile: PLACEHOLDER_PROFILE
email: aiida@localhost
first_name: AiiDA
last_name: OPTIMADE
institution: Materials-Consortia
db_backend: PLACEHOLDER_BACKEND
db_engine: postgresql_psycopg2
db_host: localhost
db_port: 5432
db_name: PLACEHOLDER_DATABASE_NAME
db_username: postgres
db_password: test
repository: PLACEHOLDER_REPOSITORY
13 changes: 13 additions & 0 deletions .github/aiida/setup_aiida.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -ev

# Replace the placeholders in configuration files with actual values
CONFIG="${GITHUB_WORKSPACE}/.github/aiida"
sed -i "s|PLACEHOLDER_BACKEND|${AIIDA_TEST_BACKEND}|" "${CONFIG}/profile.yaml"
sed -i "s|PLACEHOLDER_PROFILE|test_${AIIDA_TEST_BACKEND}|" "${CONFIG}/profile.yaml"
sed -i "s|PLACEHOLDER_DATABASE_NAME|test_${AIIDA_TEST_BACKEND}|" "${CONFIG}/profile.yaml"
sed -i "s|PLACEHOLDER_REPOSITORY|/tmp/test_repository_test_${AIIDA_TEST_BACKEND}/|" "${CONFIG}/profile.yaml"

verdi setup --config "${CONFIG}/profile.yaml"

verdi profile setdefault test_${AIIDA_TEST_BACKEND}
42 changes: 34 additions & 8 deletions .github/workflows/deps_eager.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,25 @@ jobs:
python-version: [3.6, 3.7, 3.8]

services:
mongo:
image: mongo:4.2
ports:
- 27017:27017
mongo:
image: mongo:4.2
ports:
- 27017:27017
postgres:
image: postgres:10
env:
POSTGRES_DB: test_django
POSTGRES_PASSWORD: test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432

steps:
- uses: actions/checkout@master
- uses: actions/checkout@v2

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
Expand All @@ -35,8 +47,8 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install -U setuptools
# Install package clean with all dependencies followed by eager update install
pip install -e .[testing,django,elastic]
# Install package clean with test dependencies followed by eager update install
pip install -e .[testing]
pip install -U --upgrade-strategy eager -r .github/workflows/requirements_eager.txt

- name: Run tests on updated packages
Expand All @@ -49,6 +61,20 @@ jobs:
env:
OPTIMADE_CI_FORCE_MONGO: 0

- name: Install adapter conversion dependencies (eagerly)
run: |
pip install -U --upgrade-strategy eager -e .[all]
# AiiDA-specific
reentry scan

- name: Setup up environment for AiiDA
env:
AIIDA_TEST_BACKEND: django
run: .github/aiida/setup_aiida.sh

- name: Run previously skipped tests for adapter conversion
run: pytest -rs --cov=./optimade/ --cov-report=xml --cov-append tests/adapters/

# deps_clean-install:
# runs-on: ubuntu-latest
# strategy:
Expand All @@ -58,7 +84,7 @@ jobs:
# python-version: [3.6, 3.7, 3.8]

# steps:
# - uses: actions/checkout@master
# - uses: actions/checkout@v2
CasperWA marked this conversation as resolved.
Show resolved Hide resolved

# - name: Set up Python ${{ matrix.python-version }}
# uses: actions/setup-python@v1
Expand Down
46 changes: 36 additions & 10 deletions .github/workflows/deps_lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@master
- uses: actions/checkout@v2

- name: Set up Python 3.7
uses: actions/setup-python@v1
Expand All @@ -37,7 +37,7 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@master
- uses: actions/checkout@v2

- name: Set up Python 3.7
uses: actions/setup-python@v1
Expand All @@ -59,7 +59,7 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@master
- uses: actions/checkout@v2

- name: Set up Python 3.7
uses: actions/setup-python@v1
Expand Down Expand Up @@ -94,7 +94,7 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@master
- uses: actions/checkout@v2

- name: Build the Docker images
run: docker-compose build
Expand Down Expand Up @@ -138,13 +138,25 @@ jobs:
python-version: [3.6, 3.7, 3.8]

services:
mongo:
image: mongo:4.2
ports:
- 27017:27017
mongo:
image: mongo:4.2
ports:
- 27017:27017
postgres:
image: postgres:10
env:
POSTGRES_DB: test_django
POSTGRES_PASSWORD: test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432

steps:
- uses: actions/checkout@master
- uses: actions/checkout@v2

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
Expand All @@ -157,7 +169,7 @@ jobs:
pip install -U setuptools
# Install static dependencies followed by "normal" install
python -m pip install -r .github/workflows/requirements.txt
pip install -e .[all]
pip install -e .[testing]

- name: Run all tests (using a real MongoDB)
run: pytest -rs --cov=./optimade/ --cov-report=xml
Expand All @@ -169,6 +181,20 @@ jobs:
env:
OPTIMADE_CI_FORCE_MONGO: 0

- name: Install adapter conversion dependencies
run: |
pip install -e .[all]
# AiiDA-specific
reentry scan

- name: Setup up environment for AiiDA
env:
AIIDA_TEST_BACKEND: django
run: .github/aiida/setup_aiida.sh

- name: Run previously skipped tests for adapter conversion
run: pytest -rs --cov=./optimade/ --cov-report=xml --cov-append tests/adapters/

- name: Upload coverage to Codecov
if: matrix.python-version == 3.7 && github.repository == 'Materials-Consortia/optimade-python-tools'
uses: codecov/codecov-action@v1
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish-on-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@master
uses: actions/checkout@v2

- name: Set up Python 3.7
uses: actions/setup-python@v1
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
lark-parser==0.8.1
fastapi==0.52.0
lark-parser==0.8.5
fastapi==0.53.1
CasperWA marked this conversation as resolved.
Show resolved Hide resolved
pydantic==1.4
email_validator==1.0.5
requests==2.23.0
Expand Down
7 changes: 7 additions & 0 deletions optimade/adapters/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# pylint: disable=undefined-variable
from .exceptions import *
from .references import *
from .structures import *


__all__ = exceptions.__all__ + references.__all__ + structures.__all__
120 changes: 120 additions & 0 deletions optimade/adapters/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import re
from typing import Union, Dict, Callable, Any, Tuple, List

from pydantic import BaseModel # pylint: disable=no-name-in-module

from optimade.models import EntryResource


class EntryAdapter:
"""Base class for lazy resource entry adapters
:param entry: JSON OPTIMADE single resource entry.
CasperWA marked this conversation as resolved.
Show resolved Hide resolved
"""

ENTRY_RESOURCE: EntryResource = EntryResource
_type_converters: Dict[str, Callable] = {}
CasperWA marked this conversation as resolved.
Show resolved Hide resolved

def __init__(self, entry: dict):
self._entry = None
self._converted = {}

self.entry = entry

# Note that these return also the default values for otherwise non-provided properties.
self._common_converters = {
"json": self.entry.json, # Return JSON serialized string, see https://pydantic-docs.helpmanual.io/usage/exporting_models/#modeljson
"dict": self.entry.dict, # Return Python dict, see https://pydantic-docs.helpmanual.io/usage/exporting_models/#modeldict
}

@property
def entry(self):
"""Get OPTIMADE entry"""
return self._entry

@entry.setter
def entry(self, value: dict):
"""Set OPTIMADE entry
If already set, print that this can _only_ be set once.
"""
if self._entry is None:
self._entry = self.ENTRY_RESOURCE(**value)
else:
print("entry can only be set once and is already set.")

def convert(self, format: str) -> Any:
"""Convert OPTIMADE entry to desired format"""
if (
format not in self._type_converters
and format not in self._common_converters
):
raise AttributeError(
f"Non-valid entry type to convert to: {format}\n"
f"Valid entry types: {tuple(self._type_converters.keys()) + tuple(self._common_converters.keys())}"
)

if self._converted.get(format, None) is None:
if format in self._type_converters:
self._converted[format] = self._type_converters[format](self.entry)
else:
self._converted[format] = self._common_converters[format]()

return self._converted[format]

@staticmethod
def _get_model_attributes(
starting_instances: Union[Tuple[BaseModel], List[BaseModel]], name: str
) -> Any:
"""Helper method for retrieving the OPTIMADE model's attribute, supporting "."-nested attributes"""
for res in starting_instances:
nested_attributes = name.split(".")
for nested_attribute in nested_attributes:
if nested_attribute in getattr(res, "__fields__", {}):
res = getattr(res, nested_attribute)
else:
res = None
break
if res is not None:
return res
raise AttributeError

def __getattr__(self, name: str) -> Any:
"""Get converted entry or attribute from OPTIMADE entry
Support any level of "."-nested OPTIMADE ENTRY_RESOURCE attributes, e.g., `attributes.species` for StuctureResource.
NOTE: All nested attributes must individually be subclasses of `pydantic.BaseModel`,
i.e., one can not access nested attributes in lists by passing a "."-nested `name` to this method,
e.g., `attributes.species.name` or `attributes.species[0].name` will not work for variable `name`.

Order:
- Try to return converted entry if using `as_<_type_converters key>`.
- Try to return OPTIMADE ENTRY_RESOURCE (nested) attribute.
- Try to return OPTIMADE ENTRY_RESOURCE.attributes (nested) attribute.
- Raise AttributeError
"""
# as_<entry_type>
if name.startswith("as_"):
entry_type = "_".join(name.split("_")[1:])
return self.convert(entry_type)

# Try returning ENTRY_RESOURCE attribute
try:
res = self._get_model_attributes((self.entry, self.entry.attributes), name)
except AttributeError:
pass
else:
return res

# Non-valid attribute
entry_resource_name = re.match(
r"(<class ')([a-zA-Z_]+\.)*([a-zA-Z_]+)('>)", str(self.ENTRY_RESOURCE)
)
entry_resource_name = (
entry_resource_name.group(3)
if entry_resource_name is not None
else "UNKNOWN RESOURCE"
)
raise AttributeError(
f"Unknown attribute: {name}\n"
"If you want to get a converted entry as <entry_type> use `as_<entry_type>`, "
f"where `<entry_type>` is one of {tuple(self._type_converters.keys()) + tuple(self._common_converters.keys())}\n"
f"Otherwise, you can try to retrieve an OPTIMADE {entry_resource_name} attribute or property."
)
5 changes: 5 additions & 0 deletions optimade/adapters/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
__all__ = ("ConversionError",)


class ConversionError(Exception):
"""Could not convert entry to format"""
16 changes: 16 additions & 0 deletions optimade/adapters/references/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from typing import Union

from optimade.models import ReferenceResource

from optimade.adapters.base import EntryAdapter


__all__ = ("Reference",)


class Reference(EntryAdapter):
"""Lazy reference resource converter
:param reference: a single JSON OPTIMADE reference resource entry.
"""

ENTRY_RESOURCE = ReferenceResource
30 changes: 30 additions & 0 deletions optimade/adapters/structures/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from typing import Union

from optimade.models import StructureResource

from optimade.adapters.base import EntryAdapter

from .aiida import get_aiida_structure_data
from .ase import get_ase_atoms
from .cif import get_cif
from .proteindatabank import get_pdb, get_pdbx_mmcif
from .pymatgen import get_pymatgen


__all__ = ("Structure",)


class Structure(EntryAdapter):
"""Lazy structure resource converter
:param structure: a single JSON OPTIMADE structure resource entry.
"""

ENTRY_RESOURCE = StructureResource
_type_converters = {
"aiida_structuredata": get_aiida_structure_data,
"ase": get_ase_atoms,
"cif": get_cif,
"pdb": get_pdb,
"pdbx_mmcif": get_pdbx_mmcif,
"pymatgen": get_pymatgen,
}
Copy link
Member

@ltalirz ltalirz Mar 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eventually, this dictionary might become an instance of a ConverterCollection class that could include checks on the interface of the functions it contains etc.
For the moment, this is fine

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea. And definitely one for the future, i.e., not something I will explore at this time.