diff --git a/README.md b/README.md index 0076c7a..854f06d 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Os endpoints que já estão funcionando marcados: - [X] POST /trainer - [X] POST /trainer/authenticate - [X] GET /trainer/{trainerId} -- [ ] GET /trainer/{trainerId}/pokemon -- [ ] POST /trainer/{trainerId}/pokemon +- [X] GET /trainer/{trainerId}/pokemon +- [X] POST /trainer/{trainerId}/pokemon - [ ] GET /trainer/{trainerId}/pokemon/{pokemonId} - [ ] DELETE /trainer/{trainerId}/pokemon/{pokemonId} diff --git a/api/app.py b/api/app.py index 073218d..3213595 100755 --- a/api/app.py +++ b/api/app.py @@ -9,4 +9,5 @@ db = SQLAlchemy(app) ma = Marshmallow(app) import api.models.trainer +import api.models.pokemon_owned import api.routes.routes diff --git a/api/models/__init__.py b/api/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/models/pokemon_owned.py b/api/models/pokemon_owned.py new file mode 100644 index 0000000..e461868 --- /dev/null +++ b/api/models/pokemon_owned.py @@ -0,0 +1,24 @@ +from api.app import db, ma +from .trainer import * + +class PokemonOwned(db.Model): + __tablename__ = "pokemons_owned" + + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + name = db.Column(db.Text(50), unique=True, index=True, nullable=False) + level = db.Column(db.Integer) + pokemon_id = db.Column(db.Integer) + trainer_id = db.Column(db.Integer, db.ForeignKey("trainers.id"), index=True) + + def __init__(self, name, level, pokemon_id, trainer_id): + self.name = name + self.level = level + self.pokemon_id = pokemon_id + self.trainer_id = trainer_id + +class PokemonOwnedSchema(ma.Schema): + class Meta: + fields = ('id', 'name', 'level', 'pokemon_data') + +pokemon_owned_schema = PokemonOwnedSchema() +pokemon_owned_schemas = PokemonOwnedSchema(many=True) diff --git a/api/models/trainer.py b/api/models/trainer.py index fac7329..050dc7c 100644 --- a/api/models/trainer.py +++ b/api/models/trainer.py @@ -14,7 +14,7 @@ class InvalidTeam(Exception): class Trainer(db.Model): __tablename__ = "trainers" - id = db.Column(db.Integer, primary_key=True, unique=True, autoincrement=True) + id = db.Column(db.Integer, primary_key=True, autoincrement=True) nickname = db.Column(db.String(20), unique=True, nullable=False, index=True) first_name = db.Column(db.String(30), nullable=False) last_name = db.Column(db.String(30), nullable=False) @@ -22,6 +22,7 @@ class Trainer(db.Model): password = db.Column(db.String(200), nullable=False) team = db.Column(db.String(10), nullable=False) pokemons_owned = db.Column(db.Integer, default=0) + pokemons_list = db.relationship("PokemonOwned", lazy="dynamic") def __init__(self, nickname, first_name, last_name, email, password, team): if team not in teams: diff --git a/api/routes/routes.py b/api/routes/routes.py index 8956b32..92c805f 100644 --- a/api/routes/routes.py +++ b/api/routes/routes.py @@ -1,20 +1,31 @@ +from api.app import app +from api.views import trainer, pokemon_owned, helper, errors from flask import request -from sqlite3 import ProgrammingError, IntegrityError -from api.app import app, db -from api.views import trainer +import asyncio -@app.route('/trainer/', methods=['GET']) +@app.route("/trainer//", methods=["GET"]) def route_get_trainer(trainerId): return trainer.get_trainer(trainerId) -@app.route('/trainer', methods=['GET']) +@app.route("/trainer", methods=["GET"]) def route_get_trainers(): return trainer.get_trainers() -@app.route('/trainer', methods=['POST']) +@app.route("/trainer", methods=["POST"]) def route_create_trainer(): return trainer.post_trainer() -@app.route("/trainer/authenticate", methods=['POST']) +@app.route("/trainer/authenticate", methods=["POST"]) def route_auth_trainer(): return trainer.auth_trainer() + +@app.route("/trainer//pokemon", methods=["GET"]) +def route_get_pokemons_owned(trainerId): + return asyncio.run(pokemon_owned.get_pokemons_owned(trainerId)) + +@app.route("/trainer//pokemon", methods=["POST"]) +@helper.token_required +def route_post_pokemons_owned(trainer, trainerId): + if trainer.id != trainerId: + return errors.ForbiddenError("Trainer id mismatch") + return pokemon_owned.post_pokemon_owned(trainerId) diff --git a/api/views/errors.py b/api/views/errors.py index 709bb6a..7934cbf 100644 --- a/api/views/errors.py +++ b/api/views/errors.py @@ -16,3 +16,9 @@ def ConflictingResources(message): def AuthenticationFailure(message): return error(4, "AuthenticationFailure", message, http_code=401) + +def ForbiddenError(message): + return error(5, "ForbiddenError", message, http_code=403) + +def FetchError(message): + return error(6, "FetchError", message, http_code=500) diff --git a/api/views/helper.py b/api/views/helper.py new file mode 100644 index 0000000..9321245 --- /dev/null +++ b/api/views/helper.py @@ -0,0 +1,65 @@ +from functools import wraps +from flask import request +from api.models.trainer import Trainer +from .errors import AuthenticationFailure +from api.app import app +import requests +import json +import jwt + +class HTTPError(Exception): + def __init__(self, message): + self.message = message + +class TrainerNotFound(Exception): + pass + +def get_trainer_fail(id): + try: + trainer = Trainer.query.get(id) + if trainer is None: + raise TrainerNotFound() + return trainer + except: + raise TrainerNotFound() + +def get_trainer_by_nick_fail(nickname): + try: + trainer = Trainer.query.filter_by(nickname=nickname).one() + if trainer is None: + raise TrainerNotFound() + return trainer + except: + raise TrainerNotFound() + +# authenticação do trainer (decorator) +def token_required(f): + @wraps(f) + def decorated(*args, **kwargs): + try: + token = request.headers["authorization"] + print(token) + print(app.config["SECRET_KEY"]) + data = jwt.decode(token, app.config["SECRET_KEY"], algorithms=["HS256"]) + print(data["username"]) + trainer = get_trainer_by_nick_fail(data["username"]) + except (TypeError, KeyError): + return AuthenticationFailure("JWT token required") + except: + return AuthenticationFailure("JWT token is invalid or expired") + + return f(trainer, *args, **kwargs) + return decorated + +# seguintes funções puxam informações da pokeapi +def set_pokemon_data(pokemon): + response = requests.get("https://pokeapi.co/api/v2/pokemon/{}".format(pokemon.pokemon_id)) + if response.status_code != 200: + raise HTTPError("Could not fetch pokemon with id {}".format(pokemon.pokemon_id)) + pokemon.pokemon_data = json.loads(response.text) + +async def async_set_pokemon_data(session, pokemon): + response = await session.get("https://pokeapi.co/api/v2/pokemon/{}".format(pokemon.pokemon_id)) + if response.status != 200: + raise HTTPError("Could not fetch pokemon with id {}".format(pokemon.pokemon_id)) + pokemon.pokemon_data = json.loads(await response.text()) diff --git a/api/views/parse_args.py b/api/views/parse_args.py new file mode 100644 index 0000000..1b28622 --- /dev/null +++ b/api/views/parse_args.py @@ -0,0 +1,33 @@ +from flask import request + +class ParsingException(Exception): + def __init__(self, message): + self.message = message + +def parse_int(name, minimum): + try: + result = int(request.args.get(name, minimum)) + except ValueError: + raise ParsingException("Couldn't parse {} as integer".format(name)) + + if result < minimum: + raise ParsingException("{} must be greater than {}".format(name, minimum)) + + return result + +def parse_limit(): + return parse_int("limit", -1) + +def parse_offset(): + return parse_int("offset", 0) + +def parse_json_obj(): + try: + json = request.get_json() + except: + raise ParsingException("Failed to parse JSON body") + + if type(json) is not dict: + raise ParsingException("Expected JSON object as body") + + return json diff --git a/api/views/pokemon_owned.py b/api/views/pokemon_owned.py new file mode 100644 index 0000000..8dbd8b1 --- /dev/null +++ b/api/views/pokemon_owned.py @@ -0,0 +1,64 @@ +from api.models.pokemon_owned import pokemon_owned_schema, pokemon_owned_schemas, PokemonOwned +from api.app import db +from .parse_args import parse_limit, parse_offset, ParsingException, parse_json_obj +from .errors import ParsingError, FetchError, ConflictingResources +from .helper import TrainerNotFound, HTTPError, get_trainer_fail, set_pokemon_data, async_set_pokemon_data +from aiohttp import ClientSession +from asyncio import create_task, gather +from sqlalchemy.exc import IntegrityError + +def post_pokemon_owned(trainer_id): + try: + json = parse_json_obj() + pokemon_id = json["pokemon_id"] + name = json["name"] + level = json["level"] + except ParsingException as e: + return ParsingError(e.message) + except KeyError: + return ParsingError("Missing JSON object fields") + + pokemon = PokemonOwned( + name=name, + level=level, + pokemon_id=pokemon_id, + trainer_id=trainer_id + ) + + try: + set_pokemon_data(pokemon) + + db.session.add(pokemon) + db.session.commit() + except HTTPError as e: + return FetchError(e.message) + except IntegrityError: + return ConflictingResources("Trainer already has another pokemon with the same name") + + return (pokemon_owned_schema.dump(pokemon), 201) + +async def get_pokemons_owned(trainer_id): + try: + limit = parse_limit() + offset = parse_offset() + except ParsingException as e: + return ParsingError(e.message) + + try: + trainer = get_trainer_fail(trainer_id) + except TrainerNotFound: + return "", 404 + + pokemons = trainer.pokemons_list.limit(limit).offset(offset).all() + + async with ClientSession() as session: + tasks = [] + for pokemon in pokemons: + task = create_task(async_set_pokemon_data(session, pokemon)) + tasks.append(task) + try: + await gather(*tasks) + except HTTPError as e: + return FetchError(e.message) + + return pokemon_owned_schemas.dumps(pokemons) diff --git a/api/views/trainer.py b/api/views/trainer.py index 41ba1bd..59299cc 100644 --- a/api/views/trainer.py +++ b/api/views/trainer.py @@ -3,7 +3,8 @@ from api.models.trainer import Trainer, trainer_schema, trainer_schemas, Invalid from api.app import db from api.app import app from flask import request, jsonify -from .errors import ParsingError, ConflictingParameters, ConflictingResources, AuthenticationFailure +from .errors import * +from .parse_args import parse_limit, parse_offset, ParsingException, parse_json_obj from werkzeug.security import check_password_hash import datetime import jwt @@ -12,19 +13,16 @@ def get_trainer(id): try: return trainer_schema.dump(Trainer.query.get(id)) except: - return jsonify({}) + return ("", 404) def get_trainers(): args = request.args try: - limit = int(args.get("limit", -1)) - offset = int(args.get("offset", 0)) - except ValueError: - return ParsingError("Couldn't parse parameter as integer") - - if limit < -1 or offset < 0: - return ParsingError("Expected positive integer as parameter") + limit = parse_limit() + offset = parse_offset() + except ParsingException as e: + return ParsingError(e.message) nickname = args.get("nickname", "") nickname_contains = args.get("nickname_contains", "") @@ -53,14 +51,8 @@ def get_trainer_by_email(email): def post_trainer(): try: - json = request.get_json() - except: - return ParsingError("Failed to parse JSON body") + json = parse_json_obj() - if type(json) is not dict: - return ParsingError("Expected JSON object as body") - - try: nickname = json["nickname"] first_name = json["first_name"] last_name = json["last_name"] @@ -81,25 +73,22 @@ def post_trainer(): db.session.commit() return trainer_schema.dump(trainer) + except ParsingException as e: + return ParsingError(e.message) except InvalidTeam: return ParsingError("Field team is invalid") except KeyError: return ParsingError("Missing JSON object fields") except IntegrityError: - return ConflictingResources("Trainer with the same nickname or email already exists", http_code=500) + return ConflictingResources("Trainer with the same nickname or email already exists") def auth_trainer(): try: - auth = request.get_json() - except: - return ParsingError("Failed to parse JSON body") - - if type(auth) is not dict: - return ParsingError("Expected JSON object as body") - - try: + auth = parse_json_obj() email = auth["email"] password = auth["password"] + except ParsingException as e: + return ParsingError(e.message) except KeyError: return AuthenticationFailure("Login required") @@ -113,7 +102,7 @@ def auth_trainer(): "username": trainer.nickname, "exp": datetime.datetime.now() + datetime.timedelta(hours=12) }, - app.config["SECRET_KEY"]) + app.config["SECRET_KEY"], algorithm="HS256") return jsonify( { "id": trainer.id,