Skip to content

Commit 4a1aea9

Browse files
authored
Added support for datetime column in datebase created by hydrus (#599)
* changes in helpers * function to modify object * datetime support * changes in tests * changes in sample doc * mapping for xsd:string * added docstring and annotations * added missing type annotation * iso datetime format * added exception for invalid date format * fix get_modified_object
1 parent 9723693 commit 4a1aea9

11 files changed

Lines changed: 138 additions & 37 deletions

hydrus/data/crud.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949

5050
# from sqlalchemy.orm.session import Session
5151
from sqlalchemy.orm.scoping import scoped_session
52+
from hydra_python_core.doc_writer import HydraDoc
5253
from typing import Dict, Optional, Any, List
5354

5455
from hydrus.data.resource_based_classes import (
@@ -97,12 +98,14 @@ def get(
9798

9899

99100
def insert(
101+
doc_: HydraDoc,
100102
object_: Dict[str, Any],
101103
session: scoped_session,
102104
id_: Optional[str] = None,
103105
collection: bool = False,
104106
) -> str:
105107
"""Insert an object to database [POST] and returns the inserted object.
108+
:param doc_ : Hydra Doc object
106109
:param object_: object to be inserted
107110
:param session: sqlalchemy scoped session
108111
:param id_: id of the object to be inserted (optional param)
@@ -124,15 +127,19 @@ def insert(
124127
object_template = copy.deepcopy(object_)
125128
if id_ is not None:
126129
object_template["id"] = id_
127-
inserted_object_id = insert_object(object_template, session, collection)
130+
inserted_object_id = insert_object(doc_, object_template, session, collection)
128131
return inserted_object_id
129132

130133

131134
def insert_multiple(
132-
objects_: List[Dict[str, Any]], session: scoped_session, id_: Optional[str] = ""
135+
doc: HydraDoc,
136+
objects_: List[Dict[str, Any]],
137+
session: scoped_session,
138+
id_: Optional[str] = ""
133139
) -> List[str]:
134140
"""
135141
Adds a list of object with given ids to the database
142+
:param doc : Hydra Doc object
136143
:param objects_: List of dict's to be added to the database
137144
:param session: scoped session from getSession in utils
138145
:param id_: optional parameter containing the ids of objects that have to be inserted
@@ -167,7 +174,7 @@ def insert_multiple(
167174
pass
168175
except TypeError:
169176
pass
170-
inserted_object_id = insert(object_, session, id_of_object_)
177+
inserted_object_id = insert(doc, object_, session, id_of_object_)
171178
instance_id_list.append(inserted_object_id)
172179

173180
return instance_id_list
@@ -209,6 +216,7 @@ def delete_multiple(id_: List[int], type_: str, session: scoped_session) -> None
209216

210217

211218
def update(
219+
doc_: HydraDoc,
212220
id_: str,
213221
type_: str,
214222
object_: Dict[str, str],
@@ -218,6 +226,7 @@ def update(
218226
collection: bool = False,
219227
) -> str:
220228
"""Update an object properties based on the given object [PUT].
229+
:param doc_ : Hydra Doc object
221230
:param id_: if of object to be updated
222231
:param type_: type of object to be updated
223232
:param object_: object that has to be inserted
@@ -228,7 +237,7 @@ def update(
228237
:return: id of updated object
229238
"""
230239
query_info = {"@type": type_, "id_": id_}
231-
updated_object_id = update_object(object_, query_info, session, collection)
240+
updated_object_id = update_object(doc_, object_, query_info, session, collection)
232241
return updated_object_id
233242

234243

hydrus/data/db_models.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,11 @@ def get_attr_dict(self):
101101
foreign_table_name, title
102102
)
103103
else:
104-
datatype_keys = {'integer': Integer, 'float': Float, 'decimal': Float}
104+
datatype_keys = {'integer': Integer,
105+
'float': Float,
106+
'decimal': Float,
107+
'string': String,
108+
'dateTime': DateTime}
105109
if 'range' in supported_property:
106110
datatype = supported_property['range'].split('#')[1]
107111
if datatype in datatype_keys:

hydrus/data/exceptions.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,3 +195,17 @@ def get_HTTP(self) -> HydraError:
195195
"""Return the HTTP response for the Exception."""
196196
description = f"The property {self.type_} is not given."
197197
return HydraError(code=400, title="Property not given", desc=description)
198+
199+
200+
class InvalidDateTimeFormat(Exception):
201+
"""Error when a datetime field input is invalid"""
202+
203+
def __init__(self, field_: str) -> None:
204+
"""Constructor."""
205+
self.field_ = field_
206+
207+
def get_HTTP(self) -> HydraError:
208+
"""Return the HTTP response for the Exception."""
209+
description = (f"The format of {self.field_} is invalid."
210+
f" Datetime input should be in ISO format.")
211+
return HydraError(code=400, title="Invalid Datetime format", desc=description)

hydrus/data/helpers/item_collection_helpers.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
InvalidSearchParameter,
1616
OffsetOutOfRange,
1717
PropertyNotGiven,
18+
InvalidDateTimeFormat
1819
)
1920

2021
from hydrus.data.helpers import (
@@ -34,6 +35,7 @@
3435
get_hydrus_server_url,
3536
get_page_size,
3637
get_pagination,
38+
get_doc
3739
)
3840

3941

@@ -99,6 +101,7 @@ def item_collection_put_response(path: str) -> Response:
99101
:rtype: Response
100102
"""
101103
object_ = json.loads(request.data.decode("utf-8"))
104+
doc_object = get_doc()
102105
collections, parsed_classes = get_collections_and_parsed_classes()
103106
is_collection = False
104107
if path in parsed_classes:
@@ -118,7 +121,7 @@ def item_collection_put_response(path: str) -> Response:
118121
try:
119122
# Insert object and return location in Header
120123
object_id = crud.insert(
121-
object_=object_, session=get_session(), collection=is_collection
124+
object_=object_, session=get_session(), doc_=doc_object, collection=is_collection
122125
)
123126
resource_url = (
124127
f"{get_hydrus_server_url()}{get_api_name()}/{path}/{object_id}"
@@ -133,7 +136,8 @@ def item_collection_put_response(path: str) -> Response:
133136
return set_response_headers(
134137
jsonify(status_response), headers=headers_, status_code=status.code
135138
)
136-
except (ClassNotFound, InstanceExists, PropertyNotFound, PropertyNotGiven) as e:
139+
except (ClassNotFound, InstanceExists, PropertyNotFound,
140+
PropertyNotGiven, InvalidDateTimeFormat) as e:
137141
error = e.get_HTTP()
138142
return error_response(error)
139143
else:

hydrus/data/helpers/itemhelpers.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
ClassNotFound,
1010
InstanceNotFound,
1111
InstanceExists,
12+
InvalidDateTimeFormat,
1213
PropertyNotFound,
1314
MemberInstanceNotFound,
1415
)
@@ -24,7 +25,7 @@
2425
parse_collection_members,
2526
get_collections_and_parsed_classes,
2627
)
27-
from hydrus.utils import get_session, get_api_name, get_hydrus_server_url
28+
from hydrus.utils import get_session, get_api_name, get_hydrus_server_url, get_doc
2829
from hydrus.extensions.socketio_factory import socketio
2930

3031

@@ -52,6 +53,7 @@ def items_get_check_support(id_, class_type, class_path, path, is_collection=Fal
5253
def items_post_check_support(id_, object_, class_path, path, is_collection):
5354
"""Check if class_type supports POST operation"""
5455
collections, parsed_classes = get_collections_and_parsed_classes()
56+
doc = get_doc()
5557
if path in parsed_classes:
5658
class_path = path
5759
obj_type = getType(path, "PUT")
@@ -68,6 +70,7 @@ def items_post_check_support(id_, object_, class_path, path, is_collection):
6870
# Update the right ID if the object is valid and matches
6971
# type of Item
7072
object_id = crud.update(
73+
doc,
7174
object_=object_,
7275
id_=id_,
7376
type_=object_["@type"],
@@ -99,7 +102,8 @@ def items_post_check_support(id_, object_, class_path, path, is_collection):
99102
status_response["iri"] = resource_url
100103
return set_response_headers(jsonify(status_response), headers=headers_)
101104

102-
except (ClassNotFound, InstanceNotFound, InstanceExists, PropertyNotFound) as e:
105+
except (ClassNotFound, InstanceNotFound, InstanceExists,
106+
PropertyNotFound, InvalidDateTimeFormat) as e:
103107
error = e.get_HTTP()
104108
return error_response(error)
105109
else:
@@ -110,6 +114,7 @@ def items_post_check_support(id_, object_, class_path, path, is_collection):
110114
def items_put_check_support(id_, class_path, path, is_collection):
111115
"""Check if class_type supports PUT operation"""
112116
object_ = json.loads(request.data.decode("utf-8"))
117+
doc = get_doc()
113118
collections, parsed_classes = get_collections_and_parsed_classes()
114119
if path in parsed_classes:
115120
class_path = path
@@ -126,6 +131,7 @@ def items_put_check_support(id_, class_path, path, is_collection):
126131
try:
127132
# Add the object with given ID
128133
object_id = crud.insert(
134+
doc,
129135
object_=object_,
130136
id_=id_,
131137
session=get_session(),
@@ -144,7 +150,8 @@ def items_put_check_support(id_, class_path, path, is_collection):
144150
return set_response_headers(
145151
jsonify(status_response), headers=headers_, status_code=status.code
146152
)
147-
except (ClassNotFound, InstanceExists, PropertyNotFound) as e:
153+
except (ClassNotFound, InstanceExists,
154+
PropertyNotFound, InvalidDateTimeFormat) as e:
148155
error = e.get_HTTP()
149156
return error_response(error)
150157
else:

hydrus/data/helpers/items_helpers.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
InstanceExists,
99
PropertyNotFound,
1010
InstanceNotFound,
11+
InvalidDateTimeFormat
1112
)
1213
from hydrus.data.helpers import (
1314
checkClassOp,
@@ -26,6 +27,7 @@
2627
get_api_name,
2728
get_hydrus_server_url,
2829
set_response_headers,
30+
get_doc
2931
)
3032
from hydrus.extensions.socketio_factory import socketio
3133

@@ -42,6 +44,7 @@ def items_put_response(path: str, int_list="") -> Response:
4244
:rtype: Response
4345
"""
4446
object_ = json.loads(request.data.decode("utf-8"))
47+
doc = get_doc()
4548
object_ = object_["data"]
4649
_, parsed_classes = get_collections_and_parsed_classes()
4750
if path in parsed_classes:
@@ -65,7 +68,7 @@ def items_put_response(path: str, int_list="") -> Response:
6568
try:
6669
# Insert object and return location in Header
6770
object_id = crud.insert_multiple(
68-
objects_=object_, session=get_session(), id_=int_list
71+
doc, objects_=object_, session=get_session(), id_=int_list
6972
)
7073
headers_ = [
7174
{
@@ -100,7 +103,8 @@ def items_put_response(path: str, int_list="") -> Response:
100103
headers=headers_,
101104
status_code=status.code,
102105
)
103-
except (ClassNotFound, InstanceExists, PropertyNotFound) as e:
106+
except (ClassNotFound, InstanceExists,
107+
PropertyNotFound, InvalidDateTimeFormat) as e:
104108
error = e.get_HTTP()
105109
return error_response(error)
106110

hydrus/data/resource_based_classes.py

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"""
55
import copy
66
import uuid
7+
from dateutil import parser
78
from hydrus.data.db_models import Resource
89
from hydrus.data.exceptions import (
910
ClassNotFound,
@@ -13,11 +14,13 @@
1314
PropertyNotFound,
1415
PropertyNotGiven,
1516
MemberInstanceNotFound,
17+
InvalidDateTimeFormat
1618
)
1719
from sqlalchemy import exists
1820
from sqlalchemy.exc import InvalidRequestError, IntegrityError
1921
from sqlalchemy.orm.scoping import scoped_session
2022
from sqlalchemy.orm.exc import NoResultFound
23+
from hydra_python_core.doc_writer import HydraDoc
2124
from typing import Dict, Any
2225

2326

@@ -34,6 +37,39 @@ def get_type(object_: Dict[str, Any]) -> str:
3437
return type_
3538

3639

40+
def get_modified_object(object_: Dict[str, Any], doc: HydraDoc, path: str) -> Dict[str, Any]:
41+
"""
42+
Return the object after modifying properties.
43+
:param object_: Dict containing object properties
44+
:param doc: HydraDoc object
45+
:param path: Path of the Class or Collection
46+
:return: object_ containing properties
47+
"""
48+
class_ = doc.parsed_classes[path]
49+
properties = class_["class"].supportedProperty
50+
datetimefields = []
51+
for prop in properties:
52+
kwargs = getattr(prop, 'kwargs', None)
53+
range = kwargs.get('range')
54+
title = getattr(prop, 'title')
55+
if range is not None:
56+
if "dateTime" in range:
57+
datetimefield = title
58+
datetimefields.append(datetimefield)
59+
if len(datetimefields) != 0:
60+
for field in datetimefields:
61+
try:
62+
datetime_value = object_.get(field)
63+
dt_object = parser.isoparse(datetime_value)
64+
object_[field] = dt_object
65+
except ValueError:
66+
raise InvalidDateTimeFormat(field)
67+
except TypeError:
68+
datetime_value = object_.get(field)
69+
object_[field] = datetime_value
70+
return object_
71+
72+
3773
def get_database_class(type_: str):
3874
"""
3975
Get the sqlalchemy class object from given classname
@@ -47,10 +83,11 @@ def get_database_class(type_: str):
4783

4884

4985
def insert_object(
50-
object_: Dict[str, Any], session: scoped_session, collection: bool = False
86+
doc_: HydraDoc, object_: Dict[str, Any], session: scoped_session, collection: bool = False
5187
) -> str:
5288
"""
5389
Insert the object in the database
90+
:param doc_: HydraDoc object
5491
:param object_: Dict containing object properties
5592
:param session: sqlalchemy session
5693
:return: The ID of the inserted object
@@ -94,16 +131,20 @@ def insert_object(
94131
fk_column = fk.info["column_name"]
95132
try:
96133
fk_object = object_[fk_column]
134+
fk_object_path = fk_object.get('@type')
135+
fk_object = get_modified_object(fk_object, doc_, fk_object_path)
97136
except KeyError as e:
98137
wrong_property = e.args[0]
99138
raise PropertyNotGiven(type_=wrong_property)
100139
# insert the foreign key object
101-
fk_object_id = insert_object(fk_object, session)
140+
fk_object_id = insert_object(doc_, fk_object, session)
102141
# put the id of the foreign instance in this table's column
103142
object_[fk_column] = fk_object_id
104143
try:
105144
# remove the @type from object before using the object to make a
106145
# instance of it using sqlalchemy class
146+
object_path = object_.get('@type')
147+
object_ = get_modified_object(object_, doc_, object_path)
107148
object_.pop("@type")
108149
inserted_object = database_class(**object_)
109150
except TypeError as e:
@@ -198,13 +239,15 @@ def delete_object(
198239

199240

200241
def update_object(
242+
doc_: HydraDoc,
201243
object_: Dict[str, Any],
202244
query_info: Dict[str, str],
203245
session: scoped_session,
204246
collection: bool = False,
205247
) -> str:
206248
"""
207249
Update the object from the database
250+
:param doc_: HydraDoc object
208251
:param object_: Dict containing updated object properties
209252
:param query_info: Dict containing the id and @type of object that has to retrieved
210253
:param session: sqlalchemy session
@@ -218,11 +261,11 @@ def update_object(
218261
# Try inserting new object
219262
try:
220263
object_["id"] = id_
221-
d = insert_object(object_, session, collection)
264+
d = insert_object(doc_, object_, session, collection)
222265
except Exception as e:
223266
# Put old object back
224267
old_object["id"] = id_
225-
d = insert_object(old_object, session, collection)
268+
d = insert_object(doc_, old_object, session, collection)
226269
raise e
227270
return id_
228271

0 commit comments

Comments
 (0)