apuliasoft/github-issue-labeler-integration-server

View on GitHub
app/api.py

Summary

Maintainability
C
1 day
Test Coverage
#!/usr/bin/python

from functools import wraps  
from flask import Blueprint, current_app, request, jsonify, redirect, session, url_for
from werkzeug import exceptions as exc

from datetime import datetime

from database import db, Models, Trainings, Classifications
from extensions import git, GitError, opnr
import tasks

import hashlib
import hmac

api = Blueprint('api', __name__)


def authorized(f):
  """
  Decorator to ensure that the call is authorized via a valid token
  """
  @wraps(f)
  def decorator(*args, **kwargs):
    try:
      # check for revoked access_token
      git.getUser(session['access_token'])
    except Exception:
      next = request.args.get('next') or ''
      return jsonify({
        'message': "Unauthorized request",
        'next': git.authorizeUrl(url_for('api.auth', _external=True) + next)
      }),401
    
    return f(session['access_token'], *args, **kwargs)  
  return decorator


@api.route('/auth/', defaults={'next':''})
@api.route('/auth/<path:next>')
def auth(next):
  """
  Authorization endpoint
  GitHub app needs this as a callback to return a valid token
  """
  token = git.getAccessToken(request)
  
  if token:
    session['access_token'] = token
    user = git.getUser(token)
    session['user_id'] = user['login']
    return redirect(next or 'https://github.com')
  else: 
    return jsonify({
      'message': "Invalid authorization"
    }),401


@api.route("/")
def index():
  if 'access_token' in session:
    return jsonify({'message': 'User correclty logged on!', 'username': session['user_id']})
  return jsonify({'message': 'User not logged, please login to access api.'})


@api.route('/logout')
def logout():
  """
  Logout endpoint to clear session
  ---
  tags: 
    - management
  responses:
    200:
      description: User is no more authenticated
  """
  session.pop('access_token', None)
  session.pop('user_id', None)
  return redirect(url_for('api.index'))


@api.route('/manage')
def manage():
  """
  Takes user to the app page where he can manage app permissions revocation
  ---
  tags: 
    - management
  responses:
    200:
      description: Redirect to app manamente page
  """
  return redirect(git.appManagementUrl)


@api.route('/install')
def install():
  """
    Redirect user to the app main page where he can install the app or change configurations
    ---
    tags: 
      - management
    responses:
      200:
        description: Redirect to app installation page
  """
  return redirect(git.appPageUrl)


@api.route('/train')
@authorized
def train(token):
  """
    Train a model from a GIT repository using OpenReq API
    ---
    tags:
      - api
    parameters:
      - $ref: "#/parameters/repoParam"
    responses:
      200:
        description: OK
        schema:
          id: messages
      201:
        description: Training already started
      403:
        description: Repository not in the format 'owner/name' or 'organization/name'
      404:
        description: Repository not exists
  """
  repo = request.args['repo']
  try:
    company, property = repo.split("/")
  except ValueError:
    return jsonify({
      'message': "Repository not in the format 'owner/name' or 'organization/name'"
    }), 403
  
  requirements = []
  # check if repo has been trained calling openreq
  # check also if a training istance is in progress (within time limit) but not saved in openreq (using local db)
  model = db.session.query(Models).filter(Models.repo==repo).first()
  if opnr.exists(company, property):
    # model exists remotely but not match local view
    if not model or not model.ready:
      db.session.merge(Models(repo=repo, ready=True))
      db.session.commit()
  else:
    # model not exists but we have locally an attempt (timeout?) to training
    if model and not model.ready and (model.updated - datetime.now()).seconds < current_app.config['TRAINING_TIMEOUT'] :
      return jsonify({
        'message': 'Training in progress from previous call... please wait'
      }), 201
    else:
      # check repository existance before start training async task
      if not git.exists(repo):
        return jsonify({
          'message': 'Repository not exists'
        }), 404
      # set training started in local db
      db.session.merge(Models(repo=repo, ready=False))
      db.session.commit()
      tasks.train.delay(repo)
  
  try:
    # associate model to user
    db.session.merge(Trainings(repo=repo, username=session['user_id']))
    db.session.commit()
  except Exception as e:
    return jsonify({
      'message': e.message
    }), 500
  
  return jsonify({
    'message': "Trained model has been associated to your userbase."
  })



@api.route('/classify')
@authorized
def classify(token):
  """
    Classify a repository using a model trained from another repository
    ---
    tags:
      - api
    parameters:
      - name: repo
        in: query
        type: string
        required: true
        description: Repository to classify in the format 'owner/name' or 'organization/name'
      - name: model
        in: query
        type: string
        required: true
        description: Repository to use as a model in the format 'owner/name' or 'organization/name'
    responses:
      200:
        description: OK
        schema:
          id: messages
      403:
        description: Forbidden
        schema:
          id: messages
      404:
        description: One of the repositories in input not exists
        schema:
          id: messages
  """
  repo = request.args['repo']
  model = request.args['model'] 
  
  if not git.exists(repo):
    return jsonify({
      'message': 'Repository to classify not exists'
    }), 404
        
  try:
    company, property = model.split("/")
  except ValueError:
    return jsonify({
      'message': "Repository model not in the format 'owner/name' or 'organization/name'"
    }), 403
  
  # check repo if user has installed app on and get installation token for successive calls to api
  try:
    token = git.getInstallationAccessToken(repo)
    if not token:
      raise GitError()
  except GitError:
    return jsonify({
      'message': "App not installed on this repository"
    }), 403
  
  # check model if exists
  if not opnr.exists(company, property):
    return jsonify({
      'message': "Train the model before you can use it"
    }), 403
  
  
  # check if repo is already classified with that model
  # XXX check if is good to avoid successive classification on the same model or need check for timeout classification
  classification = db.session.query(Classifications).filter(Classifications.repo == repo).first()
  if not classification or classification.model != model or (classification.started - datetime.now()).seconds >= current_app.config['CLASSIFICATION_TIMEOUT'] :
    db.session.merge(Classifications(repo=repo, model=model, started=datetime.now()))
    db.session.commit()
    tasks.classify.delay(repo, model, token, batch=True)
    
    return jsonify({
      'message': "Classification scheduled successfully."
    })
  else:
    return jsonify({
      'message': "Repository has been classified already or classification is still in progress."
    })


