From d3c6bd228e2f0c7b7e2a1047b6020d0c53f50a1b Mon Sep 17 00:00:00 2001 From: Joel Gerber Date: Sat, 26 Oct 2019 16:38:44 -0400 Subject: [PATCH 01/10] Prepare for breaking routes module into a routes package --- app/api/routes/__init__.py | 1 + app/api/{routes.py => routes/old_routes.py} | 0 2 files changed, 1 insertion(+) create mode 100644 app/api/routes/__init__.py rename app/api/{routes.py => routes/old_routes.py} (100%) diff --git a/app/api/routes/__init__.py b/app/api/routes/__init__.py new file mode 100644 index 00000000..4362971f --- /dev/null +++ b/app/api/routes/__init__.py @@ -0,0 +1 @@ +from app.api.routes import old_routes diff --git a/app/api/routes.py b/app/api/routes/old_routes.py similarity index 100% rename from app/api/routes.py rename to app/api/routes/old_routes.py From 23f24fa9bdd2a50a57b99fa9523540bfbd8500a0 Mon Sep 17 00:00:00 2001 From: Joel Gerber Date: Sat, 26 Oct 2019 16:50:04 -0400 Subject: [PATCH 02/10] Pull out shared utilities into separate package --- app/api/routes/old_routes.py | 46 ++++++++++++++++-------------------- app/api/routes/utils.py | 7 ++++++ 2 files changed, 27 insertions(+), 26 deletions(-) create mode 100644 app/api/routes/utils.py diff --git a/app/api/routes/old_routes.py b/app/api/routes/old_routes.py index 3b293122..b5243f6e 100644 --- a/app/api/routes/old_routes.py +++ b/app/api/routes/old_routes.py @@ -1,23 +1,18 @@ -from flask import request, redirect -from sqlalchemy import or_, func -from sqlalchemy.exc import IntegrityError -from algoliasearch.exceptions import AlgoliaUnreachableHostException, AlgoliaException -from app.api import bp -from app.api.auth import is_user_oc_member, authenticate -from app.api.validations import validate_resource, requires_body -from app.models import Language, Resource, Category, Key -from app import Config, db, index -from dateutil import parser from datetime import datetime -from prometheus_client import Counter, Summary -import app.utils as utils - -# Metrics -failures_counter = Counter('my_failures', 'Number of exceptions raised') -latency_summary = Summary('request_latency_seconds', 'Length of request') +from algoliasearch.exceptions import AlgoliaException, AlgoliaUnreachableHostException +from dateutil import parser +from flask import redirect, request +from sqlalchemy import func, or_ +from sqlalchemy.exc import IntegrityError -logger = utils.setup_logger('routes_logger') +import app.utils as utils +from app import Config, db, index +from app.api import bp +from app.api.auth import authenticate, is_user_oc_member +from app.api.routes.utils import failures_counter, latency_summary, logger +from app.api.validations import requires_body, validate_resource +from app.models import Category, Key, Language, Resource # Routes @@ -289,13 +284,13 @@ def search_results(): 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'], - } + "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)) @@ -377,14 +372,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)) @@ -449,7 +443,7 @@ def update_resource(id, json, db): return utils.standardize_response( payload=dict(data=resource.serialize) - ) + ) except IntegrityError as e: logger.exception(e) diff --git a/app/api/routes/utils.py b/app/api/routes/utils.py new file mode 100644 index 00000000..64a19199 --- /dev/null +++ b/app/api/routes/utils.py @@ -0,0 +1,7 @@ +from prometheus_client import Counter, Summary + +from app import utils as utils + +failures_counter = Counter('my_failures', 'Number of exceptions raised') +latency_summary = Summary('request_latency_seconds', 'Length of request') +logger = utils.setup_logger('routes_logger') From 1465736b5d2eddb94c3d1c8d65355b894176b20d Mon Sep 17 00:00:00 2001 From: Joel Gerber Date: Sat, 26 Oct 2019 17:02:00 -0400 Subject: [PATCH 03/10] Split resources routes into separate module --- app/api/routes/__init__.py | 1 + app/api/routes/old_routes.py | 241 +--------------------------------- app/api/routes/resources.py | 247 +++++++++++++++++++++++++++++++++++ 3 files changed, 250 insertions(+), 239 deletions(-) create mode 100644 app/api/routes/resources.py diff --git a/app/api/routes/__init__.py b/app/api/routes/__init__.py index 4362971f..ba72da11 100644 --- a/app/api/routes/__init__.py +++ b/app/api/routes/__init__.py @@ -1 +1,2 @@ from app.api.routes import old_routes +from app.api.routes import resources diff --git a/app/api/routes/old_routes.py b/app/api/routes/old_routes.py index b5243f6e..bed9d33e 100644 --- a/app/api/routes/old_routes.py +++ b/app/api/routes/old_routes.py @@ -1,61 +1,15 @@ -from datetime import datetime - from algoliasearch.exceptions import AlgoliaException, AlgoliaUnreachableHostException -from dateutil import parser from flask import redirect, request -from sqlalchemy import func, or_ -from sqlalchemy.exc import IntegrityError import app.utils as utils from app import Config, db, index from app.api import bp -from app.api.auth import authenticate, is_user_oc_member +from app.api.auth import is_user_oc_member from app.api.routes.utils import failures_counter, latency_summary, logger -from app.api.validations import requires_body, validate_resource +from app.api.validations import requires_body from app.models import Category, Key, Language, Resource -# 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_resource') -@requires_body -@authenticate -def post_resources(): - validation_errors = validate_resource(request) - - if validation_errors: - return utils.standardize_response(payload=validation_errors, status_code=422) - return create_resource(request.get_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): - validation_errors = validate_resource(request, id) - - if validation_errors: - return utils.standardize_response(payload=validation_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']) @@ -146,93 +100,6 @@ def apikey(): return utils.standardize_response(status_code=500) -# 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 `language` 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. - language = request.args.get('language') - category = request.args.get('category') - updated_after = request.args.get('updated_after') - paid = request.args.get('paid') - - q = Resource.query - - # Filter on language - if language: - q = q.filter( - Resource.languages.any( - Language.name.ilike(language) - ) - ) - - # 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 search_results(): term = request.args.get('q', '', str) page = request.args.get('page', 0, int) @@ -354,23 +221,6 @@ def get_category(id): return redirect('/404') -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) @@ -395,90 +245,3 @@ def add_click(id): 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') - - db.session.commit() - - try: - index.partial_update_object(index_object) - - except (AlgoliaUnreachableHostException, AlgoliaException) as e: - logger.exception(e) - print(f"Algolia failed to update index for resource '{resource.name}'") - - 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_resource(json, db): - langs, categ = get_attributes(json) - new_resource = Resource( - name=json.get('name'), - url=json.get('url'), - category=categ, - languages=langs, - paid=json.get('paid'), - notes=json.get('notes')) - - try: - db.session.add(new_resource) - db.session.commit() - index.save_object(new_resource.serialize_algolia_search) - - except (AlgoliaUnreachableHostException, AlgoliaException) as e: - logger.exception(e) - print(f"Algolia failed to index new resource '{new_resource.name}'") - - 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) - - return utils.standardize_response(payload=dict(data=new_resource.serialize)) diff --git a/app/api/routes/resources.py b/app/api/routes/resources.py new file mode 100644 index 00000000..9c31a61f --- /dev/null +++ b/app/api/routes/resources.py @@ -0,0 +1,247 @@ +from datetime import datetime + +from algoliasearch.exceptions import AlgoliaUnreachableHostException, AlgoliaException +from dateutil import parser +from flask import request, redirect +from sqlalchemy import func, or_ +from sqlalchemy.exc import IntegrityError + +from app import utils as utils, db, index +from app.api import bp +from app.api.auth import authenticate +from app.api.routes.utils import latency_summary, failures_counter, logger +from app.api.validations import requires_body, validate_resource +from app.models import Resource, Language, Category +from configs import Config + + +# 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_resource') +@requires_body +@authenticate +def post_resources(): + validation_errors = validate_resource(request) + + if validation_errors: + return utils.standardize_response(payload=validation_errors, status_code=422) + return create_resource(request.get_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): + validation_errors = validate_resource(request, id) + + if validation_errors: + return utils.standardize_response(payload=validation_errors, status_code=422) + return update_resource(id, request.get_json(), db) + + +# Business Logic +def get_resources(): + """ + Gets a paginated list of resources. + + If the URL parameters `language` 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. + language = request.args.get('language') + category = request.args.get('category') + updated_after = request.args.get('updated_after') + paid = request.args.get('paid') + + q = Resource.query + + # Filter on language + if language: + q = q.filter( + Resource.languages.any( + Language.name.ilike(language) + ) + ) + + # 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 create_resource(json, db): + langs, categ = get_attributes(json) + new_resource = Resource( + name=json.get('name'), + url=json.get('url'), + category=categ, + languages=langs, + paid=json.get('paid'), + notes=json.get('notes')) + + try: + db.session.add(new_resource) + db.session.commit() + index.save_object(new_resource.serialize_algolia_search) + + except (AlgoliaUnreachableHostException, AlgoliaException) as e: + logger.exception(e) + print(f"Algolia failed to index new resource '{new_resource.name}'") + + 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) + + return utils.standardize_response(payload=dict(data=new_resource.serialize)) + + +def get_resource(id): + resource = Resource.query.get(id) + + if resource: + return utils.standardize_response(payload=dict(data=(resource.serialize))) + + return redirect('/404') + + +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') + + db.session.commit() + + try: + index.partial_update_object(index_object) + + except (AlgoliaUnreachableHostException, AlgoliaException) as e: + logger.exception(e) + print(f"Algolia failed to update index for resource '{resource.name}'") + + 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 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) From f7b067bbcb1ac7d5a8c3712b8e430590f9482c96 Mon Sep 17 00:00:00 2001 From: Joel Gerber Date: Sat, 26 Oct 2019 17:06:00 -0400 Subject: [PATCH 04/10] Split categories routes into separate module --- app/api/routes/__init__.py | 1 + app/api/routes/categories.py | 51 ++++++++++++++++++++++++++++++++++++ app/api/routes/old_routes.py | 46 +------------------------------- 3 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 ba72da11..72fa3174 100644 --- a/app/api/routes/__init__.py +++ b/app/api/routes/__init__.py @@ -1,2 +1,3 @@ from app.api.routes import old_routes from app.api.routes import resources +from app.api.routes import categories diff --git a/app/api/routes/categories.py b/app/api/routes/categories.py new file mode 100644 index 00000000..58f28f50 --- /dev/null +++ b/app/api/routes/categories.py @@ -0,0 +1,51 @@ +from flask import request, redirect + +from app import utils as utils +from app.api import bp +from app.api.routes.utils import latency_summary, failures_counter, 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') diff --git a/app/api/routes/old_routes.py b/app/api/routes/old_routes.py index bed9d33e..67fde4ef 100644 --- a/app/api/routes/old_routes.py +++ b/app/api/routes/old_routes.py @@ -7,7 +7,7 @@ from app.api.auth import is_user_oc_member from app.api.routes.utils import failures_counter, latency_summary, logger from app.api.validations import requires_body -from app.models import Category, Key, Language, Resource +from app.models import Key, Language, Resource @latency_summary.time() @@ -52,20 +52,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) - - @latency_summary.time() @failures_counter.count_exceptions() @bp.route('/apikey', methods=['POST'], endpoint='apikey') @@ -191,36 +177,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 update_votes(id, vote_direction): resource = Resource.query.get(id) From 39a236de989267d0e2a148728789edd42eda7014 Mon Sep 17 00:00:00 2001 From: Joel Gerber Date: Sat, 26 Oct 2019 17:09:15 -0400 Subject: [PATCH 05/10] Split languages routes into separate module --- app/api/routes/__init__.py | 1 + app/api/routes/languages.py | 51 ++++++++++++++++++++++++++++++++++++ app/api/routes/old_routes.py | 46 +------------------------------- 3 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 72fa3174..ff5a2c9f 100644 --- a/app/api/routes/__init__.py +++ b/app/api/routes/__init__.py @@ -1,3 +1,4 @@ from app.api.routes import old_routes from app.api.routes import resources from app.api.routes import categories +from app.api.routes import languages diff --git a/app/api/routes/languages.py b/app/api/routes/languages.py new file mode 100644 index 00000000..4c441fd1 --- /dev/null +++ b/app/api/routes/languages.py @@ -0,0 +1,51 @@ +from flask import request, redirect + +from app import utils as utils +from app.api import bp +from app.api.routes.utils import latency_summary, failures_counter, 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') diff --git a/app/api/routes/old_routes.py b/app/api/routes/old_routes.py index 67fde4ef..dd03f671 100644 --- a/app/api/routes/old_routes.py +++ b/app/api/routes/old_routes.py @@ -7,7 +7,7 @@ from app.api.auth import is_user_oc_member from app.api.routes.utils import failures_counter, latency_summary, logger from app.api.validations import requires_body -from app.models import Key, Language, Resource +from app.models import Key, Resource @latency_summary.time() @@ -38,20 +38,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) - - @latency_summary.time() @failures_counter.count_exceptions() @bp.route('/apikey', methods=['POST'], endpoint='apikey') @@ -147,36 +133,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 update_votes(id, vote_direction): resource = Resource.query.get(id) From e1476f3c6bac6816cb1f7ac3221ff44bb98a9eee Mon Sep 17 00:00:00 2001 From: Joel Gerber Date: Sat, 26 Oct 2019 17:11:08 -0400 Subject: [PATCH 06/10] Split resource voting routes into separate module --- app/api/routes/__init__.py | 1 + app/api/routes/old_routes.py | 51 ++--------------------------- app/api/routes/resource_voting.py | 53 +++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 49 deletions(-) create mode 100644 app/api/routes/resource_voting.py diff --git a/app/api/routes/__init__.py b/app/api/routes/__init__.py index ff5a2c9f..044765ee 100644 --- a/app/api/routes/__init__.py +++ b/app/api/routes/__init__.py @@ -1,4 +1,5 @@ from app.api.routes import old_routes from app.api.routes import resources +from app.api.routes import resource_voting from app.api.routes import categories from app.api.routes import languages diff --git a/app/api/routes/old_routes.py b/app/api/routes/old_routes.py index dd03f671..5ff8d620 100644 --- a/app/api/routes/old_routes.py +++ b/app/api/routes/old_routes.py @@ -2,33 +2,12 @@ from flask import redirect, request import app.utils as utils -from app import Config, db, index +from app import Config, index from app.api import bp from app.api.auth import is_user_oc_member from app.api.routes.utils import failures_counter, latency_summary, logger from app.api.validations import requires_body -from app.models import Key, Resource - - -@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) +from app.models import Key @latency_summary.time() @@ -131,29 +110,3 @@ def search_results(): } } return utils.standardize_response(payload=dict(data=results, **pagination_details)) - - -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/resource_voting.py b/app/api/routes/resource_voting.py new file mode 100644 index 00000000..dcb5a503 --- /dev/null +++ b/app/api/routes/resource_voting.py @@ -0,0 +1,53 @@ +from werkzeug.utils import redirect + +from app import db, utils as utils +from app.api import bp +from app.api.routes.utils import latency_summary, failures_counter +from app.models import Resource + + +@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)) From f1455b92ecf802aeac21158ab9570a4bc10a273d Mon Sep 17 00:00:00 2001 From: Joel Gerber Date: Sat, 26 Oct 2019 17:15:26 -0400 Subject: [PATCH 07/10] Split search route into separate module --- app/api/routes/__init__.py | 1 + app/api/routes/old_routes.py | 72 +--------------------------------- app/api/routes/searching.py | 75 ++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 71 deletions(-) create mode 100644 app/api/routes/searching.py diff --git a/app/api/routes/__init__.py b/app/api/routes/__init__.py index 044765ee..289ed609 100644 --- a/app/api/routes/__init__.py +++ b/app/api/routes/__init__.py @@ -3,3 +3,4 @@ from app.api.routes import resource_voting from app.api.routes import categories from app.api.routes import languages +from app.api.routes import searching diff --git a/app/api/routes/old_routes.py b/app/api/routes/old_routes.py index 5ff8d620..77938b91 100644 --- a/app/api/routes/old_routes.py +++ b/app/api/routes/old_routes.py @@ -1,8 +1,6 @@ -from algoliasearch.exceptions import AlgoliaException, AlgoliaUnreachableHostException -from flask import redirect, request +from flask import request import app.utils as utils -from app import Config, index from app.api import bp from app.api.auth import is_user_oc_member from app.api.routes.utils import failures_counter, latency_summary, logger @@ -10,13 +8,6 @@ from app.models import Key -@latency_summary.time() -@failures_counter.count_exceptions() -@bp.route('/search', methods=['GET']) -def search(): - return search_results() - - @latency_summary.time() @failures_counter.count_exceptions() @bp.route('/apikey', methods=['POST'], endpoint='apikey') @@ -49,64 +40,3 @@ def apikey(): except Exception as e: logger.exception(e) return utils.standardize_response(status_code=500) - - -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 isinstance(paid, str): - 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 isinstance(category, str): - filters.append( - f"category:{category}" - ) - - # Filter on languages - if isinstance(languages, list): - for i, _ in enumerate(languages): - languages[i] = f"languages:{languages[i]}" - - # 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) - return utils.standardize_response(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)) diff --git a/app/api/routes/searching.py b/app/api/routes/searching.py new file mode 100644 index 00000000..2530e36f --- /dev/null +++ b/app/api/routes/searching.py @@ -0,0 +1,75 @@ +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.utils import latency_summary, failures_counter, 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 isinstance(paid, str): + 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 isinstance(category, str): + filters.append( + f"category:{category}" + ) + + # Filter on languages + if isinstance(languages, list): + for i, _ in enumerate(languages): + languages[i] = f"languages:{languages[i]}" + + # 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) + return utils.standardize_response(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 253eabd28956a8ce5085fbf9525b195ec6e0de0a Mon Sep 17 00:00:00 2001 From: Joel Gerber Date: Sat, 26 Oct 2019 17:16:26 -0400 Subject: [PATCH 08/10] Split api key route into separate module --- app/api/routes/__init__.py | 2 +- app/api/routes/{old_routes.py => api_key.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename app/api/routes/{old_routes.py => api_key.py} (100%) diff --git a/app/api/routes/__init__.py b/app/api/routes/__init__.py index 289ed609..5f40f51d 100644 --- a/app/api/routes/__init__.py +++ b/app/api/routes/__init__.py @@ -1,4 +1,4 @@ -from app.api.routes import old_routes +from app.api.routes import api_key from app.api.routes import resources from app.api.routes import resource_voting from app.api.routes import categories diff --git a/app/api/routes/old_routes.py b/app/api/routes/api_key.py similarity index 100% rename from app/api/routes/old_routes.py rename to app/api/routes/api_key.py From 61678bd67d0581d6939bf178efd373785e82b198 Mon Sep 17 00:00:00 2001 From: Joel Gerber Date: Sat, 26 Oct 2019 17:24:54 -0400 Subject: [PATCH 09/10] Further split apart resources routes into retrieval and update modules --- app/api/routes/__init__.py | 5 +- app/api/routes/resource_retrieval.py | 111 ++++++++++++ app/api/routes/resource_updating.py | 124 ++++++++++++++ app/api/routes/resources.py | 247 --------------------------- app/api/routes/utils.py | 19 +++ 5 files changed, 257 insertions(+), 249 deletions(-) create mode 100644 app/api/routes/resource_retrieval.py create mode 100644 app/api/routes/resource_updating.py delete mode 100644 app/api/routes/resources.py diff --git a/app/api/routes/__init__.py b/app/api/routes/__init__.py index 5f40f51d..72b334ff 100644 --- a/app/api/routes/__init__.py +++ b/app/api/routes/__init__.py @@ -1,6 +1,7 @@ from app.api.routes import api_key -from app.api.routes import resources -from app.api.routes import resource_voting from app.api.routes import categories from app.api.routes import languages +from app.api.routes import resource_retrieval +from app.api.routes import resource_updating +from app.api.routes import resource_voting from app.api.routes import searching diff --git a/app/api/routes/resource_retrieval.py b/app/api/routes/resource_retrieval.py new file mode 100644 index 00000000..47648201 --- /dev/null +++ b/app/api/routes/resource_retrieval.py @@ -0,0 +1,111 @@ +from datetime import datetime + +from dateutil import parser +from flask import request, redirect +from sqlalchemy import func, or_ + +from app import utils as utils +from app.api import bp +from app.api.routes.utils import latency_summary, failures_counter, logger +from app.models import Resource, Language, Category +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 `language` 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. + language = request.args.get('language') + category = request.args.get('category') + updated_after = request.args.get('updated_after') + paid = request.args.get('paid') + + q = Resource.query + + # Filter on language + if language: + q = q.filter( + Resource.languages.any( + Language.name.ilike(language) + ) + ) + + # 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..4bae18d3 --- /dev/null +++ b/app/api/routes/resource_updating.py @@ -0,0 +1,124 @@ +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.utils import ( + failures_counter, get_attributes, latency_summary, logger) +from app.api.validations import requires_body, validate_resource +from app.models import Resource + + +@latency_summary.time() +@failures_counter.count_exceptions() +@bp.route('/resources', methods=['POST'], endpoint='create_resource') +@requires_body +@authenticate +def post_resources(): + validation_errors = validate_resource(request) + + if validation_errors: + return utils.standardize_response(payload=validation_errors, status_code=422) + return create_resource(request.get_json(), db) + + +@latency_summary.time() +@failures_counter.count_exceptions() +@bp.route('/resources/', methods=['PUT'], endpoint='update_resource') +@requires_body +@authenticate +def put_resource(id): + validation_errors = validate_resource(request, id) + + if validation_errors: + return utils.standardize_response(payload=validation_errors, status_code=422) + return update_resource(id, request.get_json(), db) + + +def create_resource(json, db): + langs, categ = get_attributes(json) + new_resource = Resource( + name=json.get('name'), + url=json.get('url'), + category=categ, + languages=langs, + paid=json.get('paid'), + notes=json.get('notes')) + + try: + db.session.add(new_resource) + db.session.commit() + index.save_object(new_resource.serialize_algolia_search) + + except (AlgoliaUnreachableHostException, AlgoliaException) as e: + logger.exception(e) + print(f"Algolia failed to index new resource '{new_resource.name}'") + + 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) + + return utils.standardize_response(payload=dict(data=new_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') + + db.session.commit() + + try: + index.partial_update_object(index_object) + + except (AlgoliaUnreachableHostException, AlgoliaException) as e: + logger.exception(e) + print(f"Algolia failed to update index for resource '{resource.name}'") + + 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) diff --git a/app/api/routes/resources.py b/app/api/routes/resources.py deleted file mode 100644 index 9c31a61f..00000000 --- a/app/api/routes/resources.py +++ /dev/null @@ -1,247 +0,0 @@ -from datetime import datetime - -from algoliasearch.exceptions import AlgoliaUnreachableHostException, AlgoliaException -from dateutil import parser -from flask import request, redirect -from sqlalchemy import func, or_ -from sqlalchemy.exc import IntegrityError - -from app import utils as utils, db, index -from app.api import bp -from app.api.auth import authenticate -from app.api.routes.utils import latency_summary, failures_counter, logger -from app.api.validations import requires_body, validate_resource -from app.models import Resource, Language, Category -from configs import Config - - -# 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_resource') -@requires_body -@authenticate -def post_resources(): - validation_errors = validate_resource(request) - - if validation_errors: - return utils.standardize_response(payload=validation_errors, status_code=422) - return create_resource(request.get_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): - validation_errors = validate_resource(request, id) - - if validation_errors: - return utils.standardize_response(payload=validation_errors, status_code=422) - return update_resource(id, request.get_json(), db) - - -# Business Logic -def get_resources(): - """ - Gets a paginated list of resources. - - If the URL parameters `language` 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. - language = request.args.get('language') - category = request.args.get('category') - updated_after = request.args.get('updated_after') - paid = request.args.get('paid') - - q = Resource.query - - # Filter on language - if language: - q = q.filter( - Resource.languages.any( - Language.name.ilike(language) - ) - ) - - # 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 create_resource(json, db): - langs, categ = get_attributes(json) - new_resource = Resource( - name=json.get('name'), - url=json.get('url'), - category=categ, - languages=langs, - paid=json.get('paid'), - notes=json.get('notes')) - - try: - db.session.add(new_resource) - db.session.commit() - index.save_object(new_resource.serialize_algolia_search) - - except (AlgoliaUnreachableHostException, AlgoliaException) as e: - logger.exception(e) - print(f"Algolia failed to index new resource '{new_resource.name}'") - - 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) - - return utils.standardize_response(payload=dict(data=new_resource.serialize)) - - -def get_resource(id): - resource = Resource.query.get(id) - - if resource: - return utils.standardize_response(payload=dict(data=(resource.serialize))) - - return redirect('/404') - - -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') - - db.session.commit() - - try: - index.partial_update_object(index_object) - - except (AlgoliaUnreachableHostException, AlgoliaException) as e: - logger.exception(e) - print(f"Algolia failed to update index for resource '{resource.name}'") - - 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 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/utils.py b/app/api/routes/utils.py index 64a19199..30ec7df8 100644 --- a/app/api/routes/utils.py +++ b/app/api/routes/utils.py @@ -1,7 +1,26 @@ from prometheus_client import Counter, Summary from app import utils as utils +from app.models import Category, Language failures_counter = Counter('my_failures', 'Number of exceptions raised') latency_summary = Summary('request_latency_seconds', 'Length of request') logger = utils.setup_logger('routes_logger') + + +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 From 1dcb62d08dbcc7d81f286e725d2c3bd6a3cf3eaa Mon Sep 17 00:00:00 2001 From: Joel Gerber Date: Sat, 26 Oct 2019 17:27:44 -0400 Subject: [PATCH 10/10] Provide package docstring --- app/api/routes/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/api/routes/__init__.py b/app/api/routes/__init__.py index 72b334ff..00ff604b 100644 --- a/app/api/routes/__init__.py +++ b/app/api/routes/__init__.py @@ -1,3 +1,10 @@ +""" +The `routes` module is responsible for setting up HTTP routes on the API Flask +blueprint. + +Each module imported below will configure the API blueprint automatically on import. +""" + from app.api.routes import api_key from app.api.routes import categories from app.api.routes import languages