Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,32 @@
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from dotenv import load_dotenv
import os


db = SQLAlchemy()
migrate = Migrate()
load_dotenv()


def create_app(test_config=None):
app = Flask(__name__)


app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
if not test_config:
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get(
"SQLALCHEMY_DATABASE_URI")
else:
app.config["TESTING"] = True
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get(
"SQLALCHEMY_TEST_DATABASE_URI")

db.init_app(app)
migrate.init_app(app, db)
from app.models.planet import Planet

from .routes.planet_routes import planet_bp
app.register_blueprint(planet_bp)
Comment on lines +2 to +31

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Good work in setting up the Flask app!

return app
Empty file added app/models/__init__.py
Empty file.
30 changes: 30 additions & 0 deletions app/models/planet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from app import db


class Planet(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String)
description = db.Column(db.String)
color = db.Column(db.String)
Comment on lines +6 to +8

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be able to make planets with no name? To prevent our API from creating a planet with a null name, we can set that column to nullable = False.

name = db.Column(db.String, nullable=False)


def to_json(self):
return {
"id": self.id,
"name": self.name,
"description": self.description,
"color": self.color
}

def update(self, request_body):
self.name = request_body["name"]
self.description = request_body["description"]
self.color = request_body["color"]

@classmethod
def create(cls, request_body):
new_planet = cls(
name=request_body['name'],
description=request_body['description'],
color=request_body['color']
)
return new_planet
Comment on lines +10 to +30

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice use of helper methods and a class method!

2 changes: 0 additions & 2 deletions app/routes.py

This file was deleted.

Empty file added app/routes/__init__.py
Empty file.
15 changes: 15 additions & 0 deletions app/routes/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from app.models.planet import Planet
from flask import abort, make_response, jsonify

# helper function to validate
def validate_planet(planet_id):
try:
planet_id = int(planet_id)
except:
return abort(make_response(jsonify(f"Planet {planet_id} is invalid"), 400))

planet = Planet.query.get(planet_id)

if not planet:
return abort(make_response(jsonify(f"Planet {planet_id} does not exist"), 404))
return planet
Comment on lines +5 to +15

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Helpers look great! 👍 We may want to consider making the validation code as generic as possible to utilize any class and id or renaming this file to helper_planet_routes or planet_routes_helper. Either would be helpful if we expand this project to validate other objects like moons, suns, etc.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A more generic validation helper method would look like:

from flask import make_response, abort

def validate_object(cls, id):
    try:
        id = int(id)
    except:
        return abort(make_response({"message": f"{cls} {id} is invalid"}, 400))

    obj = cls.query.get(id)
    
    if not obj:
        abort(make_response({"message": f"{cls} {id} not found"},404))
    
    return obj

59 changes: 59 additions & 0 deletions app/routes/planet_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from app import db
from app.models.planet import Planet
from flask import Blueprint, jsonify, abort, make_response, request
from .helpers import validate_planet

planet_bp = Blueprint("planet", __name__, url_prefix="/planets")

@planet_bp.route("", methods=["POST"])
def create_planet():
request_body = request.get_json()

new_planet = Planet.create(request_body)

db.session.add(new_planet)
db.session.commit()

return make_response(jsonify(f"Planet {new_planet.name} has been created"), 201)
Comment on lines +8 to +17

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jsonify() is sufficient enough to generate a Response object

@planet_bp.route("", methods=["POST"])
def create_planet():
    request_body = request.get_json()

    new_planet = Planet.create(request_body)

    db.session.add(new_planet)
    db.session.commit()

    return jsonify(f"Planet {new_planet.name} has been created"), 201


@ planet_bp.route("", methods=["GET"])
def get_planets():
planet_query = request.args.get("name")
color_query = request.args.get("color")
if planet_query:
planets = Planet.query.filter_by(name=planet_query)
elif color_query:
planets = Planet.query.filter_by(color=color_query)
else:
planets = Planet.query.all()

planets_response = [planet.to_json() for planet in planets]

return make_response(jsonify(planets_response), 200)
Comment on lines +19 to +32

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work with filtering and using list comprehension! The comment about not needing make_response also applies to this view function.


