Adicionar 2 rotas

This commit is contained in:
Augusto Gunsch 2021-10-18 16:49:47 -03:00
parent b905a812b9
commit 5fc179d389
No known key found for this signature in database
GPG Key ID: F7EEFE29825C72DC
11 changed files with 230 additions and 36 deletions

View File

@ -20,7 +20,7 @@ Os endpoints que já estão funcionando marcados:
- [X] POST /trainer - [X] POST /trainer
- [X] POST /trainer/authenticate - [X] POST /trainer/authenticate
- [X] GET /trainer/{trainerId} - [X] GET /trainer/{trainerId}
- [ ] GET /trainer/{trainerId}/pokemon - [X] GET /trainer/{trainerId}/pokemon
- [ ] POST /trainer/{trainerId}/pokemon - [X] POST /trainer/{trainerId}/pokemon
- [ ] GET /trainer/{trainerId}/pokemon/{pokemonId} - [ ] GET /trainer/{trainerId}/pokemon/{pokemonId}
- [ ] DELETE /trainer/{trainerId}/pokemon/{pokemonId} - [ ] DELETE /trainer/{trainerId}/pokemon/{pokemonId}

View File

@ -9,4 +9,5 @@ db = SQLAlchemy(app)
ma = Marshmallow(app) ma = Marshmallow(app)
import api.models.trainer import api.models.trainer
import api.models.pokemon_owned
import api.routes.routes import api.routes.routes

0
api/models/__init__.py Normal file
View File

View File

@ -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)

View File

@ -14,7 +14,7 @@ class InvalidTeam(Exception):
class Trainer(db.Model): class Trainer(db.Model):
__tablename__ = "trainers" __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) nickname = db.Column(db.String(20), unique=True, nullable=False, index=True)
first_name = db.Column(db.String(30), nullable=False) first_name = db.Column(db.String(30), nullable=False)
last_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) password = db.Column(db.String(200), nullable=False)
team = db.Column(db.String(10), nullable=False) team = db.Column(db.String(10), nullable=False)
pokemons_owned = db.Column(db.Integer, default=0) 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): def __init__(self, nickname, first_name, last_name, email, password, team):
if team not in teams: if team not in teams:

View File

@ -1,20 +1,31 @@
from api.app import app
from api.views import trainer, pokemon_owned, helper, errors
from flask import request from flask import request
from sqlite3 import ProgrammingError, IntegrityError import asyncio
from api.app import app, db
from api.views import trainer
@app.route('/trainer/<int:trainerId>', methods=['GET']) @app.route("/trainer/<int:trainerId>/", methods=["GET"])
def route_get_trainer(trainerId): def route_get_trainer(trainerId):
return trainer.get_trainer(trainerId) return trainer.get_trainer(trainerId)
@app.route('/trainer', methods=['GET']) @app.route("/trainer", methods=["GET"])
def route_get_trainers(): def route_get_trainers():
return trainer.get_trainers() return trainer.get_trainers()
@app.route('/trainer', methods=['POST']) @app.route("/trainer", methods=["POST"])
def route_create_trainer(): def route_create_trainer():
return trainer.post_trainer() return trainer.post_trainer()
@app.route("/trainer/authenticate", methods=['POST']) @app.route("/trainer/authenticate", methods=["POST"])
def route_auth_trainer(): def route_auth_trainer():
return trainer.auth_trainer() return trainer.auth_trainer()
@app.route("/trainer/<int:trainerId>/pokemon", methods=["GET"])
def route_get_pokemons_owned(trainerId):
return asyncio.run(pokemon_owned.get_pokemons_owned(trainerId))
@app.route("/trainer/<int:trainerId>/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)

View File

@ -16,3 +16,9 @@ def ConflictingResources(message):
def AuthenticationFailure(message): def AuthenticationFailure(message):
return error(4, "AuthenticationFailure", message, http_code=401) 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)

65
api/views/helper.py Normal file
View File

@ -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())

33
api/views/parse_args.py Normal file
View File

@ -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

View File

@ -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)

View File

@ -3,7 +3,8 @@ from api.models.trainer import Trainer, trainer_schema, trainer_schemas, Invalid
from api.app import db from api.app import db
from api.app import app from api.app import app
from flask import request, jsonify 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 from werkzeug.security import check_password_hash
import datetime import datetime
import jwt import jwt
@ -12,19 +13,16 @@ def get_trainer(id):
try: try:
return trainer_schema.dump(Trainer.query.get(id)) return trainer_schema.dump(Trainer.query.get(id))
except: except:
return jsonify({}) return ("", 404)
def get_trainers(): def get_trainers():
args = request.args args = request.args
try: try:
limit = int(args.get("limit", -1)) limit = parse_limit()
offset = int(args.get("offset", 0)) offset = parse_offset()
except ValueError: except ParsingException as e:
return ParsingError("Couldn't parse parameter as integer") return ParsingError(e.message)
if limit < -1 or offset < 0:
return ParsingError("Expected positive integer as parameter")
nickname = args.get("nickname", "") nickname = args.get("nickname", "")
nickname_contains = args.get("nickname_contains", "") nickname_contains = args.get("nickname_contains", "")
@ -53,14 +51,8 @@ def get_trainer_by_email(email):
def post_trainer(): def post_trainer():
try: try:
json = request.get_json() json = parse_json_obj()
except:
return ParsingError("Failed to parse JSON body")
if type(json) is not dict:
return ParsingError("Expected JSON object as body")
try:
nickname = json["nickname"] nickname = json["nickname"]
first_name = json["first_name"] first_name = json["first_name"]
last_name = json["last_name"] last_name = json["last_name"]
@ -81,25 +73,22 @@ def post_trainer():
db.session.commit() db.session.commit()
return trainer_schema.dump(trainer) return trainer_schema.dump(trainer)
except ParsingException as e:
return ParsingError(e.message)
except InvalidTeam: except InvalidTeam:
return ParsingError("Field team is invalid") return ParsingError("Field team is invalid")
except KeyError: except KeyError:
return ParsingError("Missing JSON object fields") return ParsingError("Missing JSON object fields")
except IntegrityError: 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(): def auth_trainer():
try: try:
auth = request.get_json() auth = parse_json_obj()
except:
return ParsingError("Failed to parse JSON body")
if type(auth) is not dict:
return ParsingError("Expected JSON object as body")
try:
email = auth["email"] email = auth["email"]
password = auth["password"] password = auth["password"]
except ParsingException as e:
return ParsingError(e.message)
except KeyError: except KeyError:
return AuthenticationFailure("Login required") return AuthenticationFailure("Login required")
@ -113,7 +102,7 @@ def auth_trainer():
"username": trainer.nickname, "username": trainer.nickname,
"exp": datetime.datetime.now() + datetime.timedelta(hours=12) "exp": datetime.datetime.now() + datetime.timedelta(hours=12)
}, },
app.config["SECRET_KEY"]) app.config["SECRET_KEY"], algorithm="HS256")
return jsonify( return jsonify(
{ {
"id": trainer.id, "id": trainer.id,