diff --git a/docker-compose-github.yml b/docker-compose-github.yml deleted file mode 100644 index df76df2..0000000 --- a/docker-compose-github.yml +++ /dev/null @@ -1,31 +0,0 @@ -version: '3.7' - -services: - - flask: - build: - context: ./flask - dockerfile: Dockerfile - volumes: - - './flask:/usr/src/app/' - ports: - - 5001:5000 - environment: - - FLASK_ENV=development - - APP_SETTINGS=app.config.DevelopmentConfig - - DATABASE_URL=postgres://postgres:postgres@postgres:5432/jeopardy - - DATABASE_TEST_URL=postgres://postgres:postgres@postgres:5432/jeopardy_test - depends_on: - - postgres - - postgres: - build: - context: ./postgres - dockerfile: Dockerfile - volumes: - - ./postgres/init:/docker-entrypoint-initdb.d - ports: - - 5435:5432 - environment: - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=postgres diff --git a/flask/Dockerfile b/flask/Dockerfile index 10153c2..02b92a3 100644 --- a/flask/Dockerfile +++ b/flask/Dockerfile @@ -1,25 +1,23 @@ -# base image -FROM python:3.7.2-alpine +FROM python:3.11-alpine -# install dependencies RUN apk update && \ - apk add --virtual build-deps gcc python-dev musl-dev && \ - apk add postgresql-dev && \ - apk add netcat-openbsd + apk add --no-cache \ + gcc \ + python3-dev \ + musl-dev \ + postgresql-dev \ + libpq \ + netcat-openbsd -# set working directory WORKDIR /usr/src/app -# add and install requirements -COPY ./requirements.txt /usr/src/app/requirements.txt -RUN pip install -r requirements.txt +COPY requirements.txt . +RUN pip install --upgrade pip && \ + pip install -r requirements.txt -# add entrypoint.sh -COPY ./entrypoint.sh /usr/src/app/entrypoint.sh -RUN ["chmod", "+x", "/usr/src/app/entrypoint.sh"] +COPY entrypoint.sh . +RUN chmod +x entrypoint.sh -# add app -COPY . /usr/src/app +COPY . . -# run server -CMD ["/usr/src/app/entrypoint.sh"] +CMD ["./entrypoint.sh"] diff --git a/flask/app/__init__.py b/flask/app/__init__.py index 5b2dbca..093f932 100644 --- a/flask/app/__init__.py +++ b/flask/app/__init__.py @@ -5,6 +5,7 @@ from flask_restful import Resource, Api from flask_sqlalchemy import SQLAlchemy from flask_caching import Cache +from flask_cors import CORS # instantiate the db @@ -18,6 +19,8 @@ def create_app(script_info=None): # instantiate the app app = Flask(__name__) + CORS(app, resources={r"/*": {"origins": "*"}}) + # set config app_settings = os.getenv('APP_SETTINGS') app.config.from_object(app_settings) @@ -39,6 +42,8 @@ def create_app(script_info=None): app.register_blueprint(clues_blueprint) from app.api.endpoints.categories import categories_blueprint app.register_blueprint(categories_blueprint) + from app.api.endpoints.board import board_blueprint + app.register_blueprint(board_blueprint) # shell context for flask cli @app.shell_context_processor diff --git a/flask/app/api/endpoints/board.py b/flask/app/api/endpoints/board.py new file mode 100644 index 0000000..5a17a2f --- /dev/null +++ b/flask/app/api/endpoints/board.py @@ -0,0 +1,56 @@ +from flask import Blueprint, request + +from flask_restful import Resource, Api +from sqlalchemy import asc, desc, func, select, text +from sqlalchemy.orm import aliased +from app import db, cache +from app.api.models import Clues + + +board_blueprint = Blueprint('board', __name__) +api = Api(board_blueprint) + + +def get_board(cat_limit): + + valid_categories = ( + db.session.query(Clues.category) + .group_by(Clues.category) + .having(func.count(Clues.id) == 5) + .order_by(func.random()) + .limit(cat_limit) + .all() + ) + + category_names = [cat.category for cat in valid_categories] + + # Query all clues from these categories at once + board = ( + Clues.query + .filter(Clues.category.in_(category_names)) + .order_by(asc(Clues.value)) # Sort by value first + .all() + ) + + return [clue.to_json() for clue in board] + +class BoardRandom(Resource): + def get(self): + # handle the int queries + try: + cat_limit = min(int(request.args.get('cat_limit', 6)), 8) + except ValueError: + return { + 'status' : 'failure', + 'error': repr('cat_limit and/or clue_limit query parameter is not a valid number') + }, 400 + + + result = get_board(cat_limit) + + return { + 'status' : 'success', + 'data': result + }, 200 + +api.add_resource(BoardRandom, "/random_board") diff --git a/flask/app/api/endpoints/clues.py b/flask/app/api/endpoints/clues.py index ad50ec8..6dd6d0d 100644 --- a/flask/app/api/endpoints/clues.py +++ b/flask/app/api/endpoints/clues.py @@ -1,14 +1,13 @@ import random from flask import Blueprint, request + from flask_restful import Resource, Api -from sqlalchemy import func, or_ +from sqlalchemy import func from app import db, cache from app.api.models import Clues -from app.api.exceptions import LimitNotANumberError, LimitOverMaxError, \ - OffsetNotANumberError, OrderByInvalidError, \ - SortInvalidError, IdNotFoundError +from app.api.exceptions import LimitNotANumberError, LimitOverMaxError, IdNotFoundError clues_blueprint = Blueprint('clues', __name__) api = Api(clues_blueprint) @@ -27,30 +26,13 @@ def get_clues(limit, offset, order_by, sort): 'response' : Clues.response } - #### - # Checking for exceptions - #### - - digits = '0123456789' - for c in str(limit): - if digits.find(c) == -1: - raise LimitNotANumberError('"Limit" query parameter is not a valid number') - if int(limit) > 1000: - raise LimitOverMaxError('Requested too many resources. Maximum "limit" is 1000') - for c in str(offset): - if digits.find(c) == -1: - raise OffsetNotANumberError('"offset" query parameter is not a valid number') - if string_to_col.get(order_by, 'none') == 'none': - raise OrderByInvalidError('"order_by" query parameter is invalid') - if sort != 'asc' and sort != 'desc': - raise SortInvalidError('"sort" query parameter must be "asc" or "desc"') - if sort == 'asc': return [clue.to_json() for clue in Clues.query .order_by(string_to_col[order_by]) .limit(limit) .offset(offset) .all()] + if sort == 'desc': return [clue.to_json() for clue in Clues.query .order_by(string_to_col[order_by].desc()) @@ -62,18 +44,28 @@ def get_clues(limit, offset, order_by, sort): # TODO: search category and/or value class CluesList(Resource): def get(self): - limit = request.args.get('limit', 50) - offset = request.args.get('offset', 0) - order_by = request.args.get('order_by', 'id') - sort = request.args.get('sort', 'asc') + # handle the int queries try: - result = get_clues(limit, offset, order_by, sort) - except Exception as e: + limit = min(int(request.args.get('limit', 50)), 1000) + offset = request.args.get('offset', 0) + except ValueError: return { 'status' : 'failure', - 'error': repr(e) + 'error': repr('Limit and/or offset query parameter is not a valid number') }, 400 + + order_by = request.args.get('order_by', 'id') + sort = request.args.get('sort', 'asc') + + #handle the string queries + valid_columns = ['id', 'game_id', 'value', 'daily_double', 'round', 'category', 'clue', 'response'] + if order_by not in valid_columns: + return {'status': 'failure', 'error': f'Invalid order_by. Must be one of: {valid_columns}'}, 400 + if sort != 'asc' and sort != 'desc': + return {'status': 'failure', 'error': f'The "sort" query must either be "asc" or "desc"'}, 400 + result = get_clues(limit, offset, order_by, sort) + return { 'status' : 'success', 'data': result @@ -116,9 +108,15 @@ def get_clue_random(limit, category, difficulty): if category is None and difficulty is None: results = [] - max = Clues.query.with_entities(func.max(Clues.id)).first()[0] - usedIds = [0] + helper = Clues.query.with_entities(func.max(Clues.id)).first() + max = None + if not helper: + raise IdNotFoundError('The max doesnt exist or something') + else: + max = helper[0] + usedIds = [0] + randId = 0 randResult = [] diff --git a/flask/app/api/models.py b/flask/app/api/models.py index da56d2f..5f13b18 100644 --- a/flask/app/api/models.py +++ b/flask/app/api/models.py @@ -61,11 +61,11 @@ class Games(db.Model): score3 = db.Column(db.Integer) def __repr__(self): - return f'Game [id = {self.id}, episode_num = {episode_num}, ' + \ - f'season_id = {season_id}, air_date = {air_date}, notes = {notes}, ' + \ - f'contestant1 = {contestant1}, contestant2 = {contestant2}, ' + \ - f'contestant3 = {contestant3}, winner = {winner}, score1 = {score1}, ' + \ - f'score2 = {score2}, score3 = {score3}]' + return f'Game [id = {self.id}, episode_num = {self.episode_num}, ' + \ + f'season_id = {self.season_id}, air_date = {self.air_date}, notes = {self.notes}, ' + \ + f'contestant1 = {self.contestant1}, contestant2 = {self.contestant2}, ' + \ + f'contestant3 = {self.contestant3}, winner = {self.winner}, score1 = {self.score1}, ' + \ + f'score2 = {self.score2}, score3 = {self.score3}]' def to_json(self): return { diff --git a/flask/requirements.txt b/flask/requirements.txt index ae563bd..09b16f5 100644 --- a/flask/requirements.txt +++ b/flask/requirements.txt @@ -1,7 +1,8 @@ -Flask==1.0.2 -Flask-RESTful==0.3.7 -Flask-SQLAlchemy==2.3.2 -psycopg2-binary==2.7.7 -Flask-Caching==1.7.2 -gunicorn==19.9.0 -redis==3.1.0 +Flask==3.0.0 +Flask-RESTful==0.3.10 +Flask-SQLAlchemy==3.1.1 +psycopg[binary]==3.3.2 +Flask-Caching==2.1.0 +gunicorn==21.2.0 +redis==5.0.1 +flask-cors==4.0.0 \ No newline at end of file diff --git a/postgres/init/jeopardy201908021145.sql b/postgres/init/jeopardy201908021145.sql index cb81a17..3c9ce7b 100644 --- a/postgres/init/jeopardy201908021145.sql +++ b/postgres/init/jeopardy201908021145.sql @@ -366827,7 +366827,6 @@ COPY public.clues (id, game_id, value, daily_double, round, category, clue, resp 367364 6314 2000 f DJ! THE "PITS" Both cyanide & laetrile are an extract of them apricot pits \. - -- -- Data for Name: contestants; Type: TABLE DATA; Schema: public; Owner: postgres --