Skip to content

Commit

Permalink
Patch 0.1.3 (#369)
Browse files Browse the repository at this point in the history
* Add auto-deploy to PyPi
* Switch to latest PyYAML (#368)
* Implement read-only, write-only and required properties (#326)
* implement write-only properties
* Add test for required properties implementation
* Add test for write-only properties
* Handle property constraints in resources.py
* Add docstrings to new helper functions
* Change variable name according to new import
* Specify version for PyYAML
* Remove finalization from collection endpoint
* added test for cli.py
  • Loading branch information
Mec-iS authored Mar 25, 2019
1 parent 54f6bee commit 86f24ef
Show file tree
Hide file tree
Showing 10 changed files with 342 additions and 81 deletions.
7 changes: 7 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,10 @@ install:
script: python -m unittest discover
env:
- API_NAME=api PORT=8000
deploy:
provider: pypi
user: tuned
password:
secure: vw9srAqCwa4QO7ITuyApU0Y/AazAfi6wWWXszuqa3/wAKPQ1fJ7T8JY/m3pKg0vMhoNp7hmI3v/nnyJPdTZGRGivhvsRUFC0EEC2c238DfPViAfsxEbHEpO45Ak6B3MR/fyvCmAZmMg3lXiwoP/EF5xvUNLu18Bnp1cdql5cd2iRyp6oTv/5/PWCq1a5phTpa3rP0rQvfqvmYERxgh4BjN9Uge2AFhUJqLvG1mGG/E0bWXGq2yOwI5orwcxFM0n2THsvXVD2+2fsM3oo6pXm74CgQJ7HCpnoJqssyXbc9f4aUnqUz1YK2V8PxE3WGv8jT9/IRwIy8ILIi5PQc9hsADDNxCABbxCsw3gPqJi+Gl99mmqwmo74Q9CypcwCE0paZEM5Ql37McRj91mPdpNB05FF4KzNM44eCIJOJbzTDst7MrvLBZqk2dGDtDMb5A9lO78PnCaJwEoRAeM6ktOtbd0ob/umwRTsoYzrJn4DXd4sbfQIGmnC7VdbCCfORCUHE2HjhRr38/LeC6YFbGnxmwNAIW+lavQC4WLpWwxJc8QQeIWQwuOGowwTcPs712bcdmV1Xaqf+nFA7RrAb/AvxxRlS8DZikbzEFQ5nU5UBd9V7gSN6501aq/xoTaslfuFeTwcwtNXOUdkmtZ5eIZHtyT5a1QjJ+jaDNHnwrh1snY=
on:
tags: true
18 changes: 1 addition & 17 deletions hydrus/data/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@
from sqlalchemy.orm.exc import NoResultFound
from hydrus.data.db_models import (Graph, BaseProperty, RDFClass, Instance,
Terminal, GraphIAC, GraphIIT, GraphIII)
from hydrus.utils import get_doc

from hydrus.data.exceptions import (
ClassNotFound,
InstanceExists,
Expand Down Expand Up @@ -106,21 +104,7 @@ def get(id_: str, type_: str, api_name: str, session: scoped_session,
properties.id == data.predicate).one().name
instance = session.query(Instance).filter(
Instance.id == data.object_).one()
# Get class name for instance object
inst_class_name = session.query(RDFClass).filter(
RDFClass.id == instance.type_).one().name
doc = get_doc()
nested_class_path = ""
for collection in doc.collections:
if doc.collections[collection]["collection"].class_.path == inst_class_name:
nested_class_path = doc.collections[collection]["collection"].path
object_template[prop_name] = "/{}/{}/{}".format(
api_name, nested_class_path, instance.id)
break

if nested_class_path == "":
object_template[prop_name] = "/{}/{}/".format(
api_name, inst_class_name)
object_template[prop_name] = instance.id

for data in data_IIT:
prop_name = session.query(properties).filter(
Expand Down
70 changes: 69 additions & 1 deletion hydrus/helpers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Dict, Any, List, Optional, Union
from typing import Dict, Any, List, Optional, Union, Tuple

from flask import Response

Expand Down Expand Up @@ -103,3 +103,71 @@ def checkClassOp(class_type: str, method: str) -> bool:
if supportedOp.method == method:
return True
return False


def check_required_props(class_type: str, obj: Dict[str, Any]) -> bool:
"""
Check if the object contains all required properties.
:param class_type: class name of the object
:param obj: object under check
:return: True if the object contains all required properties
False otherwise.
"""
for prop in get_doc().parsed_classes[class_type]["class"].supportedProperty:
if prop.required:
if prop.title not in obj:
return False
return True


def check_read_only_props(class_type: str, obj: Dict[str, Any]) -> bool:
"""
Check that the object does not contain any read-only properties.
:param class_type: class name of the object
:param obj: object under check
:return: True if the object doesn't contain any read-only properties
False otherwise.
"""
for prop in get_doc().parsed_classes[class_type]["class"].supportedProperty:
if prop.read:
if prop.title in obj:
return False
return True


def get_nested_class_path(class_type: str) -> Tuple[str, bool]:
"""
Get the path of class
:param class_type: class name whose path is needed
:return: Tuple, where the first element is the path string and
the second element is a boolean, True if the class is a collection class
False otherwise.
"""
for collection in get_doc().collections:
if get_doc().collections[collection]["collection"].class_.title == class_type:
return get_doc().collections[collection]["collection"].path, True

return get_doc().parsed_classes[class_type]["class"].path, False


def finalize_response(class_type: str, obj: Dict[str, Any]) -> Dict[str, Any]:
"""
finalize response objects by removing write-only properties and correcting path
of nested objects.
:param class_type: class name of the object
:param obj: object being finalized
:return: An object not containing any write-only properties and having proper path
of any nested object's url.
"""
for prop in get_doc().parsed_classes[class_type]["class"].supportedProperty:
if prop.write:
obj.pop(prop.title, None)
elif 'vocab:' in prop.prop:
prop_class = prop.prop.replace("vocab:", "")
nested_path, is_collection = get_nested_class_path(prop_class)
if is_collection:
id = obj[prop.title]
obj[prop.title] = "/{}/{}/{}".format(get_api_name(), nested_path, id)
else:
obj[prop.title] = "/{}/{}".format(get_api_name(), nested_path)
return obj
143 changes: 85 additions & 58 deletions hydrus/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@
checkEndpoint,
validObjectList,
type_match,
hydrafy)
hydrafy,
check_read_only_props,
check_required_props,
finalize_response)
from hydrus.utils import get_session, get_doc, get_api_name, get_hydrus_server_url


Expand Down Expand Up @@ -105,7 +108,9 @@ def get(self, id_: str, path: str) -> Response:
api_name=get_api_name(),
session=get_session())

return set_response_headers(jsonify(hydrafy(response, path=path)))
response = finalize_response(class_type, response)
return set_response_headers(
jsonify(hydrafy(response, path=path)))

except (ClassNotFound, InstanceNotFound) as e:
status_code, message = e.get_HTTP()
Expand All @@ -124,31 +129,31 @@ def post(self, id_: str, path: str) -> Response:
return auth_response

class_type = get_doc().collections[path]["collection"].class_.title

if checkClassOp(class_type, "POST"):
object_ = json.loads(request.data.decode('utf-8'))
if checkClassOp(class_type, "POST") and check_read_only_props(class_type, object_):
# Check if class_type supports POST operation
object_ = json.loads(request.data.decode('utf-8'))
obj_type = getType(class_type, "POST")
# Load new object and type
if validObject(object_) and object_["@type"] == obj_type:
try:
# Update the right ID if the object is valid and matches
# type of Item
object_id = crud.update(
object_=object_,
id_=id_,
type_=object_["@type"],
session=get_session(),
api_name=get_api_name())
headers_ = [{"Location": "{}{}/{}/{}".format(
get_hydrus_server_url(), get_api_name(), path, object_id)}]
response = {
"message": "Object with ID {} successfully updated".format(object_id)}
return set_response_headers(jsonify(response), headers=headers_)

except (ClassNotFound, InstanceNotFound, InstanceExists, PropertyNotFound) as e:
status_code, message = e.get_HTTP()
return set_response_headers(jsonify(message), status_code=status_code)
if validObject(object_) and object_["@type"] == obj_type and check_required_props(
class_type, object_):
try:
# Update the right ID if the object is valid and matches
# type of Item
object_id = crud.update(
object_=object_,
id_=id_,
type_=object_["@type"],
session=get_session(),
api_name=get_api_name())
headers_ = [{"Location": "{}{}/{}/{}".format(
get_hydrus_server_url(), get_api_name(), path, object_id)}]
response = {
"message": "Object with ID {} successfully updated".format(object_id)}
return set_response_headers(jsonify(response), headers=headers_)

except (ClassNotFound, InstanceNotFound, InstanceExists, PropertyNotFound) as e:
status_code, message = e.get_HTTP()
return set_response_headers(jsonify(message), status_code=status_code)
else:
return set_response_headers(jsonify({400: "Data is not valid"}), status_code=400)
else:
Expand All @@ -171,19 +176,20 @@ def put(self, id_: str, path: str) -> Response:
object_ = json.loads(request.data.decode('utf-8'))
obj_type = getType(class_type, "PUT")
# Load new object and type
if validObject(object_) and object_["@type"] == obj_type:
try:
# Add the object with given ID
object_id = crud.insert(object_=object_, id_=id_, session=get_session())
headers_ = [{"Location": "{}{}/{}/{}".format(
get_hydrus_server_url(), get_api_name(), path, object_id)}]
response = {
"message": "Object with ID {} successfully added".format(object_id)}
return set_response_headers(
jsonify(response), headers=headers_, status_code=201)
except (ClassNotFound, InstanceExists, PropertyNotFound) as e:
status_code, message = e.get_HTTP()
return set_response_headers(jsonify(message), status_code=status_code)
if validObject(object_) and object_["@type"] == obj_type and check_required_props(
class_type, object_):
try:
# Add the object with given ID
object_id = crud.insert(object_=object_, id_=id_, session=get_session())
headers_ = [{"Location": "{}{}/{}/{}".format(
get_hydrus_server_url(), get_api_name(), path, object_id)}]
response = {
"message": "Object with ID {} successfully added".format(object_id)}
return set_response_headers(
jsonify(response), headers=headers_, status_code=201)
except (ClassNotFound, InstanceExists, PropertyNotFound) as e:
status_code, message = e.get_HTTP()
return set_response_headers(jsonify(message), status_code=status_code)
else:
return set_response_headers(jsonify({400: "Data is not valid"}), status_code=400)
else:
Expand Down Expand Up @@ -251,6 +257,7 @@ def get(self, path: str) -> Response:
api_name=get_api_name(),
session=get_session(),
path=path)
response = finalize_response(class_type, response)
return set_response_headers(jsonify(hydrafy(response, path=path)))

except (ClassNotFound, InstanceNotFound) as e:
Expand Down Expand Up @@ -280,7 +287,8 @@ def put(self, path: str) -> Response:
# title of HydraClass object corresponding to collection
obj_type = collection.class_.title

if validObject(object_) and object_["@type"] == obj_type:
if validObject(object_) and object_["@type"] == obj_type and check_required_props(
obj_type, object_):
# If Item in request's JSON is a valid object ie. @type is a key in object_
# and the right Item type is being added to the collection
try:
Expand All @@ -305,7 +313,8 @@ def put(self, path: str) -> Response:
).collections:
# If path is in parsed_classes but is not a collection
obj_type = getType(path, "PUT")
if object_["@type"] == obj_type and validObject(object_):
if object_["@type"] == obj_type and validObject(object_) and check_required_props(
obj_type, object_):
try:
object_id = crud.insert(object_=object_, session=get_session())
headers_ = [{"Location": "{}{}/{}/".format(
Expand Down Expand Up @@ -340,26 +349,33 @@ def post(self, path: str) -> Response:
if path in get_doc().parsed_classes and "{}Collection".format(path) not in get_doc(
).collections:
obj_type = getType(path, "POST")
if validObject(object_) and object_["@type"] == obj_type:
try:
crud.update_single(
object_=object_,
session=get_session(),
api_name=get_api_name(),
path=path)
headers_ = [{"Location": "{}{}/{}/".format(
get_hydrus_server_url(), get_api_name(), path)}]
response = {"message": "Object successfully updated"}
return set_response_headers(jsonify(response), headers=headers_)
except (ClassNotFound, InstanceNotFound,
InstanceExists, PropertyNotFound) as e:
status_code, message = e.get_HTTP()
return set_response_headers(
jsonify(message), status_code=status_code)
if check_read_only_props(obj_type, object_):
if object_["@type"] == obj_type and check_required_props(
obj_type, object_) and validObject(object_):
try:
crud.update_single(
object_=object_,
session=get_session(),
api_name=get_api_name(),
path=path)

headers_ = [
{"Location": "{}/{}/".format(
get_hydrus_server_url(), get_api_name(), path)}]
response = {
"message": "Object successfully updated"}
return set_response_headers(
jsonify(response), headers=headers_)
except (ClassNotFound, InstanceNotFound,
InstanceExists, PropertyNotFound) as e:
status_code, message = e.get_HTTP()
return set_response_headers(
jsonify(message), status_code=status_code)

else:
return set_response_headers(
jsonify({400: "Data is not valid"}), status_code=400)
else:
abort(405)

abort(endpoint_['status'])

Expand Down Expand Up @@ -414,6 +430,11 @@ def put(self, path, int_list="") -> Response:
collection = get_doc().collections[path]["collection"]
# title of HydraClass object corresponding to collection
obj_type = collection.class_.title
incomplete_objects = list()
for obj in object_:
if not check_required_props(obj_type, obj):
incomplete_objects.append(obj)
object_.remove(obj)
if validObjectList(object_):
type_result = type_match(object_, obj_type)
# If Item in request's JSON is a valid object
Expand All @@ -429,8 +450,14 @@ def put(self, path, int_list="") -> Response:
get_hydrus_server_url(), get_api_name(), path, object_id)}]
response = {
"message": "Object with ID {} successfully added".format(object_id)}
return set_response_headers(
jsonify(response), headers=headers_, status_code=201)
if len(incomplete_objects):
response = {"message": "Object(s) missing required property",
"objects": incomplete_objects}
return set_response_headers(
jsonify(response), headers=headers_, status_code=202)
else:
return set_response_headers(
jsonify(response), headers=headers_, status_code=201)
except (ClassNotFound, InstanceExists, PropertyNotFound) as e:
status_code, message = e.get_HTTP()
return set_response_headers(jsonify(message), status_code=status_code)
Expand Down
6 changes: 3 additions & 3 deletions hydrus/samples/doc_writer_sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
prop1_title = "Prop1" # Title of the property

dummyProp1 = HydraClassProp(prop1_uri, prop1_title,
required=False, read=False, write=True)
required=True, read=False, write=True)


prop2_uri = "http://props.hydrus.com/prop2"
Expand Down Expand Up @@ -122,9 +122,9 @@
class_2.add_supported_prop(dummyProp1)
class_2.add_supported_prop(dummyProp2)
class_2.add_supported_prop(HydraClassProp(
"vocab:dummyClass", "dummyProp", required=False, read=False, write=True))
"vocab:dummyClass", "dummyProp", required=False, read=False, write=False))
class_2.add_supported_prop(HydraClassProp(
"vocab:anotherSingleClass", "singleClassProp", required=False, read=False, write=True))
"vocab:anotherSingleClass", "singleClassProp", required=False, read=False, write=False))
class_1.add_supported_prop(dummyProp1)
# Add the operations to the classes
class_.add_supported_op(op1)
Expand Down
Loading

0 comments on commit 86f24ef

Please sign in to comment.