Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
575 changes: 0 additions & 575 deletions app/api/routes.py

This file was deleted.

3 changes: 3 additions & 0 deletions app/api/routes/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from app.api.routes import ( # noqa
api_key, categories, languages, resource_creation, resource_modification,
resource_retrieval, search)
60 changes: 60 additions & 0 deletions app/api/routes/api_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from flask import g, request

from app import db, utils as utils
from app.api import bp
from app.api.auth import authenticate, create_new_apikey, is_user_oc_member, rotate_key
from app.api.routes.helpers import (
unauthorized_response, failures_counter, latency_summary, logger)
from app.api.validations import requires_body
from app.models import Key


@latency_summary.time()
@failures_counter.count_exceptions()
@bp.route('/apikey', methods=['POST'], endpoint='apikey')
@requires_body
def apikey():
"""
Verify OC membership and return an API key. The API key will be
saved in the DB to verify use as well as returned upon subsequent calls
to this endpoint with the same OC credentials.
"""
json = request.get_json()
email = json.get('email')
password = json.get('password')
is_oc_member = is_user_oc_member(email, password)

if not is_oc_member:
return unauthorized_response()

try:
# We need to check the database for an existing key
apikey = Key.query.filter_by(email=email).first()

# Don't return success for blacklisted keys
if apikey and apikey.blacklisted:
return unauthorized_response()

if not apikey:
# Since they're already authenticated by is_oc_user(), we know we
# can generate an API key for them if they don't already have one
apikey = create_new_apikey(email, db.session)
if not apikey:
return utils.standardize_response(status_code=500)

logger.info(apikey.serialize)
return utils.standardize_response(payload=dict(data=apikey.serialize))
except Exception as e:
logger.exception(e)
return utils.standardize_response(status_code=500)


@latency_summary.time()
@failures_counter.count_exceptions()
@bp.route('/apikey/rotate', methods=['POST'], endpoint='rotate_apikey')
@authenticate
def rotate_apikey():
new_key = rotate_key(g.auth_key, db.session)
if not new_key:
return utils.standardize_response(status_code=500)
return utils.standardize_response(payload=dict(data=new_key.serialize))
51 changes: 51 additions & 0 deletions app/api/routes/categories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from flask import redirect, request

from app import utils as utils
from app.api import bp
from app.api.routes.helpers import failures_counter, latency_summary, logger
from app.models import Category
from configs import Config


@latency_summary.time()
@failures_counter.count_exceptions()
@bp.route('/categories', methods=['GET'])
def categories():
return get_categories()


@latency_summary.time()
@failures_counter.count_exceptions()
@bp.route('/categories/<int:id>', methods=['GET'], endpoint='get_category')
def category(id):
return get_category(id)


def get_categories():
try:
category_paginator = utils.Paginator(Config.CATEGORY_PAGINATOR, request)
query = Category.query

paginated_categories = category_paginator.paginated_data(query)
if not paginated_categories:
return redirect('/404')
category_list = [
category.serialize for category in paginated_categories.items
]
pagination_details = category_paginator.pagination_details(paginated_categories)
except Exception as e:
logger.exception(e)
return utils.standardize_response(status_code=500)

return utils.standardize_response(payload=dict(
data=category_list,
**pagination_details))


def get_category(id):
category = Category.query.get(id)

if category:
return utils.standardize_response(payload=dict(data=(category.serialize)))

return redirect('/404')
32 changes: 32 additions & 0 deletions app/api/routes/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from prometheus_client import Counter, Summary

from app import utils as utils
from app.models import Category, Language

logger = utils.setup_logger('routes_logger')
latency_summary = Summary('request_latency_seconds', 'Length of request')
failures_counter = Counter('my_failures', 'Number of exceptions raised')


def unauthorized_response():
message = "The email or password you submitted is incorrect " \
"or your account is not allowed api access"
payload = {'errors': {"invalid-credentials": {"message": message}}}
return utils.standardize_response(payload=payload, status_code=401)


def get_attributes(json):
languages_list = Language.query.all()
categories_list = Category.query.all()

language_dict = {l.key(): l for l in languages_list}
category_dict = {c.key(): c for c in categories_list}

