diff --git a/.travis.yml b/.travis.yml index 67fd431e..362435fa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/hydrus/data/crud.py b/hydrus/data/crud.py index 7b4da539..b3fb27e8 100644 --- a/hydrus/data/crud.py +++ b/hydrus/data/crud.py @@ -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, @@ -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( diff --git a/hydrus/helpers.py b/hydrus/helpers.py index 2e582c76..d07d0e04 100644 --- a/hydrus/helpers.py +++ b/hydrus/helpers.py @@ -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 @@ -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 diff --git a/hydrus/resources.py b/hydrus/resources.py index e1b44414..6986c5d4 100644 --- a/hydrus/resources.py +++ b/hydrus/resources.py @@ -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 @@ -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() @@ -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: @@ -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: @@ -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: @@ -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: @@ -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( @@ -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']) @@ -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 @@ -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) diff --git a/hydrus/samples/doc_writer_sample.py b/hydrus/samples/doc_writer_sample.py index 4cf62fcc..d43dbc8c 100644 --- a/hydrus/samples/doc_writer_sample.py +++ b/hydrus/samples/doc_writer_sample.py @@ -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" @@ -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) diff --git a/hydrus/tests/test_app.py b/hydrus/tests/test_app.py index 4f422986..b96d80fb 100644 --- a/hydrus/tests/test_app.py +++ b/hydrus/tests/test_app.py @@ -392,7 +392,7 @@ def test_GET_for_nested_class(self): print(endpoints[class_name]) print(response_get_data) for prop_name in class_props: - if "vocab:" in prop_name.prop: + if "vocab:" in prop_name.prop and not prop_name.write: nested_obj_resp = self.client.get( response_get_data[prop_name.title]) assert nested_obj_resp.status_code == 200 @@ -400,6 +400,56 @@ def test_GET_for_nested_class(self): nested_obj_resp.data.decode('utf-8')) assert "@type" in nested_obj + def test_required_props(self): + index = self.client.get("/{}".format(self.API_NAME)) + assert index.status_code == 200 + endpoints = json.loads(index.data.decode('utf-8')) + for endpoint in endpoints: + if endpoint not in ["@context", "@id", "@type"]: + class_name = "/".join(endpoints[endpoint].split( + "/{}/".format(self.API_NAME))[1:]) + if class_name not in self.doc.collections: + class_ = self.doc.parsed_classes[class_name]["class"] + class_methods = [ + x.method for x in class_.supportedOperation] + if "PUT" in class_methods: + dummy_object = gen_dummy_object(class_.title, self.doc) + required_prop = "" + for prop in class_.supportedProperty: + if prop.required: + required_prop = prop.title + break + if required_prop: + del dummy_object[required_prop] + put_response = self.client.put( + endpoints[class_name], data=json.dumps(dummy_object)) + assert put_response.status_code == 400 + + def test_write_only_props(self): + index = self.client.get("/{}".format(self.API_NAME)) + assert index.status_code == 200 + endpoints = json.loads(index.data.decode('utf-8')) + for endpoint in endpoints: + if endpoint not in ["@context", "@id", "@type"]: + class_name = "/".join(endpoints[endpoint].split( + "/{}/".format(self.API_NAME))[1:]) + if class_name not in self.doc.collections: + class_ = self.doc.parsed_classes[class_name]["class"] + class_methods = [ + x.method for x in class_.supportedOperation] + if "GET" in class_methods: + write_only_prop = "" + for prop in class_.supportedProperty: + if prop.write: + write_only_prop = prop.title + break + if write_only_prop: + get_response = self.client.get( + endpoints[class_name]) + get_response_data = json.loads( + get_response.data.decode('utf-8')) + assert write_only_prop not in get_response_data + def test_bad_objects(self): """Checks if bad objects are added or not.""" index = self.client.get("/{}".format(self.API_NAME)) diff --git a/hydrus/tests/test_crud.py b/hydrus/tests/test_crud.py index 3c89fbd2..ffe8b5db 100644 --- a/hydrus/tests/test_crud.py +++ b/hydrus/tests/test_crud.py @@ -66,6 +66,7 @@ def test_insert(self): object_ = gen_dummy_object(random.choice( self.doc_collection_classes), self.doc) response = crud.insert(object_=object_, id_="1", session=self.session) + assert isinstance(response, str) def test_get(self): diff --git a/requirements.txt b/requirements.txt index bb96a27c..68f854bb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,6 +22,6 @@ six==1.11.0 SQLAlchemy==1.2.10 thespian==3.9.2 Werkzeug==0.14.1 -PyYAML==4.2b1 +PyYAML==5.1 git+https://github.com/HTTP-APIs/hydra-python-core@v0.1#egg=hydra_python_core git+https://github.com/HTTP-APIs/hydra-openapi-parser@0.1.1#egg=hydra_openapi_parser diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 00000000..a8474ded --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,124 @@ +"""Test for checking Cli""" +import unittest +import random +import string +import json +import re +import uuid +from hydrus.app_factory import app_factory +from hydrus.utils import set_session, set_doc, set_api_name +from hydrus.data import doc_parse, crud +from hydra_python_core import doc_maker +from hydrus.samples import doc_writer_sample +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, scoped_session +from hydrus.data.db_models import Base +import click +from click.testing import CliRunner +from cli import startserver + + +def gen_dummy_object(class_, doc): + """Create a dummy object based on the definitions in the API Doc.""" + object_ = { + "@type": class_ + } + if class_ in doc.parsed_classes: + for prop in doc.parsed_classes[class_]["class"].supportedProperty: + if "vocab:" in prop.prop: + prop_class = prop.prop.replace("vocab:", "") + object_[prop.title] = gen_dummy_object(prop_class, doc) + else: + object_[prop.title] = ''.join(random.choice( + string.ascii_uppercase + string.digits) for _ in range(6)) + return object_ + + +class CliTests(unittest.TestCase): + """Test Class for the Cli.""" + + @classmethod + def setUpClass(self): + """Database setup before the tests.""" + print("Creating a temporary database...") + engine = create_engine('sqlite:///:memory:') + Base.metadata.create_all(engine) + session = scoped_session(sessionmaker(bind=engine)) + + self.session = session + self.API_NAME = "demoapi" + self.HYDRUS_SERVER_URL = "http://hydrus.com/" + + self.app = app_factory(self.API_NAME) + print("going for create doc") + + self.doc = doc_maker.create_doc( + doc_writer_sample.api_doc.generate(), + self.HYDRUS_SERVER_URL, + self.API_NAME) + test_classes = doc_parse.get_classes(self.doc.generate()) + test_properties = doc_parse.get_all_properties(test_classes) + doc_parse.insert_classes(test_classes, self.session) + doc_parse.insert_properties(test_properties, self.session) + + print("Classes and properties added successfully.") + + print("Setting up Hydrus utilities... ") + self.api_name_util = set_api_name(self.app, self.API_NAME) + self.session_util = set_session(self.app, self.session) + self.doc_util = set_doc(self.app, self.doc) + self.client = self.app.test_client() + + print("Creating utilities context... ") + self.api_name_util.__enter__() + self.session_util.__enter__() + self.doc_util.__enter__() + self.client.__enter__() + + print("Setup done, running tests...") + + @classmethod + def tearDownClass(self): + """Tear down temporary database and exit utilities""" + self.client.__exit__(None, None, None) + self.doc_util.__exit__(None, None, None) + self.session_util.__exit__(None, None, None) + self.api_name_util.__exit__(None, None, None) + self.session.close() + + def setUp(self): + for class_ in self.doc.parsed_classes: + if class_ not in self.doc.collections: + dummy_obj = gen_dummy_object(class_, self.doc) + crud.insert( + dummy_obj, + id_=str( + uuid.uuid4()), + session=self.session) + + def test_startserver(self): + runner = CliRunner() + + #starting ther server + + result = runner.invoke(startserver, + ["--adduser","--api","--no-auth","--dburl", + "--hydradoc","--port","--no-token","--serverurl", + "serve"]) + result.exit_code !=0 + + #starting server with invalid params + + result = runner.invoke(startserver, + ["--adduser","sqlite://not-valid","http://localhost", + "--port","serve"]) + assert result.exit_code == 2 + + + + +if __name__ == '__main__': + message = """ + Running tests for the cli + """ + unittest.main()