@api.route("/my-models")
@authorized
def my_models(token):
  """
  List trained models associated to current user
  ---
  tags:
    - api
  responses:
    200:
      description: Models list
      schema:
        type: array
        items:
          type: object
          properties:
            name:
              type: string
            ready:
              type: boolean
          description: Repository full name
          
  """
  return jsonify([ 
    { 'name': t.repo, 'ready': t.model.ready } 
    for t in db.session.query(Trainings).filter_by(username = session['user_id']).join(Models)
  ])


@api.route('/check-installed')
@authorized
def check_installed(token):
  """
  Check if github app is installed in a specific repository
  ---
  tags: 
    - api
  parameters:
    - $ref: "#/parameters/repoParam"
  responses:
    200:
      description: OK
      schema:
        type: boolean
    404:
      description: Invalid repository
      schema:
        id: messages
  """
  
  repo = request.args['repo']
  
  if not git.exists(repo, token):
    return jsonify({
      'message': 'Invalid repository'
    }), 404
  
  return jsonify({ 'result': git.isInstalled(repo) })


@api.route("/is-owner")
@authorized
def is_owner(token):
  """
  Return true/false if current user own the repository passed in input
  ---
  tags:
    - api
  parameters:
    - $ref: "#/parameters/repoParam"
  responses:
    200:
      description: OK
      schema:
        type: boolean
    404:
      description: Invalid repository
      schema:
        id: messages
  """
  
  repo = request.args['repo']
  
  if not git.exists(repo, token):
    return jsonify({
      'message': 'Invalid repository'
    }), 404
  
  try:
    return jsonify({ 'result': git.getRepo(repo, token)['permissions']['admin'] })
  except GitError:
    return jsonify({ 'result': False })


@api.route("/webhook", methods = ['GET','POST'])
def webhook(): 
  """
  WebHook endpoint
  No need to call directly
  Needed by Git as a callback endpoint to receive server side events
  ---
  """
  # check git signature in payload 
  signature = hmac.new(bytes(current_app.config['GITHUB_WEBHOOK_SECRET'], 'latin-1'), request.data, hashlib.sha1).hexdigest()
  if hmac.compare_digest(str(signature), request.headers['X-Hub-Signature'].split('=')[1]):
   
    data = request.get_json()
    
    # check for incoming issues (new or changed)
    if 'issue' in data and data['action'] in ['opened', 'edited']:
      issue = data['issue']
      repo = data['repository']['full_name']
      # username = data['sender']['login']
      # check if repo first classification has done
      # retrieve model associated to the repo
      classification = db.session.query(Classifications).filter(Classifications.repo == repo).first()
      if not classification or not classification.classified:
        return jsonify({
          'message': "No model associated to this repository or batch classification still in progress"
        }), 404
      
      tasks.classify.delay(repo, classification.model, None, [issue])
      
      return jsonify({
        'message': "Issue classified."
      })
  
  return jsonify({
    'message': "nothing to do with this by now"
  }), 204 # CHECK may be a 501 is more proper




# test endpoints can be removed when project is ready to production

@api.route("/delete-training")
@authorized
def delete_training(token):
  repo = request.args['repo']

  try:
    # delete association of model to user
    db.session.query(Trainings).filter_by(repo=repo, username=session['user_id']).delete()
    db.session.commit()
  except Exception as e:
    return jsonify({
      'message': e.message
    }), 500
  
  return jsonify({
    'message': "Model has been dissociated from your userbase."
  })
  

@api.route("/limit")
@authorized
def limit(token):
  import requests
  
  limits = {}
  
  r = requests.get('https://api.github.com/rate_limit')
  limits['no_token'] = r.json()
  
  r = requests.get('https://api.github.com/rate_limit', headers={'Authorization': 'token ' + token})
  limits['auth_token'] = r.json()
  
  return jsonify(limits)
  #return jsonify([ x['full_name'] for x in r.json() ])

@api.route("/myrepos")
@authorized
def myrepos(token):
  import requests
  #r = requests.get('https://api.github.com/app/installation/repositories'.format(username = session['user_id']), headers=git.jwtHeader)
  r = requests.get('https://api.github.com/user/repos', headers=git.getAuthHeader(token))
  #return jsonify(r.json())
  return jsonify([ x['full_name'] for x in r.json() ])

@api.route("/exists")
@authorized
def exists(token):
  repo = request.args['repo']
  company, property = repo.split("/")
  return jsonify(opnr.exists(company, property))

@api.route("/issues")
@authorized
def issues(token):
  repo = request.args['repo']
  return jsonify(git.getIssues(repo))
  #return jsonify({'requirements': _issuesToRequirements(git.getIssues(repo))})

@api.route("/login")
@authorized
def login(token):
  return jsonify(git.getUser(token))

@api.route("/access_token")
@authorized
def access_token(token):
  return jsonify(session['access_token'])


@api.route("/test")
@authorized
def test(token):
  repo = request.args['repo']
  return jsonify(git.exists(repo, token))
  return "testato"