Source code for estuary.api.v1

# SPDX-License-Identifier: GPL-3.0+

from __future__ import unicode_literals

from flask import Blueprint, current_app, jsonify, request
from werkzeug.exceptions import NotFound

import estuary.utils.story
from estuary import log, version
from estuary.error import ValidationError
from estuary.models.base import EstuaryStructuredNode
from estuary.utils.general import (get_neo4j_node, inflate_node,
                                   login_required, str_to_bool)
from estuary.utils.recents import get_recent_nodes

api_v1 = Blueprint('api_v1', __name__)


[docs]@api_v1.route('/about') def about(): """ Display general information about the app. :rtype: flask.Response """ return jsonify({ 'auth_required': current_app.config['ENABLE_AUTH'], 'version': version })
[docs]@api_v1.route('/<resource>/<uid>') @login_required def get_resource(resource, uid): """ Get a resource from Neo4j. :param str resource: a resource name that maps to a neomodel class :param str uid: the value of the UniqueIdProperty to query with :return: a Flask JSON response :rtype: flask.Response :raises NotFound: if the item is not found :raises ValidationError: if an invalid resource was requested """ # Default the relationship flag to True relationship = True if request.args.get('relationship'): relationship = str_to_bool(request.args['relationship']) item = get_neo4j_node(resource, uid) if not item: raise NotFound('This item does not exist') if relationship: return jsonify(item.serialized_all) else: return jsonify(item.serialized)
[docs]@api_v1.route('/story/<resource>/<uid>') @login_required def get_resource_story(resource, uid): """ Get the story of a resource from Neo4j. :param str resource: a resource name that maps to a neomodel class :param str uid: the value of the UniqueIdProperty to query with :return: a Flask JSON response :rtype: flask.Response :raises NotFound: if the item is not found :raises ValidationError: if an invalid resource was requested """ fallback_resources = request.args.getlist('fallback') # Try all resources input by the user for _resource in [resource] + fallback_resources: item = get_neo4j_node(_resource, uid) # If a resource is found, we don't need to try the other resources if item: break if not item: raise NotFound('This item does not exist') story_manager = estuary.utils.story.BaseStoryManager.get_story_manager( item, current_app.config, limit=True) def _get_partial_story(results, reverse=False): if not results: return [] # Assuming that if Path is the first result, then that's all we want to process results = [list(results[0][0].nodes)] # Reverse will be true when it is a backward query to preserve the story order if reverse: results = [results[0][::-1]] return EstuaryStructuredNode.inflate_results(results)[0] results = [] if story_manager.forward_story: results = story_manager.set_story_labels(item.__label__, _get_partial_story( story_manager.forward_story)) if story_manager.backward_story: backward_query_results = story_manager.set_story_labels( item.__label__, _get_partial_story( story_manager.backward_story, reverse=True), reverse=True) if backward_query_results and results: # Remove the first element of backward_query_results in order to avoid # duplication of the requested resource when result of forward query are not None. backward_query_results = backward_query_results[:-1] results = backward_query_results + results # Adding the artifact itself if it's story is not available if not results: base_instance = estuary.utils.story.BaseStoryManager() wait_times, total_wait_time = base_instance.get_wait_times([item]) rv = {'data': [item.serialized_all], 'meta': {}} rv['meta']['story_related_nodes_forward'] = [0] rv['meta']['story_related_nodes_backward'] = [0] rv['meta']['requested_node_index'] = 0 rv['meta']['story_type'] = story_manager.__class__.__name__[:-12].lower() rv['meta']['wait_times'] = wait_times rv['meta']['total_wait_time'] = total_wait_time rv['meta']['total_processing_time'] = None rv['meta']['processing_time_flag'] = False rv['meta']['total_lead_time'] = 0 try: total_processing_time, flag = base_instance.get_total_processing_time([item]) rv['meta']['total_processing_time'] = total_processing_time rv['meta']['processing_time_flag'] = flag except: # noqa E722 log.exception('Failed to compute total processing time.') rv['data'][0]['resource_type'] = item.__label__ rv['data'][0]['display_name'] = item.display_name rv['data'][0]['timeline_timestamp'] = item.timeline_timestamp return jsonify(rv) return jsonify(story_manager.format_story_results(results, item))
[docs]@api_v1.route('/allstories/<resource>/<uid>') @login_required def get_resource_all_stories(resource, uid): """ Get all unique stories of an artifact from Neo4j. :param str resource: a resource name that maps to a neomodel class :param str uid: the value of the UniqueIdProperty to query with :return: a Flask JSON response :rtype: flask.Response :raises NotFound: if the item is not found :raises ValidationError: if an invalid resource was requested """ fallback_resources = request.args.getlist('fallback') # Try all resources input by the user for _resource in [resource] + fallback_resources: item = get_neo4j_node(_resource, uid) # If a resource is found, we don't need to try the other resources if item: break story_manager = estuary.utils.story.BaseStoryManager.get_story_manager(item, current_app.config) def _get_partial_stories(results, reverse=False): results_list = [] if not results: return results_list # Creating a list of lists where each list is a collection of node IDs # of the nodes present in that particular story path. # Paths are re-sorted in ascending order to simplify the logic below path_nodes_id = [] for path in reversed(results): path_nodes_id.append([node.id for node in path[0].nodes]) unique_paths = [] for index, node_set in enumerate(path_nodes_id[:-1]): unique = True for alternate_set in path_nodes_id[index + 1:]: # If the node_set is a subset of alternate_set, # we know they are the same path except the alternate_set is longer. # If alternate_set and node_set only have one node ID of difference, # we know it's the same path but from the perspective of different siblings. if set(node_set).issubset(set(alternate_set)) or len( set(alternate_set).difference(set(node_set))) == 1: unique = False break if unique: # Since results is from longest to shortest, we need to get the opposite index. unique_paths.append(results[(len(path_nodes_id) - index) - 1][0]) # While traversing, the outer for loop only goes until the second to last element # because the inner for loop always starts one element ahead of the outer for loop. # Hence, all the subsets of the last element will not be added to the unique_paths # list as the for loops will eliminate them. So we add the last element # since we are sure it is unique. unique_paths.append(results[0][0]) if reverse: unique_paths_nodes = [path.nodes[::-1] for path in unique_paths] else: unique_paths_nodes = [path.nodes for path in unique_paths] return EstuaryStructuredNode.inflate_results(unique_paths_nodes) if story_manager.forward_story: results_forward = _get_partial_stories(story_manager.forward_story) else: results_forward = [] if story_manager.backward_story: results_backward = _get_partial_stories(story_manager.backward_story, reverse=True) else: results_backward = [] all_results = [] if not results_backward or not results_forward: if results_forward: results_unidir = [story_manager.set_story_labels( item.__label__, result) for result in results_forward] else: results_unidir = [story_manager.set_story_labels( item.__label__, result, reverse=True) for result in results_backward] for result in results_unidir: all_results.append(story_manager.format_story_results(result, item)) else: # Combining all the backward and forward paths to generate all the possible full paths for result_forward in results_forward: for result_backward in results_backward: results = story_manager.set_story_labels( item.__label__, result_backward, reverse=True) + \ story_manager.set_story_labels(item.__label__, result_forward)[1:] all_results.append(story_manager.format_story_results(results, item)) # Adding the artifact itself if its story is not available if not all_results: base_instance = estuary.utils.story.BaseStoryManager() wait_times, total_wait_time = base_instance.get_wait_times([item]) rv = {'data': [item.serialized_all], 'meta': {}} rv['meta']['story_related_nodes_forward'] = [0] rv['meta']['story_related_nodes_backward'] = [0] rv['meta']['requested_node_index'] = 0 rv['meta']['story_type'] = story_manager.__class__.__name__[:-12].lower() rv['meta']['wait_times'] = wait_times rv['meta']['total_wait_time'] = total_wait_time rv['meta']['total_processing_time'] = None rv['meta']['processing_time_flag'] = False rv['meta']['total_lead_time'] = 0 try: total_processing_time, flag = base_instance.get_total_processing_time([item]) rv['meta']['total_processing_time'] = total_processing_time rv['meta']['processing_time_flag'] = flag except: # noqa E722 log.exception('Failed to compute total processing time.') rv['data'][0]['resource_type'] = item.__label__ rv['data'][0]['display_name'] = item.display_name rv['data'][0]['timeline_timestamp'] = item.timeline_timestamp all_results.append(rv) return jsonify(all_results)
[docs]@api_v1.route('/siblings/<resource>/<uid>') @login_required def get_siblings(resource, uid): """ Get siblings of next/previous node that are correlated to the node in question. :param str resource: a resource name that maps to a neomodel class :param str uid: the value of the UniqueIdProperty to query with :return: a Flask JSON response :rtype: flask.Response :raises NotFound: if the item is not found :raises ValidationError: if an invalid resource was requested """ story_type_mapper = {'container': 'ContainerStoryManager', 'module': 'ModuleStoryManager'} story_type = request.args.get('story_type', 'container').lower() story_manager_class = story_type_mapper.get(story_type) if story_manager_class: story_manager = getattr(estuary.utils.story, story_manager_class)() else: raise ValidationError('Supplied story type is invalid. Select from: {0}' .format(', '.join(current_app.config['STORY_MANAGER_SEQUENCE']))) # This is the node that is part of a story which has the relationship to the desired # sibling nodes story_node = get_neo4j_node(resource, uid) if not story_node: raise NotFound('This item does not exist') story_node_story_flow = story_manager.story_flow(story_node.__label__) # If backward_rel is true, we fetch siblings of the previous node, next node otherwise. # For example, if you pass in an advisory and want to know the Freshmaker events triggered from # that advisory, backward_rel would be false. If you want to know the Koji builds attached to # that advisory, then backward_rel would true. backward = str_to_bool(request.args.get('backward_rel')) if backward and story_node_story_flow['backward_label']: desired_siblings_label = story_node_story_flow['backward_label'] elif not backward and story_node_story_flow['forward_label']: desired_siblings_label = story_node_story_flow['forward_label'] else: raise ValidationError('Siblings cannot be determined on this kind of resource') sibling_nodes = story_manager.get_sibling_nodes(desired_siblings_label, story_node) # Inflating and formatting results from Neo4j serialized_results = [] for result in sibling_nodes: inflated_node = inflate_node(result[0]) serialized_node = inflated_node.serialized_all serialized_node['resource_type'] = inflated_node.__label__ serialized_node['display_name'] = inflated_node.display_name serialized_results.append(serialized_node) description = story_manager.get_siblings_description( story_node.display_name, story_node_story_flow, backward) result = { 'data': serialized_results, 'meta': { 'description': description } } return jsonify(result)
[docs]@api_v1.route('/relationships/<resource>/<uid>/<relationship>') @login_required def get_artifact_relationships(resource, uid, relationship): """ Get one-to-many relationships of a particular artifact. :param str resource: a resource name that maps to a neomodel class :param str uid: the value of the UniqueIdProperty to query with :param str relationship: relationship to expand :return: a Flask JSON response :rtype: flask.Response :raises NotFound: if the item is not found :raises ValidationError: if an invalid resource/relationship was requested """ item = get_neo4j_node(resource, uid) if not item: raise NotFound('This item does not exist') if relationship not in [rel[0] for rel in item.__all_relationships__]: raise ValidationError( 'Please provide a valid relationship name for {0} with uid {1}'.format(resource, uid)) rel_display_name = relationship.replace('_', ' ') results = { 'data': [], 'meta': { 'description': '{0} of {1}'.format(rel_display_name, item.display_name) } } related_nodes = getattr(item, relationship).match() for node in related_nodes: serialized_node = node.serialized_all serialized_node['resource_type'] = node.__label__ serialized_node['display_name'] = node.display_name results['data'].append(serialized_node) return jsonify(results)
[docs]@api_v1.route('/recents') @login_required def get_recent_stories(): """Get stories that were most recently updated, by their artifact type.""" nodes, meta = get_recent_nodes() result = { 'data': nodes, 'metadata': meta } return jsonify(result)