langs = []
for lang in json.get('languages') or []:
language = language_dict.get(lang)
if not language:
language = Language(name=lang)
langs.append(language)
categ = category_dict.get(json.get('category'), Category(name=json.get('category')))
return (langs, categ)
51 changes: 51 additions & 0 deletions app/api/routes/languages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from flask import redirect, request

from app import utils as utils
from app.api import bp
from app.api.routes.helpers import failures_counter, latency_summary, logger
from app.models import Language
from configs import Config


@latency_summary.time()
@failures_counter.count_exceptions()
@bp.route('/languages', methods=['GET'])
def languages():
return get_languages()


@latency_summary.time()
@failures_counter.count_exceptions()
@bp.route('/languages/<int:id>', methods=['GET'], endpoint='get_language')
def language(id):
return get_language(id)


def get_languages():
language_paginator = utils.Paginator(Config.LANGUAGE_PAGINATOR, request)
query = Language.query

try:
paginated_languages = language_paginator.paginated_data(query)
if not paginated_languages:
return redirect('/404')
language_list = [
language.serialize for language in paginated_languages.items
]
pagination_details = language_paginator.pagination_details(paginated_languages)
except Exception as e:
logger.exception(e)
return utils.standardize_response(status_code=500)

return utils.standardize_response(payload=dict(
data=language_list,
**pagination_details))


def get_language(id):
language = Language.query.get(id)

if language:
return utils.standardize_response(payload=dict(data=(language.serialize)))

return redirect('/404')
94 changes: 94 additions & 0 deletions app/api/routes/resource_creation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
from os import environ

from algoliasearch.exceptions import AlgoliaException, AlgoliaUnreachableHostException
from flask import request
from sqlalchemy.exc import IntegrityError

from app import db, index, utils as utils
from app.api import bp
from app.api.auth import authenticate
from app.api.routes.helpers import (
failures_counter, get_attributes, latency_summary, logger)
from app.api.validations import requires_body, validate_resource_list, wrong_type
from app.models import Resource


@latency_summary.time()
@failures_counter.count_exceptions()
@bp.route('/resources', methods=['POST'], endpoint='create_resources')
@requires_body
@authenticate
def post_resources():
json = request.get_json()

if not isinstance(json, list):
return wrong_type("list of resources objects", type(json))

validation_errors = validate_resource_list(request.method, json)

if validation_errors:
return utils.standardize_response(payload=validation_errors, status_code=422)

return create_resources(json, db)


def create_resources(json, db):
try:
# Resources to return in the response
created_resources = []
# Serialized Resources to send to Algolia
created_resources_algolia = []
# Resource IDs to delete if Algolia fails to index
resource_id_cache = []

# Create each Resource in the database one by one
for resource in json:
langs, categ = get_attributes(resource)
new_resource = Resource(
name=resource.get('name'),
url=resource.get('url'),
category=categ,
languages=langs,
paid=resource.get('paid'),
notes=resource.get('notes'))

try:
db.session.add(new_resource)
db.session.commit()
created_resources_algolia.append(new_resource.serialize_algolia_search)
resource_id_cache.append(new_resource.id)

except IntegrityError as e:
logger.exception(e)
return utils.standardize_response(status_code=422)

except Exception as e:
logger.exception(e)
return utils.standardize_response(status_code=500)

created_resources.append(new_resource.serialize)

# Take all the created resources and save them in Algolia with one API call
try:
index.save_objects(created_resources_algolia)

except (AlgoliaUnreachableHostException, AlgoliaException) as e:
if (environ.get("FLASK_ENV") != 'development'):
logger.exception(e)

# Remove created resources from the db to stay in sync with Algolia
for res_id in resource_id_cache:
res = Resource.query.get(res_id)
res.languages.clear()
db.session.delete(res)
db.session.commit()

msg = "Algolia failed to index resources"
error = {'errors': [{"algolia-failed": {"message": msg}}]}
return utils.standardize_response(payload=error, status_code=500)

# Success
return utils.standardize_response(payload=dict(data=created_resources))
except Exception as e:
logger.exception(e)
return utils.standardize_response(status_code=500)
Loading