diff --git a/Makefile b/Makefile index 7eaea23..9a352c3 100644 --- a/Makefile +++ b/Makefile @@ -1,2 +1,2 @@ requirements: - pipreqs + pipreqs --force diff --git a/app/routes/__init__.py b/api/__init__.py similarity index 100% rename from app/routes/__init__.py rename to api/__init__.py diff --git a/api/app.py b/api/app.py new file mode 100755 index 0000000..073218d --- /dev/null +++ b/api/app.py @@ -0,0 +1,12 @@ +#!/bin/python3 +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from flask_marshmallow import Marshmallow + +app = Flask(__name__) +app.config.from_object('api.config') +db = SQLAlchemy(app) +ma = Marshmallow(app) + +import api.models.trainer +import api.routes.routes diff --git a/api/config.py b/api/config.py new file mode 100644 index 0000000..7461cfb --- /dev/null +++ b/api/config.py @@ -0,0 +1,2 @@ +SQLALCHEMY_DATABASE_URI = 'sqlite:///database.db' +SQLALCHEMY_TRACK_MODIFICATIONS = False diff --git a/api/models/trainer.py b/api/models/trainer.py new file mode 100644 index 0000000..91474c1 --- /dev/null +++ b/api/models/trainer.py @@ -0,0 +1,44 @@ +from api.app import db, ma +from werkzeug.security import generate_password_hash +from enum import Enum + +teams = ( + "Team Valor", + "Team Instinct", + "Team Mystic" + ) + +class InvalidTeam(Exception): + pass + +# modelo do Trainer para o SQLAlchemy +class Trainer(db.Model): + __tablename__ = "trainers" + + id = db.Column(db.Integer, primary_key=True, unique=True, autoincrement=True) + nickname = db.Column(db.String(20), nullable=False, index=True) + first_name = db.Column(db.String(30), nullable=False) + last_name = db.Column(db.String(30), nullable=False) + email = db.Column(db.String(60), unique=True, nullable=False) + password = db.Column(db.String(200), nullable=False) + team = db.Column(db.String(10), nullable=False) + pokemons_owned = db.Column(db.Integer, default=0) + + def __init__(self, nickname, first_name, last_name, email, password, team): + if team not in teams: + raise InvalidTeam() + self.nickname = nickname + self.first_name = first_name + self.last_name = last_name + self.email = email + self.password = generate_password_hash(password) + self.team = team + + +# schema do Marshmallow +class TrainerSchema(ma.Schema): + class Meta: + fields = ('id', 'nickname', 'first_name', 'last_name', 'email', 'team', 'pokemons_owned') + +trainer_schema = TrainerSchema() +trainer_schemas = TrainerSchema(many=True) diff --git a/api/routes/__init__.py b/api/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/routes/routes.py b/api/routes/routes.py new file mode 100644 index 0000000..b322a4e --- /dev/null +++ b/api/routes/routes.py @@ -0,0 +1,12 @@ +from flask import request +from sqlite3 import ProgrammingError, IntegrityError +from api.app import app, db +from api.views import trainer + +@app.route('/trainer', methods=['GET']) +def route_get_trainers(): + return trainer.get_trainers() + +@app.route('/trainer', methods=['POST']) +def route_create_trainer(): + return trainer.post_trainer() diff --git a/api/views/__init__.py b/api/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/views/trainer.py b/api/views/trainer.py new file mode 100644 index 0000000..fa56982 --- /dev/null +++ b/api/views/trainer.py @@ -0,0 +1,80 @@ +from sqlalchemy.exc import NoResultFound, IntegrityError +from api.models.trainer import Trainer, trainer_schema, trainer_schemas, InvalidTeam +from api.app import db +from flask import request, jsonify + +# função auxiliar +def error(code, type, message, http_code=400): + return ({ + "code": code, + "type": type, + "message": message + }, http_code) + +def get_trainers(): + args = request.args + + try: + limit = int(args.get("limit", -1)) + offset = int(args.get("offset", 0)) + except ValueError: + return error(0, "ParsingError", "Couldn't parse parameter as integer") + + if limit < -1 or offset < 0: + return error(1, "ParsingError", "Expected positive integer as parameter") + + nickname = args.get("nickname", "") + nickname_contains = args.get("nickname_contains", "") + + try: + if nickname: + if nickname_contains: + return error(2, "ConflictingParameters", "nickname and nickname_contains are mutually exclusive") + + query = Trainer.query.filter_by(nickname=nickname) + return trainer_schemas.dumps(query.all()) + + else: # se nickname_contains também está vazio, retornará todos trainers + pattern = '%'+nickname_contains+'%' + query = Trainer.query.filter(Trainer.nickname.like(pattern)) + return trainer_schemas.dumps(query.all()) + + except NoResultFound: + return jsonify([]) + +def post_trainer(): + try: + json = request.get_json() + except: + return error(3, "ParsingError", "Failed to parse JSON content") + + if type(json) is not dict: + return error(4, "ParsingError", "Expected JSON object as content") + + try: + nickname = json["nickname"] + first_name = json["first_name"] + last_name = json["last_name"] + email = json["email"] + password = json["password"] + team = json["team"] + + trainer = Trainer( + nickname=nickname, + first_name=first_name, + last_name=last_name, + email=email, + password=password, + team=team + ) + + db.session.add(trainer) + db.session.commit() + + return trainer_schema.dump(trainer) + except InvalidTeam: + return error(5, "ParsingError", "Field team is invalid") + except KeyError: + return error(6, "ParsingError", "Missing JSON object fields") + except IntegrityError: + return error(7, "ConflictingResource", "Trainer with the same nickname or email already exists", http_code=500) diff --git a/app/__init__.py b/app/__init__.py deleted file mode 100644 index 62e9225..0000000 --- a/app/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from flask import Flask - -app = Flask(__name__) - -app.debug = True - -from .routes import routes diff --git a/app/database.py b/app/database.py deleted file mode 100644 index 34cedc3..0000000 --- a/app/database.py +++ /dev/null @@ -1,55 +0,0 @@ -from .trainer import Trainer -import sqlite3 - -class Database: - def __init__(self, db_file): - self.db_file = db_file - - def create_trainers_table(self): - con = sqlite3.connect(self.db_file) - - con.execute(""" - CREATE TABLE IF NOT EXISTS Trainers - ( - id INT UNSIGNED UNIQUE, - nickname TINYTEXT PRIMARY KEY, - first_name TINYTEXT, - last_name TINYTEXT, - email TINYTEXT, - password TINYTEXT, - team TINYTEXT, - pokemons_owned INT UNSIGNED - ) - """) - - con.commit() - con.close() - - def __get_trainers(self, sql): - con = sqlite3.connect(self.db_file) - trainers = [] - for row in con.execute(*sql): - trainers.append(Trainer(*row).__dict__) - con.close() - return trainers - - def get_trainer_by_nickname(self, nickname, limit, offset): - return self.__get_trainers(("SELECT * FROM Trainers WHERE nickname = ? LIMIT ? OFFSET ?", (nickname, limit, offset))) - - def get_trainers_by_nickname_contains(self, contains, limit, offset): - return self.__get_trainers(("SELECT * FROM Trainers WHERE nickname LIKE ? LIMIT ? OFFSET ?", ("%" + contains + "%", limit, offset))) - - def insert_trainer(self, trainer): - con = sqlite3.connect(self.db_file) - - con.execute("INSERT INTO Trainers VALUES (?, ?, ?, ?, ?, ?, ?, ?)", - (trainer.id, trainer.nickname, - trainer.first_name, trainer.last_name, - trainer.email, trainer.password, - trainer.team, trainer.pokemons_owned) - ) - - con.commit() - con.close() - -db = Database("database.db") diff --git a/app/routes/routes.py b/app/routes/routes.py deleted file mode 100644 index b939fcc..0000000 --- a/app/routes/routes.py +++ /dev/null @@ -1,39 +0,0 @@ -from flask import request, jsonify -from app import app -from app.database import db - -@app.route('/trainer', methods=['GET']) -def route_get_trainers(): - args = request.args - - try: - limit = int(args.get("limit", -1)) - offset = int(args.get("offset", 0)) - except ValueError: - return { - "code": 1, - "type": "Integer parsing error", - "message": "Couldn't parse parameter as integer" - }, 500 - - if limit < -1 or offset < 0: - return { - "code": 2, - "type": "Integer parsing error", - "message": "Expected positive integer as parameter" - }, 500 - - nickname = args.get("nickname", "") - nickname_contains = args.get("nickname_contains", "") - - if nickname and nickname_contains: - return { - "code": 3, - "type": "Invalid parameter", - "message": "Parameters \"nickname\" and \"nickname_contains\" are mutually exclusive" - }, 500 - - if nickname: - return jsonify(db.get_trainer_by_nickname(nickname, limit, offset)) - else: - return jsonify(db.get_trainers_by_nickname_contains(nickname_contains, limit, offset)) diff --git a/app/trainer.py b/app/trainer.py deleted file mode 100644 index 764a8fd..0000000 --- a/app/trainer.py +++ /dev/null @@ -1,18 +0,0 @@ -class Trainer: - def __init__(self, - id, - nickname, - first_name, - last_name, - email, - password, - team, - pokemons_owned): - self.id = id - self.nickname = nickname - self.first_name = first_name - self.last_name = last_name - self.email = email - self.password = password - self.team = team - self.pokemons_owned = pokemons_owned diff --git a/create_db.py b/create_db.py index c856b05..d24f94e 100755 --- a/create_db.py +++ b/create_db.py @@ -1,5 +1,3 @@ #!/bin/python3 -from app.database import db -from app.trainer import Trainer - -db.create_trainers_table() +from api.app import db +db.create_all() diff --git a/requirements.txt b/requirements.txt index e433f90..74206ce 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,5 @@ +flask_marshmallow==0.14.0 +Flask_SQLAlchemy==2.5.1 +Werkzeug==2.0.2 Flask==2.0.2 +SQLAlchemy==1.4.25 diff --git a/run.py b/run.py deleted file mode 100755 index ceac1f9..0000000 --- a/run.py +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/python3 -from app import app - -if __name__ == "__main__": - app.run()