From ebf89891157359124bb2ab17b100f069bff2508e Mon Sep 17 00:00:00 2001 From: Joel Gerber Date: Wed, 25 Dec 2019 14:05:58 -0500 Subject: [PATCH 1/9] Transition routes module into package --- app/api/{routes.py => routes/__init__.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/api/{routes.py => routes/__init__.py} (100%) diff --git a/app/api/routes.py b/app/api/routes/__init__.py similarity index 100% rename from app/api/routes.py rename to app/api/routes/__init__.py From 42dbbb62c1366909fe8bef87413dec352e3f3417 Mon Sep 17 00:00:00 2001 From: Joel Gerber Date: Wed, 25 Dec 2019 14:27:22 -0500 Subject: [PATCH 2/9] Extract /apikey/... to separate module I had to extract some shared stuff into other modules in order to support doing this. --- app/api/routes/__init__.py | 84 +++++--------------------------------- app/api/routes/api_key.py | 61 +++++++++++++++++++++++++++ app/api/routes/helpers.py | 8 ++++ app/api/routes/logger.py | 3 ++ app/api/routes/metrics.py | 4 ++ 5 files changed, 86 insertions(+), 74 deletions(-) create mode 100644 app/api/routes/api_key.py create mode 100644 app/api/routes/helpers.py create mode 100644 app/api/routes/logger.py create mode 100644 app/api/routes/metrics.py diff --git a/app/api/routes/__init__.py b/app/api/routes/__init__.py index 17b5028d..c1ddd0ce 100644 --- a/app/api/routes/__init__.py +++ b/app/api/routes/__init__.py @@ -4,25 +4,20 @@ from dateutil import parser import app.utils as utils -from algoliasearch.exceptions import (AlgoliaException, - AlgoliaUnreachableHostException) +from algoliasearch.exceptions import AlgoliaException, AlgoliaUnreachableHostException from app import Config, db, index from app.api import bp -from app.api.auth import (authenticate, create_new_apikey, is_user_oc_member, - rotate_key) -from app.api.validations import (requires_body, validate_resource, - validate_resource_list, wrong_type) -from app.models import Category, Key, Language, Resource -from flask import g, redirect, request -from prometheus_client import Counter, Summary +from app.api.auth import authenticate +from app.api.routes import api_key +from app.api.routes.logger import logger +from app.api.routes.metrics import latency_summary, failures_counter +from app.api.validations import ( + requires_body, validate_resource, validate_resource_list, wrong_type) +from app.models import Category, Language, Resource +from flask import redirect, request from sqlalchemy import func, or_ from sqlalchemy.exc import IntegrityError -# Metrics -failures_counter = Counter('my_failures', 'Number of exceptions raised') -latency_summary = Summary('request_latency_seconds', 'Length of request') - -logger = utils.setup_logger('routes_logger') # Routes @latency_summary.time() @@ -133,64 +128,6 @@ def category(id): return get_category(id) -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) - - -@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)) - - # Helpers def get_resource(id): resource = Resource.query.get(id) @@ -425,14 +362,13 @@ def get_attributes(json): def update_votes(id, vote_direction): - resource = Resource.query.get(id) if not resource: return redirect('/404') initial_count = getattr(resource, vote_direction) - setattr(resource, vote_direction, initial_count+1) + setattr(resource, vote_direction, initial_count + 1) db.session.commit() return utils.standardize_response(payload=dict(data=resource.serialize)) diff --git a/app/api/routes/api_key.py b/app/api/routes/api_key.py new file mode 100644 index 00000000..5a734dc5 --- /dev/null +++ b/app/api/routes/api_key.py @@ -0,0 +1,61 @@ +from flask import request, g + +from app import db, utils as utils +from app.api import bp +from app.api.auth import is_user_oc_member, create_new_apikey, authenticate, rotate_key +from app.api.routes.logger import logger +from app.api.routes.metrics import latency_summary, failures_counter +from app.api.routes.helpers import _unauthorized_response +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)) diff --git a/app/api/routes/helpers.py b/app/api/routes/helpers.py new file mode 100644 index 00000000..32411904 --- /dev/null +++ b/app/api/routes/helpers.py @@ -0,0 +1,8 @@ +from app import utils as utils + + +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) diff --git a/app/api/routes/logger.py b/app/api/routes/logger.py new file mode 100644 index 00000000..ad8f46c4 --- /dev/null +++ b/app/api/routes/logger.py @@ -0,0 +1,3 @@ +from app import utils as utils + +logger = utils.setup_logger('routes_logger') diff --git a/app/api/routes/metrics.py b/app/api/routes/metrics.py new file mode 100644 index 00000000..d72f750a --- /dev/null +++ b/app/api/routes/metrics.py @@ -0,0 +1,4 @@ +from prometheus_client import Summary, Counter + +latency_summary = Summary('request_latency_seconds', 'Length of request') +failures_counter = Counter('my_failures', 'Number of exceptions raised') From 6957de2f3ae8109891e4bed975b699d4e5d73408 Mon Sep 17 00:00:00 2001 From: Joel Gerber Date: Wed, 25 Dec 2019 14:54:33 -0500 Subject: [PATCH 3/9] Extract /categories/... to separate module --- app/api/routes/__init__.py | 46 +------------------------------ app/api/routes/categories.py | 52 ++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 45 deletions(-) create mode 100644 app/api/routes/categories.py diff --git a/app/api/routes/__init__.py b/app/api/routes/__init__.py index c1ddd0ce..ed672559 100644 --- a/app/api/routes/__init__.py +++ b/app/api/routes/__init__.py @@ -8,7 +8,7 @@ from app import Config, db, index from app.api import bp from app.api.auth import authenticate -from app.api.routes import api_key +from app.api.routes import api_key, categories from app.api.routes.logger import logger from app.api.routes.metrics import latency_summary, failures_counter from app.api.validations import ( @@ -114,20 +114,6 @@ def language(id): return get_language(id) -@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/', methods=['GET'], endpoint='get_category') -def category(id): - return get_category(id) - - # Helpers def get_resource(id): resource = Resource.query.get(id) @@ -314,36 +300,6 @@ def get_language(id): return redirect('/404') -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') - - def get_attributes(json): languages_list = Language.query.all() categories_list = Category.query.all() diff --git a/app/api/routes/categories.py b/app/api/routes/categories.py new file mode 100644 index 00000000..e62a8669 --- /dev/null +++ b/app/api/routes/categories.py @@ -0,0 +1,52 @@ +from flask import request, redirect + +from app import utils as utils +from app.api import bp +from app.api.routes.metrics import latency_summary, failures_counter +from app.api.routes.logger import 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/', 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') From 9dbb44c838f747fb9caaa0e9b00ebe59b58a628c Mon Sep 17 00:00:00 2001 From: Joel Gerber Date: Wed, 25 Dec 2019 14:56:54 -0500 Subject: [PATCH 4/9] Extract /languages/... to separate module --- app/api/routes/__init__.py | 46 +------------------------------- app/api/routes/languages.py | 52 +++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 45 deletions(-) create mode 100644 app/api/routes/languages.py diff --git a/app/api/routes/__init__.py b/app/api/routes/__init__.py index ed672559..dd4f2542 100644 --- a/app/api/routes/__init__.py +++ b/app/api/routes/__init__.py @@ -8,7 +8,7 @@ from app import Config, db, index from app.api import bp from app.api.auth import authenticate -from app.api.routes import api_key, categories +from app.api.routes import api_key, categories, languages from app.api.routes.logger import logger from app.api.routes.metrics import latency_summary, failures_counter from app.api.validations import ( @@ -100,20 +100,6 @@ def search(): return search_results() -@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/', methods=['GET'], endpoint='get_language') -def language(id): - return get_language(id) - - # Helpers def get_resource(id): resource = Resource.query.get(id) @@ -270,36 +256,6 @@ def search_results(): return utils.standardize_response(payload=dict(data=results, **pagination_details)) -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') - - def get_attributes(json): languages_list = Language.query.all() categories_list = Category.query.all() diff --git a/app/api/routes/languages.py b/app/api/routes/languages.py new file mode 100644 index 00000000..abc757f3 --- /dev/null +++ b/app/api/routes/languages.py @@ -0,0 +1,52 @@ +from flask import request, redirect + +from app import utils as utils +from app.api import bp +from app.api.routes.metrics import latency_summary, failures_counter +from app.api.routes.logger import 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/', 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') From 29eae9858610cdb2ac4d3ee7b90559bd73d25842 Mon Sep 17 00:00:00 2001 From: Joel Gerber Date: Wed, 25 Dec 2019 15:01:18 -0500 Subject: [PATCH 5/9] Extract /search/... to separate module --- app/api/routes/__init__.py | 77 +---------------------------------- app/api/routes/search.py | 83 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 76 deletions(-) create mode 100644 app/api/routes/search.py diff --git a/app/api/routes/__init__.py b/app/api/routes/__init__.py index dd4f2542..62f81c89 100644 --- a/app/api/routes/__init__.py +++ b/app/api/routes/__init__.py @@ -8,7 +8,7 @@ from app import Config, db, index from app.api import bp from app.api.auth import authenticate -from app.api.routes import api_key, categories, languages +from app.api.routes import api_key, categories, languages, search from app.api.routes.logger import logger from app.api.routes.metrics import latency_summary, failures_counter from app.api.validations import ( @@ -93,13 +93,6 @@ def update_resource_click(id): return add_click(id) -@latency_summary.time() -@failures_counter.count_exceptions() -@bp.route('/search', methods=['GET']) -def search(): - return search_results() - - # Helpers def get_resource(id): resource = Resource.query.get(id) @@ -188,74 +181,6 @@ def get_resources(): **pagination_details)) -def search_results(): - term = request.args.get('q', '', str) - page = request.args.get('page', 0, int) - page_size = request.args.get('page_size', Config.RESOURCE_PAGINATOR.per_page, int) - - # Fetch the filter params from the url, if they were provided. - paid = request.args.get('paid') - category = request.args.get('category') - languages = request.args.getlist('languages') - filters = [] - - # Filter on paid - if paid: - paid = paid.lower() - # algolia filters boolean attributes with either 0 or 1 - if paid == 'true': - filters.append('paid=1') - elif paid == 'false': - filters.append('paid=0') - - # Filter on category - if category: - # to not let double quotes conflict with algolia filter format - category = category.replace('"', "'") - - filters.append( - f'category: "{category}"' - ) - - # Filter on languages - if languages and '' not in languages: - for i, _ in enumerate(languages): - # to not let double quotes conflict with algolia filter format - languages[i] = 'languages:"{}"'.format(languages[i].replace('"', "'")) - - # joining all possible language values to algolia filter query - filters.append(f"( {' OR '.join(languages)} )") - - try: - search_result = index.search(f'{term}', { - 'filters': " AND ".join(filters), - 'page': page, - 'hitsPerPage': page_size - }) - - except (AlgoliaUnreachableHostException, AlgoliaException) as e: - logger.exception(e) - msg = "Failed to get resources from Algolia" - logger.warn(msg) - error = {'errors': [{"algolia-failed": {"message": msg}}]} - return utils.standardize_response(payload=error, status_code=500) - - if page >= int(search_result['nbPages']): - return redirect('/404') - - results = [utils.format_resource_search(result) for result in search_result['hits']] - - pagination_details = { - "pagination_details": { - "page": search_result['page'], - "number_of_pages": search_result['nbPages'], - "records_per_page": search_result['hitsPerPage'], - "total_count": search_result['nbHits'], - } - } - return utils.standardize_response(payload=dict(data=results, **pagination_details)) - - def get_attributes(json): languages_list = Language.query.all() categories_list = Category.query.all() diff --git a/app/api/routes/search.py b/app/api/routes/search.py new file mode 100644 index 00000000..3a24ae46 --- /dev/null +++ b/app/api/routes/search.py @@ -0,0 +1,83 @@ +from algoliasearch.exceptions import AlgoliaUnreachableHostException, AlgoliaException +from flask import request, redirect + +from app import index, utils as utils +from app.api import bp +from app.api.routes.metrics import latency_summary, failures_counter +from app.api.routes.logger import logger +from configs import Config + + +@latency_summary.time() +@failures_counter.count_exceptions() +@bp.route('/search', methods=['GET']) +def search(): + return search_results() + + +def search_results(): + term = request.args.get('q', '', str) + page = request.args.get('page', 0, int) + page_size = request.args.get('page_size', Config.RESOURCE_PAGINATOR.per_page, int) + + # Fetch the filter params from the url, if they were provided. + paid = request.args.get('paid') + category = request.args.get('category') + languages = request.args.getlist('languages') + filters = [] + + # Filter on paid + if paid: + paid = paid.lower() + # algolia filters boolean attributes with either 0 or 1 + if paid == 'true': + filters.append('paid=1') + elif paid == 'false': + filters.append('paid=0') + + # Filter on category + if category: + # to not let double quotes conflict with algolia filter format + category = category.replace('"', "'") + + filters.append( + f'category: "{category}"' + ) + + # Filter on languages + if languages and '' not in languages: + for i, _ in enumerate(languages): + # to not let double quotes conflict with algolia filter format + languages[i] = 'languages:"{}"'.format(languages[i].replace('"', "'")) + + # joining all possible language values to algolia filter query + filters.append(f"( {' OR '.join(languages)} )") + + try: + search_result = index.search(f'{term}', { + 'filters': " AND ".join(filters), + 'page': page, + 'hitsPerPage': page_size + }) + + except (AlgoliaUnreachableHostException, AlgoliaException) as e: + logger.exception(e) + msg = "Failed to get resources from Algolia" + logger.warn(msg) + error = {'errors': [{"algolia-failed": {"message": msg}}]} + return utils.standardize_response(payload=error, status_code=500) + + if page >= int(search_result['nbPages']): + return redirect('/404') + + results = [utils.format_resource_search(result) for result in search_result['hits']] + + pagination_details = { + "pagination_details": { + "page": search_result['page'], + "number_of_pages": search_result['nbPages'], + "records_per_page": search_result['hitsPerPage'], + "total_count": search_result['nbHits'], + } + } + return utils.standardize_response(payload=dict(data=results, **pagination_details)) From 26babece35f4cdcc77b8e3177def74a8dbc48fb6 Mon Sep 17 00:00:00 2001 From: Joel Gerber Date: Wed, 25 Dec 2019 15:15:39 -0500 Subject: [PATCH 6/9] Extract /resources/... to separate modules --- app/api/routes/__init__.py | 351 +-------------------------- app/api/routes/api_key.py | 8 +- app/api/routes/categories.py | 4 +- app/api/routes/helpers.py | 18 ++ app/api/routes/languages.py | 4 +- app/api/routes/metrics.py | 2 +- app/api/routes/resource_creation.py | 95 ++++++++ app/api/routes/resource_retrieval.py | 113 +++++++++ app/api/routes/resource_updating.py | 142 +++++++++++ app/api/routes/search.py | 6 +- 10 files changed, 383 insertions(+), 360 deletions(-) create mode 100644 app/api/routes/resource_creation.py create mode 100644 app/api/routes/resource_retrieval.py create mode 100644 app/api/routes/resource_updating.py diff --git a/app/api/routes/__init__.py b/app/api/routes/__init__.py index 62f81c89..bc1567f2 100644 --- a/app/api/routes/__init__.py +++ b/app/api/routes/__init__.py @@ -1,348 +1,3 @@ -from datetime import datetime -from os import environ - -from dateutil import parser - -import app.utils as utils -from algoliasearch.exceptions import AlgoliaException, AlgoliaUnreachableHostException -from app import Config, db, index -from app.api import bp -from app.api.auth import authenticate -from app.api.routes import api_key, categories, languages, search -from app.api.routes.logger import logger -from app.api.routes.metrics import latency_summary, failures_counter -from app.api.validations import ( - requires_body, validate_resource, validate_resource_list, wrong_type) -from app.models import Category, Language, Resource -from flask import redirect, request -from sqlalchemy import func, or_ -from sqlalchemy.exc import IntegrityError - - -# Routes -@latency_summary.time() -@failures_counter.count_exceptions() -@bp.route('/resources', methods=['GET'], endpoint='get_resources') -def resources(): - return get_resources() - - -@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) - - -@latency_summary.time() -@failures_counter.count_exceptions() -@bp.route('/resources/', methods=['GET'], endpoint='get_resource') -def resource(id): - return get_resource(id) - - -@latency_summary.time() -@failures_counter.count_exceptions() -@bp.route('/resources/', methods=['PUT'], endpoint='update_resource') -@requires_body -@authenticate -def put_resource(id): - json = request.get_json() - - if not isinstance(json, dict): - return wrong_type("resource object", type(json)) - - validation_errors = validate_resource(request.method, json, id) - - if validation_errors: - errors = {"errors": validation_errors} - return utils.standardize_response(payload=errors, status_code=422) - return update_resource(id, request.get_json(), db) - - -@latency_summary.time() -@failures_counter.count_exceptions() -@bp.route('/resources//upvote', methods=['PUT']) -def upvote(id): - return update_votes(id, 'upvotes') - - -@latency_summary.time() -@failures_counter.count_exceptions() -@bp.route('/resources//downvote', methods=['PUT']) -def downvote(id): - return update_votes(id, 'downvotes') - - -@latency_summary.time() -@failures_counter.count_exceptions() -@bp.route('/resources//click', methods=['PUT']) -def update_resource_click(id): - return add_click(id) - - -# Helpers -def get_resource(id): - resource = Resource.query.get(id) - - if resource: - return utils.standardize_response(payload=dict(data=(resource.serialize))) - - return redirect('/404') - - -def get_resources(): - """ - Gets a paginated list of resources. - - If the URL parameters `languages` or `category` are found - in the request, the list will be filtered by these parameters. - - The filters are case insensitive. - """ - resource_paginator = utils.Paginator(Config.RESOURCE_PAGINATOR, request) - - # Fetch the filter params from the url, if they were provided. - languages = request.args.getlist('languages') - category = request.args.get('category') - updated_after = request.args.get('updated_after') - paid = request.args.get('paid') - - q = Resource.query - - # Filter on languages - if languages: - # Take the list of languages they pass in, join them all with OR - q = q.filter( - or_(*map(Resource.languages.any, - map(Language.name.ilike, languages)) - ) - ) - - # Filter on category - if category: - q = q.filter( - Resource.category.has( - func.lower(Category.name) == category.lower() - ) - ) - - # Filter on updated_after - if updated_after: - try: - uaDate = parser.parse(updated_after) - if uaDate > datetime.now(): - raise Exception("updated_after greater than today's date") - uaDate = uaDate.strftime("%Y-%m-%d") - except Exception as e: - logger.exception(e) - message = 'The value for "updated_after" is invalid' - res = {"errors": {"unprocessable-entity": {"message": message}}} - return utils.standardize_response(payload=res, status_code=422) - - q = q.filter( - or_( - Resource.created_at >= uaDate, - Resource.last_updated >= uaDate - ) - ) - - # Filter on paid - if isinstance(paid, str) and paid.lower() in ['true', 'false']: - paidAsBool = paid.lower() == 'true' - q = q.filter(Resource.paid == paidAsBool) - - try: - paginated_resources = resource_paginator.paginated_data(q) - if not paginated_resources: - return redirect('/404') - resource_list = [ - resource.serialize for resource in paginated_resources.items - ] - pagination_details = resource_paginator.pagination_details(paginated_resources) - except Exception as e: - logger.exception(e) - return utils.standardize_response(status_code=500) - - return utils.standardize_response(payload=dict( - data=resource_list, - **pagination_details)) - - -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) - - -def update_votes(id, vote_direction): - resource = Resource.query.get(id) - - if not resource: - return redirect('/404') - - initial_count = getattr(resource, vote_direction) - setattr(resource, vote_direction, initial_count + 1) - db.session.commit() - - return utils.standardize_response(payload=dict(data=resource.serialize)) - - -def add_click(id): - resource = Resource.query.get(id) - - if not resource: - return redirect('/404') - - initial_count = getattr(resource, 'times_clicked') - setattr(resource, 'times_clicked', initial_count + 1) - db.session.commit() - - return utils.standardize_response(payload=dict(data=resource.serialize)) - - -def update_resource(id, json, db): - resource = Resource.query.get(id) - - if not resource: - return redirect('/404') - - langs, categ = get_attributes(json) - index_object = {'objectID': id} - - try: - logger.info(f"Updating resource. Old data: {resource.serialize}") - if json.get('languages'): - resource.languages = langs - index_object['languages'] = resource.serialize['languages'] - if json.get('category'): - resource.category = categ - index_object['category'] = categ.name - if json.get('name'): - resource.name = json.get('name') - index_object['name'] = json.get('name') - if json.get('url'): - resource.url = json.get('url') - index_object['url'] = json.get('url') - if 'paid' in json: - paid = json.get('paid') - - # Converts "false" and "true" to their bool - if type(paid) is str and paid.lower() in ["true", "false"]: - paid = paid.lower().strip() == "true" - resource.paid = paid - index_object['paid'] = paid - if 'notes' in json: - resource.notes = json.get('notes') - index_object['notes'] = json.get('notes') - - try: - index.partial_update_object(index_object) - - except (AlgoliaUnreachableHostException, AlgoliaException) as e: - if (environ.get("FLASK_ENV") != 'development'): - logger.exception(e) - msg = f"Algolia failed to update index for resource '{resource.name}'" - logger.warn(msg) - error = {'errors': [{"algolia-failed": {"message": msg}}]} - return utils.standardize_response(payload=error, status_code=500) - - # Wait to commit the changes until we know that Aloglia was updated - db.session.commit() - - return utils.standardize_response( - payload=dict(data=resource.serialize) - ) - - 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) - - -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) +from app.api.routes import ( + api_key, categories, languages, resource_creation, resource_retrieval, + resource_updating, search) diff --git a/app/api/routes/api_key.py b/app/api/routes/api_key.py index 5a734dc5..927f356f 100644 --- a/app/api/routes/api_key.py +++ b/app/api/routes/api_key.py @@ -1,11 +1,11 @@ -from flask import request, g +from flask import g, request from app import db, utils as utils from app.api import bp -from app.api.auth import is_user_oc_member, create_new_apikey, authenticate, rotate_key -from app.api.routes.logger import logger -from app.api.routes.metrics import latency_summary, failures_counter +from app.api.auth import authenticate, create_new_apikey, is_user_oc_member, rotate_key from app.api.routes.helpers import _unauthorized_response +from app.api.routes.logger import logger +from app.api.routes.metrics import failures_counter, latency_summary from app.api.validations import requires_body from app.models import Key diff --git a/app/api/routes/categories.py b/app/api/routes/categories.py index e62a8669..a3261be0 100644 --- a/app/api/routes/categories.py +++ b/app/api/routes/categories.py @@ -1,9 +1,9 @@ -from flask import request, redirect +from flask import redirect, request from app import utils as utils from app.api import bp -from app.api.routes.metrics import latency_summary, failures_counter from app.api.routes.logger import logger +from app.api.routes.metrics import failures_counter, latency_summary from app.models import Category from configs import Config diff --git a/app/api/routes/helpers.py b/app/api/routes/helpers.py index 32411904..c005eabe 100644 --- a/app/api/routes/helpers.py +++ b/app/api/routes/helpers.py @@ -1,4 +1,5 @@ from app import utils as utils +from app.models import Category, Language def _unauthorized_response(): @@ -6,3 +7,20 @@ def _unauthorized_response(): "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) diff --git a/app/api/routes/languages.py b/app/api/routes/languages.py index abc757f3..418c9af5 100644 --- a/app/api/routes/languages.py +++ b/app/api/routes/languages.py @@ -1,9 +1,9 @@ -from flask import request, redirect +from flask import redirect, request from app import utils as utils from app.api import bp -from app.api.routes.metrics import latency_summary, failures_counter from app.api.routes.logger import logger +from app.api.routes.metrics import failures_counter, latency_summary from app.models import Language from configs import Config diff --git a/app/api/routes/metrics.py b/app/api/routes/metrics.py index d72f750a..dda10a0d 100644 --- a/app/api/routes/metrics.py +++ b/app/api/routes/metrics.py @@ -1,4 +1,4 @@ -from prometheus_client import Summary, Counter +from prometheus_client import Counter, Summary latency_summary = Summary('request_latency_seconds', 'Length of request') failures_counter = Counter('my_failures', 'Number of exceptions raised') diff --git a/app/api/routes/resource_creation.py b/app/api/routes/resource_creation.py new file mode 100644 index 00000000..7dc69a3a --- /dev/null +++ b/app/api/routes/resource_creation.py @@ -0,0 +1,95 @@ +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 get_attributes +from app.api.routes.logger import logger +from app.api.routes.metrics import failures_counter, latency_summary +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) diff --git a/app/api/routes/resource_retrieval.py b/app/api/routes/resource_retrieval.py new file mode 100644 index 00000000..4c3e4477 --- /dev/null +++ b/app/api/routes/resource_retrieval.py @@ -0,0 +1,113 @@ +from datetime import datetime + +from dateutil import parser +from flask import redirect, request +from sqlalchemy import func, or_ + +from app import utils as utils +from app.api import bp +from app.api.routes.logger import logger +from app.api.routes.metrics import failures_counter, latency_summary +from app.models import Category, Language, Resource +from configs import Config + + +@latency_summary.time() +@failures_counter.count_exceptions() +@bp.route('/resources', methods=['GET'], endpoint='get_resources') +def resources(): + return get_resources() + + +@latency_summary.time() +@failures_counter.count_exceptions() +@bp.route('/resources/', methods=['GET'], endpoint='get_resource') +def resource(id): + return get_resource(id) + + +def get_resources(): + """ + Gets a paginated list of resources. + + If the URL parameters `languages` or `category` are found + in the request, the list will be filtered by these parameters. + + The filters are case insensitive. + """ + resource_paginator = utils.Paginator(Config.RESOURCE_PAGINATOR, request) + + # Fetch the filter params from the url, if they were provided. + languages = request.args.getlist('languages') + category = request.args.get('category') + updated_after = request.args.get('updated_after') + paid = request.args.get('paid') + + q = Resource.query + + # Filter on languages + if languages: + # Take the list of languages they pass in, join them all with OR + q = q.filter( + or_(*map(Resource.languages.any, + map(Language.name.ilike, languages)) + ) + ) + + # Filter on category + if category: + q = q.filter( + Resource.category.has( + func.lower(Category.name) == category.lower() + ) + ) + + # Filter on updated_after + if updated_after: + try: + uaDate = parser.parse(updated_after) + if uaDate > datetime.now(): + raise Exception("updated_after greater than today's date") + uaDate = uaDate.strftime("%Y-%m-%d") + except Exception as e: + logger.exception(e) + message = 'The value for "updated_after" is invalid' + res = {"errors": {"unprocessable-entity": {"message": message}}} + return utils.standardize_response(payload=res, status_code=422) + + q = q.filter( + or_( + Resource.created_at >= uaDate, + Resource.last_updated >= uaDate + ) + ) + + # Filter on paid + if isinstance(paid, str) and paid.lower() in ['true', 'false']: + paidAsBool = paid.lower() == 'true' + q = q.filter(Resource.paid == paidAsBool) + + try: + paginated_resources = resource_paginator.paginated_data(q) + if not paginated_resources: + return redirect('/404') + resource_list = [ + resource.serialize for resource in paginated_resources.items + ] + pagination_details = resource_paginator.pagination_details(paginated_resources) + except Exception as e: + logger.exception(e) + return utils.standardize_response(status_code=500) + + return utils.standardize_response(payload=dict( + data=resource_list, + **pagination_details)) + + +def get_resource(id): + resource = Resource.query.get(id) + + if resource: + return utils.standardize_response(payload=dict(data=(resource.serialize))) + + return redirect('/404') diff --git a/app/api/routes/resource_updating.py b/app/api/routes/resource_updating.py new file mode 100644 index 00000000..0ddf863e --- /dev/null +++ b/app/api/routes/resource_updating.py @@ -0,0 +1,142 @@ +from os import environ + +from algoliasearch.exceptions import AlgoliaException, AlgoliaUnreachableHostException +from flask import redirect, 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 get_attributes +from app.api.routes.logger import logger +from app.api.routes.metrics import failures_counter, latency_summary +from app.api.validations import requires_body, validate_resource, wrong_type +from app.models import Resource + + +@latency_summary.time() +@failures_counter.count_exceptions() +@bp.route('/resources/', methods=['PUT'], endpoint='update_resource') +@requires_body +@authenticate +def put_resource(id): + json = request.get_json() + + if not isinstance(json, dict): + return wrong_type("resource object", type(json)) + + validation_errors = validate_resource(request.method, json, id) + + if validation_errors: + errors = {"errors": validation_errors} + return utils.standardize_response(payload=errors, status_code=422) + return update_resource(id, request.get_json(), db) + + +def update_resource(id, json, db): + resource = Resource.query.get(id) + + if not resource: + return redirect('/404') + + langs, categ = get_attributes(json) + index_object = {'objectID': id} + + try: + logger.info(f"Updating resource. Old data: {resource.serialize}") + if json.get('languages'): + resource.languages = langs + index_object['languages'] = resource.serialize['languages'] + if json.get('category'): + resource.category = categ + index_object['category'] = categ.name + if json.get('name'): + resource.name = json.get('name') + index_object['name'] = json.get('name') + if json.get('url'): + resource.url = json.get('url') + index_object['url'] = json.get('url') + if 'paid' in json: + paid = json.get('paid') + + # Converts "false" and "true" to their bool + if type(paid) is str and paid.lower() in ["true", "false"]: + paid = paid.lower().strip() == "true" + resource.paid = paid + index_object['paid'] = paid + if 'notes' in json: + resource.notes = json.get('notes') + index_object['notes'] = json.get('notes') + + try: + index.partial_update_object(index_object) + + except (AlgoliaUnreachableHostException, AlgoliaException) as e: + if environ.get("FLASK_ENV") != 'development': + logger.exception(e) + msg = f"Algolia failed to update index for resource '{resource.name}'" + logger.warn(msg) + error = {'errors': [{"algolia-failed": {"message": msg}}]} + return utils.standardize_response(payload=error, status_code=500) + + # Wait to commit the changes until we know that Aloglia was updated + db.session.commit() + + return utils.standardize_response( + payload=dict(data=resource.serialize) + ) + + 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) + + +@latency_summary.time() +@failures_counter.count_exceptions() +@bp.route('/resources//upvote', methods=['PUT']) +def upvote(id): + return update_votes(id, 'upvotes') + + +@latency_summary.time() +@failures_counter.count_exceptions() +@bp.route('/resources//downvote', methods=['PUT']) +def downvote(id): + return update_votes(id, 'downvotes') + + +@latency_summary.time() +@failures_counter.count_exceptions() +@bp.route('/resources//click', methods=['PUT']) +def update_resource_click(id): + return add_click(id) + + +def update_votes(id, vote_direction): + resource = Resource.query.get(id) + + if not resource: + return redirect('/404') + + initial_count = getattr(resource, vote_direction) + setattr(resource, vote_direction, initial_count + 1) + db.session.commit() + + return utils.standardize_response(payload=dict(data=resource.serialize)) + + +def add_click(id): + resource = Resource.query.get(id) + + if not resource: + return redirect('/404') + + initial_count = getattr(resource, 'times_clicked') + setattr(resource, 'times_clicked', initial_count + 1) + db.session.commit() + + return utils.standardize_response(payload=dict(data=resource.serialize)) diff --git a/app/api/routes/search.py b/app/api/routes/search.py index 3a24ae46..1a85cc50 100644 --- a/app/api/routes/search.py +++ b/app/api/routes/search.py @@ -1,10 +1,10 @@ -from algoliasearch.exceptions import AlgoliaUnreachableHostException, AlgoliaException -from flask import request, redirect +from algoliasearch.exceptions import AlgoliaException, AlgoliaUnreachableHostException +from flask import redirect, request from app import index, utils as utils from app.api import bp -from app.api.routes.metrics import latency_summary, failures_counter from app.api.routes.logger import logger +from app.api.routes.metrics import failures_counter, latency_summary from configs import Config From c9a69d2f2d3acc2fa99f6bb085ab444f8352a13e Mon Sep 17 00:00:00 2001 From: Joel Gerber Date: Wed, 25 Dec 2019 15:19:00 -0500 Subject: [PATCH 7/9] Consolidate helpers and rename resource updating to resource retrieval --- app/api/routes/__init__.py | 4 ++-- app/api/routes/api_key.py | 5 ++--- app/api/routes/categories.py | 3 +-- app/api/routes/helpers.py | 6 ++++++ app/api/routes/languages.py | 3 +-- app/api/routes/logger.py | 3 --- app/api/routes/metrics.py | 4 ---- app/api/routes/resource_creation.py | 5 ++--- .../{resource_updating.py => resource_modification.py} | 5 ++--- app/api/routes/resource_retrieval.py | 3 +-- app/api/routes/search.py | 3 +-- 11 files changed, 18 insertions(+), 26 deletions(-) delete mode 100644 app/api/routes/logger.py delete mode 100644 app/api/routes/metrics.py rename app/api/routes/{resource_updating.py => resource_modification.py} (96%) diff --git a/app/api/routes/__init__.py b/app/api/routes/__init__.py index bc1567f2..0113a588 100644 --- a/app/api/routes/__init__.py +++ b/app/api/routes/__init__.py @@ -1,3 +1,3 @@ from app.api.routes import ( - api_key, categories, languages, resource_creation, resource_retrieval, - resource_updating, search) + api_key, categories, languages, resource_creation, resource_modification, + resource_retrieval, search) diff --git a/app/api/routes/api_key.py b/app/api/routes/api_key.py index 927f356f..de435812 100644 --- a/app/api/routes/api_key.py +++ b/app/api/routes/api_key.py @@ -3,9 +3,8 @@ 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 -from app.api.routes.logger import logger -from app.api.routes.metrics import failures_counter, latency_summary +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 diff --git a/app/api/routes/categories.py b/app/api/routes/categories.py index a3261be0..3a154cc1 100644 --- a/app/api/routes/categories.py +++ b/app/api/routes/categories.py @@ -2,8 +2,7 @@ from app import utils as utils from app.api import bp -from app.api.routes.logger import logger -from app.api.routes.metrics import failures_counter, latency_summary +from app.api.routes.helpers import failures_counter, latency_summary, logger from app.models import Category from configs import Config diff --git a/app/api/routes/helpers.py b/app/api/routes/helpers.py index c005eabe..b7f70cea 100644 --- a/app/api/routes/helpers.py +++ b/app/api/routes/helpers.py @@ -1,6 +1,12 @@ +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 " \ diff --git a/app/api/routes/languages.py b/app/api/routes/languages.py index 418c9af5..f3c095f9 100644 --- a/app/api/routes/languages.py +++ b/app/api/routes/languages.py @@ -2,8 +2,7 @@ from app import utils as utils from app.api import bp -from app.api.routes.logger import logger -from app.api.routes.metrics import failures_counter, latency_summary +from app.api.routes.helpers import failures_counter, latency_summary, logger from app.models import Language from configs import Config diff --git a/app/api/routes/logger.py b/app/api/routes/logger.py deleted file mode 100644 index ad8f46c4..00000000 --- a/app/api/routes/logger.py +++ /dev/null @@ -1,3 +0,0 @@ -from app import utils as utils - -logger = utils.setup_logger('routes_logger') diff --git a/app/api/routes/metrics.py b/app/api/routes/metrics.py deleted file mode 100644 index dda10a0d..00000000 --- a/app/api/routes/metrics.py +++ /dev/null @@ -1,4 +0,0 @@ -from prometheus_client import Counter, Summary - -latency_summary = Summary('request_latency_seconds', 'Length of request') -failures_counter = Counter('my_failures', 'Number of exceptions raised') diff --git a/app/api/routes/resource_creation.py b/app/api/routes/resource_creation.py index 7dc69a3a..ab4901bc 100644 --- a/app/api/routes/resource_creation.py +++ b/app/api/routes/resource_creation.py @@ -7,9 +7,8 @@ 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 get_attributes -from app.api.routes.logger import logger -from app.api.routes.metrics import failures_counter, latency_summary +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 diff --git a/app/api/routes/resource_updating.py b/app/api/routes/resource_modification.py similarity index 96% rename from app/api/routes/resource_updating.py rename to app/api/routes/resource_modification.py index 0ddf863e..e024651e 100644 --- a/app/api/routes/resource_updating.py +++ b/app/api/routes/resource_modification.py @@ -7,9 +7,8 @@ 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 get_attributes -from app.api.routes.logger import logger -from app.api.routes.metrics import failures_counter, latency_summary +from app.api.routes.helpers import ( + failures_counter, get_attributes, latency_summary, logger) from app.api.validations import requires_body, validate_resource, wrong_type from app.models import Resource diff --git a/app/api/routes/resource_retrieval.py b/app/api/routes/resource_retrieval.py index 4c3e4477..6a5fff6b 100644 --- a/app/api/routes/resource_retrieval.py +++ b/app/api/routes/resource_retrieval.py @@ -6,8 +6,7 @@ from app import utils as utils from app.api import bp -from app.api.routes.logger import logger -from app.api.routes.metrics import failures_counter, latency_summary +from app.api.routes.helpers import failures_counter, latency_summary, logger from app.models import Category, Language, Resource from configs import Config diff --git a/app/api/routes/search.py b/app/api/routes/search.py index 1a85cc50..16868ebf 100644 --- a/app/api/routes/search.py +++ b/app/api/routes/search.py @@ -3,8 +3,7 @@ from app import index, utils as utils from app.api import bp -from app.api.routes.logger import logger -from app.api.routes.metrics import failures_counter, latency_summary +from app.api.routes.helpers import failures_counter, latency_summary, logger from configs import Config From d126f79604df8a17533645419d9cc6c6cab4895a Mon Sep 17 00:00:00 2001 From: Joel Gerber Date: Wed, 25 Dec 2019 15:27:48 -0500 Subject: [PATCH 8/9] Resolve linter not recognizing import side effects The linter is complaining about unused imports, but importing them here actually causes side effects that Flask relies on to have the web service properly configured. --- app/api/routes/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/routes/__init__.py b/app/api/routes/__init__.py index 0113a588..1dc85c0e 100644 --- a/app/api/routes/__init__.py +++ b/app/api/routes/__init__.py @@ -1,3 +1,3 @@ from app.api.routes import ( api_key, categories, languages, resource_creation, resource_modification, - resource_retrieval, search) + resource_retrieval, search) # noqa From c204a080f8f738fd62b15da4495c76faae6f06bc Mon Sep 17 00:00:00 2001 From: Richard Brown Date: Thu, 26 Dec 2019 12:10:24 -0700 Subject: [PATCH 9/9] Review changes Renames _unauthorized_response to unauthorized_response Exempts __init__.py from test coverage. --- app/api/routes/__init__.py | 4 ++-- app/api/routes/api_key.py | 6 +++--- app/api/routes/helpers.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/api/routes/__init__.py b/app/api/routes/__init__.py index 1dc85c0e..479b2639 100644 --- a/app/api/routes/__init__.py +++ b/app/api/routes/__init__.py @@ -1,3 +1,3 @@ -from app.api.routes import ( +from app.api.routes import ( # noqa api_key, categories, languages, resource_creation, resource_modification, - resource_retrieval, search) # noqa + resource_retrieval, search) diff --git a/app/api/routes/api_key.py b/app/api/routes/api_key.py index de435812..a25198a2 100644 --- a/app/api/routes/api_key.py +++ b/app/api/routes/api_key.py @@ -4,7 +4,7 @@ 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) + unauthorized_response, failures_counter, latency_summary, logger) from app.api.validations import requires_body from app.models import Key @@ -25,7 +25,7 @@ def apikey(): is_oc_member = is_user_oc_member(email, password) if not is_oc_member: - return _unauthorized_response() + return unauthorized_response() try: # We need to check the database for an existing key @@ -33,7 +33,7 @@ def apikey(): # Don't return success for blacklisted keys if apikey and apikey.blacklisted: - return _unauthorized_response() + return unauthorized_response() if not apikey: # Since they're already authenticated by is_oc_user(), we know we diff --git a/app/api/routes/helpers.py b/app/api/routes/helpers.py index b7f70cea..e7b8ddd6 100644 --- a/app/api/routes/helpers.py +++ b/app/api/routes/helpers.py @@ -8,7 +8,7 @@ failures_counter = Counter('my_failures', 'Number of exceptions raised') -def _unauthorized_response(): +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}}}