Skip to content

Commit

Permalink
Merge pull request #1228 from ltalirz/add_cifdata_rest
Browse files Browse the repository at this point in the history
Add REST endpoint for CifData
  • Loading branch information
ltalirz authored Mar 7, 2018
2 parents f05a071 + 173a752 commit 22ca65e
Show file tree
Hide file tree
Showing 10 changed files with 717 additions and 460 deletions.
4 changes: 4 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
.travis_data/test_setup.py|
.travis-data/test_plugin_testcase.py|
aiida/backends/tests/verdi_commands.py|
aiida/restapi/resources.py|
aiida/restapi/translator/data/cif.py|
aiida/utils/fixtures.py
)$ # $ -> end of file path, to add a directory, give full/path/.*
Expand All @@ -31,6 +33,8 @@
.travis_data/test_setup.py|
.travis-data/test_plugin_testcase.py|
aiida/backends/tests/verdi_commands.py|
aiida/restapi/translator/data/cif.py|
aiida/restapi/resources.py|
aiida/utils/fixtures.py
)$
Expand Down
73 changes: 52 additions & 21 deletions aiida/backends/tests/restapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from aiida.restapi.api import App, AiidaApi

StructureData = DataFactory('structure')
CifData = DataFactory('cif')
ParameterData = DataFactory('parameter')
KpointsData = DataFactory('array.kpoints')

Expand Down Expand Up @@ -50,6 +51,7 @@ def setUpClass(cls):
LIMIT_DEFAULT=cls._LIMIT_DEFAULT)

cls.app = App(__name__)
cls.app.config['TESTING'] = True
api = AiidaApi(cls.app, **kwargs)

# create test inputs
Expand All @@ -58,6 +60,9 @@ def setUpClass(cls):
structure.append_atom(position=(0., 0., 0.), symbols=['Ba'])
structure.store()

cif = CifData(ase=structure.get_ase())
cif.store()

parameter1 = ParameterData(dict={"a": 1, "b": 2})
parameter1.store()

