Skip to content

Commit

Permalink
🐛 FIX: REST API date-time query (#4959)
Browse files Browse the repository at this point in the history
This was excepting because the `DatetimePrecision` class did not have an assigned hashing function.
Here we move this class to `aiida.common`, to avoid cyclic import,
and add the hash function. 

Co-authored-by: Chris Sewell <chrisj_sewell@hotmail.com>
  • Loading branch information
NinadBhat and chrisjsewell authored Jul 28, 2021
1 parent 6606a5b commit 91c1c0b
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 26 deletions.
11 changes: 10 additions & 1 deletion aiida/common/hashing.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
from functools import singledispatch
from itertools import chain
from operator import itemgetter

import pytz

from aiida.common.utils import DatetimePrecision
from aiida.common.constants import AIIDA_FLOAT_PRECISION
from aiida.common.exceptions import HashingError

Expand Down Expand Up @@ -279,6 +279,15 @@ def _(val, **kwargs):
return [_single_digest('uuid', val.bytes)]


@_make_hash.register(DatetimePrecision)
def _(datetime_precision, **kwargs):
""" Hashes for DatetimePrecision object
"""
return [_single_digest('dt_prec')] + list(
chain.from_iterable(_make_hash(i, **kwargs) for i in [datetime_precision.dtobj, datetime_precision.precision])
) + [_END_DIGEST]


@_make_hash.register(Folder)
def _(folder, **kwargs):
"""
Expand Down
26 changes: 25 additions & 1 deletion aiida/common/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import re
import sys
from uuid import UUID

from datetime import datetime
from .lang import classproperty


Expand Down Expand Up @@ -595,3 +595,27 @@ def result(self, raise_error=Exception):
def raise_errors(self, raise_cls):
if not self.success():
raise raise_cls(f'The following errors were encountered: {self.errors}')


class DatetimePrecision:
"""
A simple class which stores a datetime object with its precision. No
internal check is done (cause itis not possible).
precision: 1 (only full date)
2 (date plus hour)
3 (date + hour + minute)
4 (dare + hour + minute +second)
"""

def __init__(self, dtobj, precision):
""" Constructor to check valid datetime object and precision """

if not isinstance(dtobj, datetime):
raise TypeError('dtobj argument has to be a datetime object')

if not isinstance(precision, int):
raise TypeError('precision argument has to be an integer')

self.dtobj = dtobj
self.precision = precision
25 changes: 1 addition & 24 deletions aiida/restapi/common/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from flask.json import JSONEncoder
from wrapt import decorator

from aiida.common.utils import DatetimePrecision
from aiida.common.exceptions import InputValidationError, ValidationError
from aiida.manage.manager import get_manager
from aiida.restapi.common.exceptions import RestValidationError, \
Expand Down Expand Up @@ -62,30 +63,6 @@ def default(self, o):
return JSONEncoder.default(self, o)


class DatetimePrecision:
"""
A simple class which stores a datetime object with its precision. No
internal check is done (cause itis not possible).
precision: 1 (only full date)
2 (date plus hour)
3 (date + hour + minute)
4 (dare + hour + minute +second)
"""

def __init__(self, dtobj, precision):
""" Constructor to check valid datetime object and precision """

if not isinstance(dtobj, datetime):
raise TypeError('dtobj argument has to be a datetime object')

if not isinstance(precision, int):
raise TypeError('precision argument has to be an integer')

self.dtobj = dtobj
self.precision = precision


class Utils:
"""
A class that gathers all the utility functions for parsing URI,
Expand Down
7 changes: 7 additions & 0 deletions tests/common/test_hashing.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
except ImportError:
import unittest

from aiida.common.utils import DatetimePrecision
from aiida.common.exceptions import HashingError
from aiida.common.hashing import make_hash, float_to_text, chunked_file_hash
from aiida.common.folders import SandboxFolder
Expand Down Expand Up @@ -169,6 +170,12 @@ def test_datetime(self):
'be7c7c7faaff07d796db4cbef4d3d07ed29fdfd4a38c9aded00a4c2da2b89b9c'
)

def test_datetime_precision_hashing(self):
dt_prec = DatetimePrecision(datetime(2018, 8, 18, 8, 18), 10)
self.assertEqual(make_hash(dt_prec), '837ab70b3b7bd04c1718834a0394a2230d81242c442e4aa088abeab15622df37')
dt_prec_utc = DatetimePrecision(datetime.utcfromtimestamp(0), 0)
self.assertEqual(make_hash(dt_prec_utc), '8c756ee99eaf9655bb00166839b9d40aa44eac97684b28f6e3c07d4331ae644e')

def test_numpy_types(self):
self.assertEqual(
make_hash(np.float64(3.141)), 'b3302aad550413e14fe44d5ead10b3aeda9884055fca77f9368c48517916d4be'
Expand Down
28 changes: 28 additions & 0 deletions tests/restapi/test_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
# pylint: disable=too-many-lines
"""Unittests for REST API."""
import io
from datetime import date

from flask_cors.core import ACL_ORIGIN

Expand Down Expand Up @@ -979,6 +980,33 @@ def test_nodes_full_type_filter(self):
for node in response['data']['nodes']:
self.assertIn(node['uuid'], expected_node_uuids)

def test_nodes_time_filters(self):
"""
Get the list of node filtered by time
"""
today = date.today().strftime('%Y-%m-%d')

expected_node_uuids = []
data = self.get_dummy_data()
for calc in data['calculations']:
expected_node_uuids.append(calc['uuid'])

# ctime filter test
url = f"{self.get_url_prefix()}/nodes/?ctime={today}&full_type=\"process.calculation.calcjob.CalcJobNode.|\""
with self.app.test_client() as client:
rv_obj = client.get(url)
response = json.loads(rv_obj.data)
for node in response['data']['nodes']:
self.assertIn(node['uuid'], expected_node_uuids)

# mtime filter test
url = f"{self.get_url_prefix()}/nodes/?mtime={today}&full_type=\"process.calculation.calcjob.CalcJobNode.|\""
with self.app.test_client() as client:
rv_obj = client.get(url)
response = json.loads(rv_obj.data)
for node in response['data']['nodes']:
self.assertIn(node['uuid'], expected_node_uuids)

############### Structure visualization and download #############
def test_structure_derived_properties(self):
"""
Expand Down

0 comments on commit 91c1c0b

Please sign in to comment.