@ planet_bp.route("/<planet_id>", methods=["GET"])
def get_one_planet(planet_id):
planet = validate_planet(planet_id)
return make_response(jsonify(planet.to_json()), 200)
Comment on lines +34 to +37

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Nice work!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, if a view function returns a dictionary (planet.to_json() in this case), Flask will automatically convert the dictionary into a JSON response object (aka Flask will turn dictionaries into JSON data for us). This means you can leave out make_response() like so:

@planets_bp.route("/<planet_id>", methods=["GET"])
def get_one_planet(planet_id):
    planet = validate_planet(planet_id)
    return planet.to_json(), 200

Here's the documentation with more info:
https://flask.palletsprojects.com/en/2.0.x/quickstart/#apis-with-json


@ planet_bp.route("/<planet_id>", methods=["PUT"])
def update_planet(planet_id):
planet = validate_planet(planet_id)
request_body = request.get_json()

try:
Planet.update(request_body)
db.session.commit()
except KeyError:
return abort(make_response(jsonify("Missing information")), 400)

return make_response(jsonify(f"Planet #{planet.id} successfully updated"), 200)
Comment on lines +39 to +50

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work! We may want to consider validating the request_body data in a separate helper method.


@ planet_bp.route("/<planet_id>", methods=["DELETE"])
def delete_one_planet(planet_id):
planet = validate_planet(planet_id)

db.session.delete(planet)
db.session.commit()

return make_response(jsonify(f"Planet {planet.id} has been deleted"), 200)
Comment on lines +52 to +59

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Comment about not needing make_response also applies.

Empty file added app/tests/__init__.py
Empty file.
39 changes: 39 additions & 0 deletions app/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import pytest
from app import create_app
from app import db
from flask.signals import request_finished
from app.models.planet import Planet


@pytest.fixture
def app():
app = create_app({"TESTING": True})

@request_finished.connect_via(app)
def expire_session(sender, response, **extra):
db.session.remove()

with app.app_context():
db.create_all()
yield app

with app.app_context():
db.drop_all()


@pytest.fixture
def client(app):
return app.test_client()

@pytest.fixture
def two_saved_planets(app):
# Arrange
planet_one = Planet(name="Mars",
description="Close enough",
color="Red")
planet_two = Planet(name="Earth",
description="we out here",
color = "Green")

db.session.add_all([planet_one, planet_two])
db.session.commit()
Comment on lines +29 to +39

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

73 changes: 73 additions & 0 deletions app/tests/test_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#`GET` `/planets` returns `200` and an empty array.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test routes look great 👍 !

def test_get_all_planets_with_no_records(client):
# ACT
response = client.get("/planets")
response_body = response.get_json()

# ASSERT
assert response.status_code == 200
assert response_body == []


# GET /planets/1 returns a response body that matches our fixture
def test_get_one_planet(client, two_saved_planets):
#ACT
response = client.get("/planets/1")
response_body = response.get_json()

#ASSERT
assert response.status_code == 200
assert response_body == {
"id" : 1,
"name": "Mars",
"description": "Close enough",
"color": "Red"
}


# GET /planets/1 with no data in test database (no fixture) returns a 404
def test_get_one_planet_but_no_data(client):
# ACT
response = client.get("/planets/1")
response_body = response.get_json()

# ASSERT
assert response.status_code == 404
assert response_body == "Planet 1 does not exist"


# GET /planets with valid test data (fixtures) returns a 200 with
# an array including appropriate test data
def test_get_planets_with_records(client, two_saved_planets):
# ACT
response = client.get("/planets")
response_body = response.get_json()

# ASSERT
assert response.status_code == 200
assert response_body == [
{
"id" : 1,
"name": "Mars",
"description": "Close enough",
"color": "Red"},
{
"id" : 2,
"name": "Earth",
"description":"we out here",
"color" : "Green"}]


# POST /planets with a JSON request body returns a 201
def test_create_one_planet(client):
#ACT
response = client.post("/planets", json={
"name": "new planet",
"description": "with aliens",
"color": "rainbow"
})
response_body = response.get_json()

#ASSERT
assert response.status_code == 201
assert response_body == "Planet new planet has been created"
1 change: 1 addition & 0 deletions migrations/README
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Generic single-database configuration.
45 changes: 45 additions & 0 deletions migrations/alembic.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# A generic, single database configuration.

[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s

# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false


# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARN
handlers = console
qualname =

[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine

[logger_alembic]
level = INFO
handlers =
qualname = alembic

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
Loading