Expand Down Expand Up @@ -127,41 +132,53 @@ def process_dummy_data(cls):
"""
This functions prepare atomic chunks of typical responses from the
RESTapi and puts them into class attributes
"""
computer_projects = ["id", "uuid", "name", "hostname",
#TODO: Storing the different nodes as lists and accessing them
# by their list index is very fragile and a pain to debug.
# Please change this!
computer_projections = ["id", "uuid", "name", "hostname",
"transport_type", "scheduler_type"]
computers = QueryBuilder().append(
Computer, tag="comp", project=computer_projects).order_by(
Computer, tag="comp", project=computer_projections).order_by(
{'comp': [{'name': {'order': 'asc'}}]}).dict()

# Cast UUID into a string (e.g. in sqlalchemy it comes as a UUID object)
computers = [_['comp'] for _ in computers]
for comp in computers:
if comp['uuid'] is not None:
comp['uuid'] = str(comp['uuid'])
cls._dummy_data["computers"] = computers


calculation_projects = ["id", "uuid", "user_id", "type"]
calculation_projections = ["id", "uuid", "user_id", "type"]
calculations = QueryBuilder().append(Calculation, tag="calc",
project=calculation_projects).order_by(
project=calculation_projections).order_by(
{'calc': [{'id': {'order': 'desc'}}]}).dict()

calculations = [_['calc'] for _ in calculations]
for calc in calculations:
if calc['uuid'] is not None:
calc['uuid'] = str(calc['uuid'])
cls._dummy_data["calculations"] = calculations

data_projects = ["id", "uuid", "user_id", "type"]
data = QueryBuilder().append(Data, tag="data", project=data_projects).order_by(
data_projections = ["id", "uuid", "user_id", "type"]
data_types = {
'cifdata': CifData,
'parameterdata': ParameterData,
'structuredata': StructureData,
'data': Data,
}
for label, dataclass in data_types.iteritems():
data = QueryBuilder().append(dataclass, tag="data", project=data_projections).order_by(
{'data': [{'id': {'order': 'desc'}}]}).dict()
data = [_['data'] for _ in data]
for datum in data:
if datum['uuid'] is not None:
datum['uuid'] = str(datum['uuid'])
data = [_['data'] for _ in data]

cls._dummy_data["computers"] = computers
cls._dummy_data["calculations"] = calculations
cls._dummy_data["data"] = data
for datum in data:
if datum['uuid'] is not None:
datum['uuid'] = str(datum['uuid'])

cls._dummy_data[label] = data


def split_path(self, url):
Expand Down Expand Up @@ -208,7 +225,8 @@ def process_test(self, node_type, url, full_list=False, empty_list=False,
expected_errormsg=None, uuid=None, result_node_type=None,
result_name=None):
"""
Get the full list of nodes from database
Check whether response matches expected values.
:param node_type: url requested fot the type of the node
:param url: web url
:param full_list: if url is requested to get full list
Expand All @@ -224,8 +242,9 @@ def process_test(self, node_type, url, full_list=False, empty_list=False,
if result_node_type == None and result_name == None:
result_node_type = node_type
result_name = node_type

url = self._url_prefix + url
self.app.config['TESTING'] = True

with self.app.test_client() as client:
rv = client.get(url)
response = json.loads(rv.data)
Expand Down Expand Up @@ -686,7 +705,7 @@ def test_calculation_inputs(self):
node_uuid = self.get_dummy_data()["calculations"][1]["uuid"]
self.process_test("calculations", "/calculations/" + str(
node_uuid) + "/io/inputs?orderby=id",
expected_list_ids=[3, 2], uuid=node_uuid,
expected_list_ids=[4, 2], uuid=node_uuid,
result_node_type="data",
result_name="inputs")

Expand All @@ -709,7 +728,6 @@ def test_calculation_attributes(self):
node_uuid = self.get_dummy_data()["calculations"][1]["uuid"]
url = self.get_url_prefix() + "/calculations/" + str(
node_uuid) + "/content/attributes"
self.app.config['TESTING'] = True
with self.app.test_client() as client:
rv = client.get(url)
response = json.loads(rv.data)
Expand All @@ -726,7 +744,6 @@ def test_calculation_attributes_nalist_filter(self):
node_uuid = self.get_dummy_data()["calculations"][1]["uuid"]
url = self.get_url_prefix() + '/calculations/' + str(
node_uuid) + '/content/attributes?nalist="attr1"'
self.app.config['TESTING'] = True
with self.app.test_client() as client:
rv = client.get(url)
response = json.loads(rv.data)
Expand All @@ -742,7 +759,6 @@ def test_calculation_attributes_alist_filter(self):
node_uuid = self.get_dummy_data()["calculations"][1]["uuid"]
url = self.get_url_prefix() + '/calculations/' + str(
node_uuid) + '/content/attributes?alist="attr1"'
self.app.config['TESTING'] = True
with self.app.test_client() as client:
rv = client.get(url)
response = json.loads(rv.data)
Expand All @@ -756,10 +772,9 @@ def test_structure_visualization(self):
"""
Get the list of give calculation inputs
"""
node_uuid = self.get_dummy_data()["data"][3]["uuid"]
node_uuid = self.get_dummy_data()["structuredata"][0]["uuid"]
url = self.get_url_prefix() + '/structures/' + str(
node_uuid) + '/content/visualization?visformat=cif'
self.app.config['TESTING'] = True
with self.app.test_client() as client:
rv = client.get(url)
response = json.loads(rv.data)
Expand All @@ -774,3 +789,19 @@ def test_structure_visualization(self):
url,
response, uuid=node_uuid)

def test_cif(self):
"""
Test download of cif file
"""
from aiida.orm import load_node

node_uuid = self.get_dummy_data()["cifdata"][0]["uuid"]
url = self.get_url_prefix() + '/cifs/' + node_uuid + '/content/download'

with self.app.test_client() as client:
rv = client.get(url)

node = load_node(node_uuid)
self.assertEquals(rv.data, node._prepare_cif()[0] )


15 changes: 8 additions & 7 deletions aiida/orm/implementation/general/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,14 +230,15 @@ def query(cls, name=None, type_string="", pk=None, uuid=None, nodes=None,
user=None, node_attributes=None, past_days=None, **kwargs):
"""
Query for groups.
:note: By default, query for user-defined groups only (type_string=="").
:note: By default, query for user-defined groups only (type_string=="")
If you want to query for all type of groups, pass type_string=None.
If you want to query for a specific type of groups, pass a specific
string as the type_string argument.
:param name: the name of the group
:param nodes: a node or list of nodes that belongs to the group (alternatively,
you can also pass a DbNode or list of DbNodes)
:param nodes: a node or list of nodes that belongs to the group
(alternatively, you can also pass a DbNode or list of DbNodes)
:param pk: the pk of the group
:param uuid: the uuid of the group
:param type_string: the string for the type of node; by default, look
Expand All @@ -257,10 +258,10 @@ def query(cls, name=None, type_string="", pk=None, uuid=None, nodes=None,
value=each of the values of the iterable.
:param kwargs: any other filter to be passed to DbGroup.objects.filter
Example: if ``node_attributes = {'elements': ['Ba', 'Ti'],
'md5sum': 'xxx'}``, it will find groups that contain the node
with md5sum = 'xxx', and moreover contain at least one node for
element 'Ba' and one node for element 'Ti'.
Example: ``node_attributes = {'elements': ['Ba', 'Ti'], 'md5sum': 'xxx'}``
will find groups that contain the node with md5sum = 'xxx', and
moreover contain at least one node for element 'Ba' and one node
for element 'Ti'.
"""
pass

Expand Down
4 changes: 4 additions & 0 deletions aiida/orm/implementation/general/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@


class AbstractUser(object):
"""
An AiiDA ORM implementation of a user.
"""

__metaclass__ = ABCMeta

_logger = logging.getLogger(__name__)
Expand Down
36 changes: 27 additions & 9 deletions aiida/restapi/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def __init__(self, app=None, **kwargs):
"""

from aiida.restapi.resources import Calculation, Computer, User, Code, Data, \
Group, Node, StructureData, KpointsData, BandsData, UpfData, ServerInfo
Group, Node, StructureData, KpointsData, BandsData, UpfData, CifData, ServerInfo

self.app = app

Expand Down Expand Up @@ -286,12 +286,36 @@ def __init__(self, app=None, **kwargs):
resource_class_kwargs=kwargs
)


self.add_resource(CifData,
'/cifs/',
'/cifs/schema/',
'/cifs/page/',
'/cifs/page/<int:page>',
'/cifs/<id>/',
'/cifs/<id>/io/inputs/',
'/cifs/<id>/io/inputs/page/',
'/cifs/<id>/io/inputs/page/<int:page>/',
'/cifs/<id>/io/outputs/',
'/cifs/<id>/io/outputs/page/',
'/cifs/<id>/io/outputs/page/<int:page>/',
'/cifs/<id>/io/tree/',
'/cifs/<id>/content/attributes/',
'/cifs/<id>/content/extras/',
'/cifs/<id>/content/visualization/',
'/cifs/<id>/content/download/',
endpoint='cifs',
strict_slashes=False,
resource_class_kwargs=kwargs
)

self.add_resource(User,
'/users/',
'/users/schema/',
'/users/page/',
'/users/page/<int:page>/',
'/users/<id>/',
endpoint='users',
strict_slashes=False,
resource_class_kwargs=kwargs)

Expand All @@ -315,20 +339,14 @@ def handle_error(self, e):
if isinstance(e, HTTPException):
if e.code == 404:

from aiida.restapi.common.config import PREFIX
import json
from aiida.restapi.common.utils import list_routes

response = {}

response["status"] = "404 Not Found"
response["message"] = "The requested URL is not found on the server."
response["available_endpoints"] = []
tmp = []

for rule in sorted(self.app.url_map.iter_rules()):
if rule.endpoint not in tmp and rule.endpoint != "static":
tmp.append(rule.endpoint)
response["available_endpoints"].append(PREFIX + "/" + rule.endpoint)
response["available_endpoints"] = list_routes()

return jsonify(response)

Expand Down
18 changes: 18 additions & 0 deletions aiida/restapi/common/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -869,3 +869,21 @@ def validate_time(s, loc, toks):

## return the translator instructions elaborated from the field_list
return self.build_translator_parameters(field_list)


def list_routes():
"""List available routes"""
import urllib
from flask import current_app, url_for

output = []
for rule in current_app.url_map.iter_rules():
if rule.endpoint is "static":
continue

methods = ','.join(rule.methods)
line = urllib.unquote("{:15s} {:20s} {}".format(rule.endpoint, methods, rule))
output.append(line)

return sorted(set(output))

Loading

0 comments on commit 22ca65e

Please sign in to comment.