From d54c924deba5c0c7be125a33fb272e5164c4cfa2 Mon Sep 17 00:00:00 2001 From: Alvin Noe Ladines <ladinesalvinnoe@gmail.com> Date: Fri, 11 Oct 2019 14:46:32 +0200 Subject: [PATCH] Added more detailed test for optimade API. --- nomad/app/optimade/endpoints.py | 28 ++++------- nomad/app/optimade/models.py | 33 ++++++++++-- nomad/normalizing/optimade.py | 12 +++-- tests/app/test_optimade.py | 89 +++++++++++++++++++++++++-------- 4 files changed, 117 insertions(+), 45 deletions(-) diff --git a/nomad/app/optimade/endpoints.py b/nomad/app/optimade/endpoints.py index 375e8e5660..c2d22b2dd2 100644 --- a/nomad/app/optimade/endpoints.py +++ b/nomad/app/optimade/endpoints.py @@ -21,7 +21,8 @@ from nomad.metainfo.optimade import OptimadeEntry from .api import api, url from .models import json_api_single_response_model, entry_listing_endpoint_parser, Meta, \ - Links, CalculationDataObject, single_entry_endpoint_parser, base_endpoint_parser + Links, CalculationDataObject, single_entry_endpoint_parser, base_endpoint_parser,\ + json_api_info_response_model from .filterparser import parse_filter, FilterException @@ -33,7 +34,7 @@ def base_request_args(): if request.args.get('response_format', 'json') != 'json': abort(400, 'Response format is not supported.') - properties_str = request.args.get('response_fields', None) + properties_str = request.args.get('request_fields', None) if properties_str is not None: return properties_str.split(',') return None @@ -131,26 +132,19 @@ class CalculationInfo(Resource): @api.doc('calculations_info') @api.response(400, 'Invalid requests, e.g. bad parameter.') @api.expect(base_endpoint_parser, validate=True) - @api.marshal_with(json_api_single_response_model, skip_none=True, code=200) + @api.marshal_with(json_api_info_response_model, skip_none=True, code=200) def get(self): """ Returns information relating to the API implementation- """ base_request_args() result = { - 'type': 'info', - 'id': 'calculation', - 'attributes': { - 'description': 'A calculations entry.', - # TODO non optimade, nomad specific properties - 'properties': { - attr.name: dict(description=attr.description) - for attr in OptimadeEntry.m_def.all_properties.values() - }, - 'formats': ['json'], - 'output_fields_by_format': { - 'json': OptimadeEntry.m_def.all_properties.keys() - } - } + 'description': 'a calculation entry', + 'properties': { + attr.name: dict(description=attr.description) + for attr in OptimadeEntry.m_def.all_properties.values()}, + 'formats': ['json'], + 'output_fields_by_format': { + 'json': OptimadeEntry.m_def.all_properties.keys()} } return dict( diff --git a/nomad/app/optimade/models.py b/nomad/app/optimade/models.py index 32d47f56ca..3fbef7e238 100644 --- a/nomad/app/optimade/models.py +++ b/nomad/app/optimade/models.py @@ -212,15 +212,32 @@ json_api_data_object_model = api.model('DataObject', { # further optional fields: links, meta, relationships }) +json_api_calculation_info_model = api.model('CalculationInfo', { + 'description': fields.String( + description='Description of the entry'), + + 'properties': fields.Raw( + description=('A dictionary describing queryable properties for this ' + 'entry type, where each key is a property name')), + + 'formats': fields.List( + fields.String(), + required=True, + description='List of output formats available for this type of entry'), + + 'output_fields_by_format': fields.Raw( + description=('Dictionary of available output fields for this entry' + 'type, where the keys are the values of the formats list' + 'and the values are the keys of the properties dictionary')) + +}) + class CalculationDataObject: def __init__(self, calc: CalcWithMetadata, request_fields: Set[str] = None): def include(key): - if request_fields is None or \ - (key == 'optimade' and key in request_fields) or \ - (key != 'optimade' and '_nomad_%s' % key in request_fields): - + if request_fields is None or (key in request_fields): return True return False @@ -266,6 +283,14 @@ json_api_list_response_model = api.inherit( description=('The list of returned response objects.')) }) +json_api_info_response_model = api.inherit( + 'SingleResponse', json_api_response_model, { + 'data': fields.Nested( + model=json_api_calculation_info_model, + required=True, + description=('The returned response object.')) + }) + base_endpoint_parser = api.parser() base_endpoint_parser.add_argument( diff --git a/nomad/normalizing/optimade.py b/nomad/normalizing/optimade.py index e439c9a2ee..c91754a75d 100644 --- a/nomad/normalizing/optimade.py +++ b/nomad/normalizing/optimade.py @@ -16,6 +16,7 @@ from typing import Any, Dict import numpy as np import re import ase.data +from string import ascii_uppercase from nomad.normalizing.normalizer import SystemBasedNormalizer from nomad.metainfo import units @@ -77,9 +78,12 @@ class OptimadeNormalizer(SystemBasedNormalizer): optimade.chemical_formula_reduced = get_value('chemical_composition_reduced') optimade.chemical_formula_hill = get_value('chemical_composition_bulk_reduced') optimade.chemical_formula_descriptive = optimade.chemical_formula_hill - optimade.chemical_formula_anonymous = ''.join([ - '%s' % element + (str(atom_counts[element]) if atom_counts[element] > 1 else '') - for element in optimade.elements]) + optimade.chemical_formula_anonymous = '' + for i in range(len(optimade.elements)): + part = '%s' % ascii_uppercase[i % len(ascii_uppercase)] + if atom_counts[optimade.elements[i]] > 1: + part += str(atom_counts[optimade.elements[i]]) + optimade.chemical_formula_anonymous += part # sites optimade.nsites = len(nomad_species) @@ -94,7 +98,7 @@ class OptimadeNormalizer(SystemBasedNormalizer): for species_label in set(nomad_species): match = re.match(species_re, species_label) - element_label, index = match.groups(1), match.groups(2) + element_label = match.group(1) species = optimade.m_create(Species) species.name = species_label diff --git a/tests/app/test_optimade.py b/tests/app/test_optimade.py index 9b73107e28..d862d1ada8 100644 --- a/tests/app/test_optimade.py +++ b/tests/app/test_optimade.py @@ -27,6 +27,7 @@ from nomad.app.optimade import parse_filter, url from tests.app.test_app import BlueprintClient from tests.test_normalizing import run_normalize from tests.conftest import clear_elastic +from tests.utils import assert_exception @pytest.fixture(scope='session') @@ -36,10 +37,8 @@ def api(nomad_app): def test_get_entry(published: Upload): calc_id = list(published.calcs)[0].calc_id - with published.upload_files.archive_file(calc_id) as f: data = json.load(f) - assert 'OptimadeEntry' in data search_result = search.SearchRequest().search_parameter('calc_id', calc_id).execute_paginated()['results'][0] assert 'optimade' in search_result @@ -80,7 +79,8 @@ def create_test_structure( calc.optimade = None # type: ignore proc.Calc.from_calc_with_metadata(calc).save() - search.Entry.from_calc_with_metadata(calc).save() + search_entry = search.Entry.from_calc_with_metadata(calc) + search_entry.save() assert proc.Calc.objects(calc_id__in=[calc_id]).count() == 1 @@ -128,6 +128,11 @@ def example_structures(meta_info, elastic_infra, mongo_infra): ('elements HAS ANY "C"', 1), ('elements HAS ONLY "C"', 0), ('elements HAS ONLY "H", "O"', 3), + ('nelements >= 2 AND elements HAS ONLY "H", "O"', 3), + ('nelements >= 2 AND elements HAS ALL "H", "O", "C"', 1), + ('nelements >= 2 AND NOT elements HAS ALL "H", "O", "C"', 3), + ('NOT nelements = 2 AND elements HAS ANY "H", "O", "C"', 1), + ('NOT nelements = 3 AND NOT elements HAS ONLY "H", "O"', 0), ('elements:elements_ratios HAS "H":>0.66', 2), ('elements:elements_ratios HAS ALL "O":>0.33', 3), ('elements:elements_ratios HAS ALL "O":>0.33,"O":<0.34', 2), @@ -141,6 +146,13 @@ def example_structures(meta_info, elastic_infra, mongo_infra): ('chemical_formula_reduced STARTS WITH "H2"', 3), ('chemical_formula_reduced ENDS WITH "C"', 1), ('chemical_formula_reduced ENDS "C"', 1), + ('chemical_formula_hill CONTAINS "1"', 0), + ('chemical_formula_hill STARTS WITH "H" AND chemical_formula_hill ENDS WITH "O"', 3), + ('NOT chemical_formula_descriptive ENDS WITH "1"', 4), + ('chemical_formula_descriptive CONTAINS "C" AND NOT chemical_formula_descriptive STARTS WITH "O"', 1), + ('NOT chemical_formula_anonymous STARTS WITH "A"', 0), + ('chemical_formula_anonymous CONTAINS "AB2" AND chemical_formula_anonymous ENDS WITH "C"', 1), + ('nsites >=3 AND LENGTH elements = 2', 2), ('LENGTH elements = 2', 3), ('LENGTH elements = 3', 1), ('LENGTH dimension_types = 0', 3), @@ -150,12 +162,19 @@ def example_structures(meta_info, elastic_infra, mongo_infra): ('nelements = 3 OR LENGTH dimension_types = 1', 2), ('nelements > 1 OR LENGTH dimension_types = 1 AND nelements = 2', 4), ('(nelements > 1 OR LENGTH dimension_types = 1) AND nelements = 2', 3), - ('NOT LENGTH dimension_types = 1', 3) + ('NOT LENGTH dimension_types = 1', 3), + ('LENGTH nelements = 1', -1), + ('chemical_formula_anonymous starts with "A"', -1), + ('elements HAS ONY "H", "O"', -1) ]) def test_optimade_parser(example_structures, query, results): - query = parse_filter(query) - result = search.SearchRequest(query=query).execute_paginated() - assert result['pagination']['total'] == results + if results >= 0: + query = parse_filter(query) + result = search.SearchRequest(query=query).execute_paginated() + assert result['pagination']['total'] == results + else: + with assert_exception(): + query = parse_filter(query) def test_url(): @@ -166,42 +185,72 @@ def test_list_endpoint(api, example_structures): rv = api.get('/calculations') assert rv.status_code == 200 data = json.loads(rv.data) - # TODO replace with real assertions - # print(json.dumps(data, indent=2)) + for entry in ['data', 'links', 'meta']: + assert entry in data + assert len(data['data']) == 4 + + +def assert_eq_attrib(data, key, ref, item=None): + if item is None: + assert data['data']['attributes'][key] == ref + else: + assert data['data'][item]['attributes'][key] == ref def test_list_endpoint_request_fields(api, example_structures): rv = api.get('/calculations?request_fields=nelements,elements') assert rv.status_code == 200 data = json.loads(rv.data) - # TODO replace with real assertions - # print(json.dumps(data, indent=2)) + ref_elements = [['H', 'O'], ['C', 'H', 'O'], ['H', 'O'], ['H', 'O']] + for i in range(len(data['data'])): + rf = list(data['data'][i]['attributes'].keys()) + rf.sort() + assert rf == ['elements', 'nelements'] + assert_eq_attrib(data, 'elements', ref_elements[i], i) + assert_eq_attrib(data, 'nelements', len(ref_elements[i]), i) + + +def test_single_endpoint_request_fields(api, example_structures): + rv = api.get('/calculations/%s?request_fields=nelements,elements' % 'test_calc_id_1') + assert rv.status_code == 200 + data = json.loads(rv.data) + ref_elements = ['H', 'O'] + rf = list(data['data']['attributes'].keys()) + assert rf == ['elements', 'nelements'] + assert_eq_attrib(data, 'elements', ref_elements) + assert_eq_attrib(data, 'nelements', len(ref_elements)) def test_single_endpoint(api, example_structures): rv = api.get('/calculations/%s' % 'test_calc_id_1') assert rv.status_code == 200 data = json.loads(rv.data) - # TODO replace with real assertions - # print(json.dumps(data, indent=2)) + for key in ['type', 'id', 'attributes']: + assert key in data['data'] + fields = ['elements', 'nelements', 'elements_ratios', + 'chemical_formula_descriptive', 'chemical_formula_reduced', + 'chemical_formula_hill', 'chemical_formula_anonymous', + 'dimension_types', 'lattice_vectors', 'cartesian_site_positions', + 'nsites', 'species_at_sites', 'species'] + for field in fields: + assert field in data['data']['attributes'] def test_base_info_endpoint(api): rv = api.get('/info') assert rv.status_code == 200 data = json.loads(rv.data) - # TODO replace with real assertions - # print(json.dumps(data, indent=2)) + for key in ['type', 'id', 'attributes']: + assert key in data['data'] + assert data['data']['type'] == 'info' + assert data['data']['id'] == '/' def test_calculation_info_endpoint(api): rv = api.get('/info/calculation') assert rv.status_code == 200 data = json.loads(rv.data) - # TODO replace with real assertions - # print(json.dumps(data, indent=2)) - + for key in ['description', 'properties', 'formats', 'output_fields_by_format']: + assert key in data['data'] -# TODO test single with request_fields -# TODO test errors # TODO test response format (deny everything but json) -- GitLab