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/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}

View File

@ -9,4 +9,5 @@ db = SQLAlchemy(app)
ma = Marshmallow(app)
import api.models.trainer
import api.models.pokemon_owned
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):
__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:

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 sqlite3 import ProgrammingError, IntegrityError
from api.app import app, db
from api.views import trainer
import asyncio
@app.route('/trainer/<int:trainerId>', methods=['GET'])
@app.route("/trainer/<int:trainerId>/", 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/<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):
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 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,