def get_context(category: str) -> Response: """ Generate the context for a given category. :param category: The category of class for which context is required :type category: str :return: Response with context :rtype: Response """ collections, parsed_classes = get_collections_and_parsed_classes() # Check for collection if category in get_doc().collections: # type: Union[Dict[str,Any],Dict[int,str]] response = {"@context": collections[category]["context"].generate()} return set_response_headers(jsonify(response)) # Check for non collection class elif category in parsed_classes: response = { "@context": get_doc().parsed_classes[category]["context"].generate() } return set_response_headers(jsonify(response)) else: error = HydraError(code=404, title="NOT FOUND", desc="Context not found") return error_response(error)
def get_path_from_type(type_: str) -> str: _, parsed_classes = get_collections_and_parsed_classes() expanded_base_url = DocUrl.doc_url for class_name in parsed_classes: class_ = parsed_classes[class_name]['class'] if type_ == class_.id_.split(expanded_base_url)[1]: return class_.path
def items_put_check_support(id_, class_path, path, is_collection): """Check if class_type supports PUT operation""" object_ = json.loads(request.data.decode('utf-8')) collections, parsed_classes = get_collections_and_parsed_classes() if path in parsed_classes: class_path = path obj_type = getType(path, "PUT") elif path in collections: collection = collections[path]["collection"] class_path = collection.path obj_type = collection.name link_props, link_type_check = get_link_props(class_path, object_) # Load new object and type if (validate_object(object_, obj_type, class_path) and link_type_check): if is_collection: object_ = parse_collection_members(object_) try: # Add the object with given ID object_id = crud.insert(object_=object_, id_=id_, session=get_session(), collection=is_collection) headers_ = [{"Location": f"{get_hydrus_server_url()}" f"{get_api_name()}/{path}/{object_id}"}] status_description = f"Object with ID {object_id} successfully added" status = HydraStatus(code=201, title="Object successfully added.", desc=status_description) return set_response_headers( jsonify(status.generate()), headers=headers_, status_code=status.code) except (ClassNotFound, InstanceExists, PropertyNotFound) as e: error = e.get_HTTP() return error_response(error) else: error = HydraError(code=400, title="Data is not valid") return error_response(error)
def items_put_response(path: str, int_list="") -> Response: """ Handles PUT operation to insert multiple items. :param path: Path for Item Collection :type path: str :param int_list: Optional String containing ',' separated ID's :type int_list: List :return: Appropriate response for the PUT operation on multiple items. :rtype: Response """ object_ = json.loads(request.data.decode('utf-8')) object_ = object_["data"] _, parsed_classes = get_collections_and_parsed_classes() if path in parsed_classes: class_path = path obj_type = getType(path, "PUT") incomplete_objects = [] for obj in object_: if not check_required_props(class_path, obj): incomplete_objects.append(obj) object_.remove(obj) link_props_list, link_type_check = get_link_props_for_multiple_objects(class_path, object_) if validObjectList(object_) and link_type_check: type_result = type_match(object_, obj_type) # If Item in request's JSON is a valid object # ie. @type is one of the keys in object_ if type_result: # If the right Item type is being added to the # collection try: # Insert object and return location in Header object_id = crud.insert_multiple( objects_=object_, session=get_session(), id_=int_list) headers_ = [{"Location": f"{get_hydrus_server_url()}" f"{get_api_name()}/{path}/{object_id}"}] if len(incomplete_objects) > 0: status = HydraStatus(code=202, title="Object(s) missing required property") response = status.generate() response["objects"] = incomplete_objects return set_response_headers( jsonify(response), headers=headers_, status_code=status.code) else: status_description = f"Objects with ID {object_id} successfully added" status = HydraStatus(code=201, title="Objects successfully added", desc=status_description) return set_response_headers( jsonify(status.generate()), headers=headers_, status_code=status.code) except (ClassNotFound, InstanceExists, PropertyNotFound) as e: error = e.get_HTTP() return error_response(error) error = HydraError(code=400, title="Data is not valid") return error_response(error)
def finalize_response(path: str, obj: Dict[str, Any]) -> Dict[str, Any]: """ finalize response objects by removing properties which are not readable and correcting path of nested objects. :param path: Path of the collection or non-collection class. :param obj: object being finalized :return: An object not containing any `readable=False` properties and having proper path of any nested object's url. """ collections, parsed_classes = get_collections_and_parsed_classes() expanded_base_url = DocUrl.doc_url if path in collections: members = list() for member in obj["members"]: member_id = member[0] member_type = member[1] member_path = get_path_from_type(member_type) member = { "@type": "hydra:Link", "@id": f"{get_host_domain()}/{get_api_name()}/{member_path}/{member_id}", } members.append(member) obj['members'] = members return obj else: # path is of a non-collection class supported_properties = get_doc( ).parsed_classes[path]["class"].supportedProperty expanded_base_url = DocUrl.doc_url for prop in supported_properties: # Skip not required properties which are not inserted yet. if not prop.required and prop.title not in obj: continue # if prop.read is False: # obj.pop(prop.title, None) elif isinstance(prop.prop, HydraLink): hydra_link = prop.prop range_class = hydra_link.range.split(expanded_base_url)[1] nested_path, is_collection = get_nested_class_path(range_class) if is_collection: id = obj[prop.title] obj[prop.title] = f"/{get_api_name()}/{nested_path}/{id}" else: obj[prop.title] = f"/{get_api_name()}/{nested_path}" elif expanded_base_url in prop.prop: prop_class = prop.prop.split(expanded_base_url)[1] prop_class_path = parsed_classes[prop_class]['class'].path id = obj[prop.title] class_resp = crud.get(id, prop_class, get_api_name(), get_session(), path=prop_class_path) obj[prop.title] = finalize_response(prop_class_path, class_resp) return obj
def items_post_check_support(id_, object_, class_path, path, is_collection): """Check if class_type supports POST operation""" collections, parsed_classes = get_collections_and_parsed_classes() if path in parsed_classes: class_path = path obj_type = getType(path, "PUT") elif path in collections: collection = collections[path]["collection"] class_path = collection.path obj_type = collection.name link_props, link_type_check = get_link_props(class_path, object_) # Load new object and type if (validate_object(object_, obj_type, class_path) and link_type_check): if is_collection: object_ = parse_collection_members(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(), collection=is_collection) method = "POST" resource_url = f"{get_hydrus_server_url()}{get_api_name()}/{path}/{object_id}" last_job_id = crud.get_last_modification_job_id( session=get_session()) new_job_id = crud.insert_modification_record(method, resource_url, session=get_session()) send_sync_update(socketio=socketio, new_job_id=new_job_id, last_job_id=last_job_id, method=method, resource_url=resource_url) headers_ = [{"Location": resource_url}] status_description = (f"Object with ID {object_id} successfully " "updated") status = HydraStatus(code=200, title="Object updated", desc=status_description) return set_response_headers(jsonify(status.generate()), headers=headers_) except (ClassNotFound, InstanceNotFound, InstanceExists, PropertyNotFound) as e: error = e.get_HTTP() return error_response(error) else: error = HydraError(code=400, title="Data is not valid") return error_response(error)
def item_collection_get_response(path: str) -> Response: """ Handles GET operation on item collection classes. :param path: Path for Item Collection :type path: str :return: Appropriate response for the GET operation. :rtype: Response """ search_params = request.args.to_dict() collections, parsed_classes = get_collections_and_parsed_classes() api_name = get_api_name() expanded_base_url = DocUrl.doc_url # If endpoint and GET method is supported in the API and class is supported if path in parsed_classes: abort(405) if path in collections: collection = collections[path]["collection"] class_name = collection.manages["object"].split(expanded_base_url)[1] collection_manages_class = parsed_classes[class_name]["class"] class_type = collection_manages_class.title class_path = collection_manages_class.path try: # Get collection details from the database # create partial function for crud operation crud_response = partial(crud.get_collection, api_name, class_type, session=get_session(), path=path, search_params=search_params, collection=False) if get_pagination(): # Get paginated response response = crud_response(paginate=True, page_size=get_page_size()) else: # Get whole collection response = crud_response(paginate=False) response["search"] = add_iri_template(path=class_path, API_NAME=api_name, collection_path=path) return set_response_headers(jsonify(hydrafy(response, path=path))) except (ClassNotFound, PageNotFound, InvalidSearchParameter, OffsetOutOfRange) as e: error = e.get_HTTP() return error_response(error)
def item_collection_put_response(path: str) -> Response: """ Handles PUT operation on item collection classes. :param path: Path for Item Collection :type path: str :return: Appropriate response for the PUT operation. :rtype: Response """ object_ = json.loads(request.data.decode('utf-8')) collections, parsed_classes = get_collections_and_parsed_classes() is_collection = False if path in parsed_classes: class_path = path is_collection = False obj_type = getType(path, "PUT") elif path in collections: collection = collections[path]["collection"] class_path = collection.path obj_type = collection.name is_collection = True if validate_object(object_, obj_type, class_path): # 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 if is_collection: object_ = parse_collection_members(object_) try: # Insert object and return location in Header object_id = crud.insert(object_=object_, session=get_session(), collection=is_collection) headers_ = [{ "Location": f"{get_hydrus_server_url()}{get_api_name()}/{path}/{object_id}" }] status_description = f"Object with ID {object_id} successfully added" status = HydraStatus(code=201, title="Object successfully added", desc=status_description) return set_response_headers(jsonify(status.generate()), headers=headers_, status_code=status.code) except (ClassNotFound, InstanceExists, PropertyNotFound, PropertyNotGiven) as e: error = e.get_HTTP() return error_response(error) else: error = HydraError(code=400, title="Data is not valid") return error_response(error)
def checkClassOp(path: str, method: str) -> bool: """ Check if the Class supports the operation. :param path: Path of the collection or non-collection class. :param method: Method name. :return: True if the method is defined, false otherwise. """ collections, parsed_classes = get_collections_and_parsed_classes() if path in collections: supported_operations = get_doc( ).collections[path]["collection"].supportedOperation else: supported_operations = get_doc( ).parsed_classes[path]["class"].supportedOperation for supportedOp in supported_operations: if supportedOp.method == method: return True return False
def get_link_props(path: str, object_) -> Tuple[Dict[str, Any], bool]: """ Get dict of all hydra_link properties of a class. :param path: Path of the collection or non-collection class. :param object_: Object being inserted/updated. :return: Tuple with one elements as Dict with property_title as key and instance_id(for collection class) or class_name(for non-collection class) as value, second element represents boolean representing validity of the link. """ link_props = {} collections, parsed_classes = get_collections_and_parsed_classes() expanded_base_url = DocUrl.doc_url if path in collections: # path is of a collection class supported_properties = get_doc( ).collections[path]["collection"].supportedProperty else: # path is of a non-collection class supported_properties = get_doc( ).parsed_classes[path]["class"].supportedProperty for supportedProp in supported_properties: if isinstance(supportedProp.prop, HydraLink) and supportedProp.title in object_: prop_range = supportedProp.prop.range range_class_name = prop_range.split(expanded_base_url)[1] for collection_path in get_doc().collections: if collection_path in object_[supportedProp.title]: class_title = get_doc( ).collections[collection_path]['collection'].class_.title if range_class_name != class_title: return {}, False link_props[supportedProp.title] = object_[ supportedProp.title].split('/')[-1] break if supportedProp.title not in link_props: for class_path in get_doc().parsed_classes: if class_path in object_[supportedProp.title]: class_title = get_doc( ).parsed_classes[class_path]['class'].title if range_class_name != class_title: return {}, False link_props[supportedProp.title] = class_title break return link_props, True
def items_delete_response(path: str, int_list="") -> Response: """ Handles DELETE operation to insert multiple items. :param path: Path for Item Collection :type path: str :param int_list: Optional String containing ',' separated ID's :type int_list: List :return: Appropriate response for the DELETE operation on multiple items. :rtype: Response """ _, parsed_classes = get_collections_and_parsed_classes() if path in parsed_classes: class_type = getType(path, "DELETE") if checkClassOp(class_type, "DELETE"): # Check if class_type supports PUT operation try: # Delete the Item with ID == id_ crud.delete_multiple(int_list, class_type, session=get_session()) method = "DELETE" path_url = f"{get_hydrus_server_url()}{get_api_name()}/{path}" last_job_id = crud.get_last_modification_job_id(session=get_session()) id_list = int_list.split(',') for item in id_list: resource_url = path_url + item new_job_id = crud.insert_modification_record(method, resource_url, session=get_session()) send_sync_update(socketio=socketio, new_job_id=new_job_id, last_job_id=last_job_id, method=method, resource_url=resource_url) last_job_id = new_job_id status_description = f"Objects with ID {id_list} successfully deleted" status = HydraStatus(code=200, title="Objects successfully deleted", desc=status_description) return set_response_headers(jsonify(status.generate())) except (ClassNotFound, InstanceNotFound) as e: error = e.get_HTTP() return error_response(error) abort(405)
def put(self, id_: str, path: str) -> Response: """ Add new object_ optional <id_> parameter using HTTP PUT. :param id_ - ID of Item to be updated :param path - Path for Item type( Specified in APIDoc @id) to be updated """ id_ = str(id_) collections, parsed_classes = get_collections_and_parsed_classes() is_collection = False if path in parsed_classes: class_path = path if path in collections: item_class = collections[path]["collection"] class_path = item_class.path is_collection = True if checkClassOp(class_path, "PUT"): return items_put_check_support(id_, class_path, path, is_collection) abort(405)
def post(self, id_: str, path: str) -> Response: """ Update object of type<path> at ID<id_> with new object_ using HTTP POST. :param id_ - ID of Item to be updated :param path - Path for Item type( Specified in APIDoc @id) """ id_ = str(id_) collections, parsed_classes = get_collections_and_parsed_classes() is_collection = False if path in parsed_classes: class_path = path if path in collections: item_class = collections[path]["collection"] class_path = item_class.path is_collection = True object_ = json.loads(request.data.decode('utf-8')) if checkClassOp(class_path, "POST") and check_writeable_props( class_path, object_): return items_post_check_support(id_, object_, class_path, path, is_collection) abort(405)
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. """ expanded_base_url = DocUrl.doc_url collections, parsed_classes = get_collections_and_parsed_classes() for path in collections: collection = collections[path]["collection"] class_name = collection.manages["object"].split(expanded_base_url)[1] collection_manages_class = parsed_classes[class_name]["class"] collection_manages_class_type = collection_manages_class.title collection_manages_class_path = collection_manages_class.path if collection_manages_class_type == class_type: return collection_manages_class_path, True for class_path in parsed_classes: if class_type == parsed_classes[class_path]["class"].title: return class_path, False
def check_required_props(path: str, obj: Dict[str, Any]) -> bool: """ Check if the object contains all required properties. :param path: Path of the collection or non-collection class. :param obj: object under check :return: True if the object contains all required properties False otherwise. """ collections, parsed_classes = get_collections_and_parsed_classes() if path in collections: # path is of a collection class supported_properties = get_doc( ).collections[path]["collection"].supportedProperty else: # path is of a non-collection class supported_properties = get_doc( ).parsed_classes[path]["class"].supportedProperty for prop in supported_properties: if prop.required: if prop.title not in obj: return False return True
def delete(self, id_: str, path: str) -> Response: """ Delete object with id=id_ from database. :param id_ - ID of Item to be deleted :param path - Path for Item type( Specified in APIDoc @id) to be deleted """ id_ = str(id_) collections, parsed_classes = get_collections_and_parsed_classes() is_collection = False if path in parsed_classes: class_path = path class_type = parsed_classes[path]['class'].title if path in collections: item_class = collections[path]["collection"] class_type = item_class.name # Get path of the collection-class class_path = item_class.path is_collection = True if checkClassOp(class_path, "DELETE"): return items_delete_check_support(id_, class_type, path, is_collection) abort(405)
def check_writeable_props(path: str, obj: Dict[str, Any]) -> bool: """ Check that the object only contains writeable fields(properties). :param path: Path of the collection or non-collection class. :param obj: object under check :return: True if the object only contains writeable properties False otherwise. """ collections, parsed_classes = get_collections_and_parsed_classes() if path in collections: # path is of a collection class supported_properties = get_doc( ).collections[path]["collection"].supportedProperty else: # path is of a non-collection class supported_properties = get_doc( ).parsed_classes[path]["class"].supportedProperty for prop in supported_properties: if prop.write is False: if prop.title in obj: return False return True
def get(self, id_: str, path: str) -> Response: """ GET object with id = id_ from the database. :param id_ : Item ID :param path : Path for Item ( Specified in APIDoc @id) :return : object with id=id_ """ id_ = str(id_) collections, parsed_classes = get_collections_and_parsed_classes() is_collection = False if path in parsed_classes: class_path = path class_type = parsed_classes[path]['class'].title if path in collections: item_class = collections[path]["collection"] class_type = item_class.name # Get path of the collection-class class_path = item_class.path is_collection = True if checkClassOp(class_path, "GET"): return items_get_check_support(id_, class_type, class_path, path, is_collection) abort(405)