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
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: gunicorn 'app:create_app()'
28 changes: 28 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,35 @@
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):

Choose a reason for hiding this comment

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

👍

app = Flask(__name__)

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

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


from app.models.planet import Planet

db.init_app(app)
migrate.init_app(app, db)

from .routes.routes import planet_bp

Choose a reason for hiding this comment

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

We may want to change the import to planet_routes.py since it contains the nested route for moons.

And planet_routes is a much better file name to hold all the planet routes.

Suggested change
from .routes.routes import planet_bp
from .routes.planet_routes import planet_bp

app.register_blueprint(planet_bp)

return app


2 changes: 2 additions & 0 deletions app/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@


13 changes: 13 additions & 0 deletions app/models/moon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from app import db

class Moon(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String)
planet_id = db.Column(db.Integer, db.ForeignKey('planet.id'))
planet = db.relationship("Planet", back_populates="moons")

def to_json(self):
return {
"id": self.id,
"name": self.name
}
15 changes: 15 additions & 0 deletions app/models/planet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from app import db

class Planet(db.Model):

Choose a reason for hiding this comment

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

👍

id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String)
description = db.Column(db.String)
moons = db.Column(db.Integer)
Comment on lines +5 to +7

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.

Additionally, shouldn't moons be a relationship instead of a column? I also noticed that y'all forgot to add back_populates to Planet. Remember that if we use back_populates, we need to add it to both the parent and child models.

    name = db.Column(db.String, nullable=False)
    description = db.Column(db.String)
    moons = db.Relationship('Moon', back_populates='planet')


def to_json(self):
return {
"id": self.id,
"name": self.name,
"description": self.description,
"moons": self.moons
}
Comment on lines +9 to +15

Choose a reason for hiding this comment

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

👍

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

This file was deleted.

14 changes: 14 additions & 0 deletions app/routes/helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Helper function:
from flask import abort, make_response, jsonify
from app.models.planet import Planet

def validate_planet(planet_id):
try:
planet_id = int(planet_id)
except:
abort(make_response(jsonify(f"planet {planet_id} invalid"), 400))

planet = Planet.query.get(planet_id)
if not planet:
abort(make_response(jsonify(f"planet {planet_id} not found"), 404))
return planet
Comment on lines +1 to +14

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 process 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 contain more routes for moons, suns, etc.

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

9 changes: 9 additions & 0 deletions app/routes/moon_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# from app import db
# from app.models.planet import Planet
# from app.models.moon import Moon
# from flask import Blueprint, jsonify, abort, make_response, request
# from .helper import validate_planet

# moons_bp = Blueprint("moons", __name__, url_prefix="/moons")

# do we even need this

Choose a reason for hiding this comment

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

Nope, you don't need this file unless you're planning on adding future enhancements for moons 🌚 ✨

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

planets_bp = Blueprint("planets_bp", __name__, url_prefix="/planets")

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

try:
new_planet = Planet(name = request_body["name"], description = request_body["description"])
db.session.add(new_planet)
db.session.commit()
except:
abort(make_response(jsonify({"message":f"invalid input"}), 400))
return make_response(jsonify(f"Planet {new_planet.name} successfully created"), 201)
Comment on lines +9 to +19

Choose a reason for hiding this comment

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

👍

There are a few parts to this function:

  • try/except to catch any potential errors/validate the input (request body).
  • Creating the planet instance
  • adding a new planet instance to the database

We can move the try/except into a helper method and create the planet instance through a @classmethod in the Planet model class.



@planets_bp.route("", methods=["GET"])
def read_all_planets():


name_query = request.args.get("name")
# moons_query = request.args.get("moons")
if name_query:
planets = Planet.query.filter_by(name = name_query)
# elif moons_query:
# planets = Planet.query.filter_by(moons = moons_query)
else:
planets = Planet.query.all()
planets_response = []
try:
for planet in planets:
planets_response.append(planet.to_json())

except:
abort(make_response(jsonify(f"planet not found"), 404))
Comment on lines +39 to +40

Choose a reason for hiding this comment

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

Will this except clause ever get executed? It seems like we're trying to throw an error if a planet is found, but even if there are no planets, planets_response will still produce an empty list.

return make_response(jsonify(planets_response),200)




@planets_bp.route("/<planet_id>", methods=["GET"])
def read_one_planet(planet_id):
planet = validate_planet(planet_id)
return make_response(planet.to_json(), 200)
Comment on lines +46 to +49

Choose a reason for hiding this comment

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

Nice helper! 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 read_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



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

request_body = request.get_json()
try:
planet.name = request_body["name"]
planet.description = request_body["description"]
# planet.moons = request_body["moons"]
except:
abort(make_response(jsonify(f"invalid data"), 400))
Comment on lines +57 to +62

Choose a reason for hiding this comment

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

It'd be helpful to also include what error would occur in the except clause. I imagine it'd be a keyError.


db.session.commit()

return jsonify(planet.to_json(), 200)

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

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

return make_response(jsonify(f"planet{planet_id} successfully deleted"), 200)
Comment on lines +68 to +75

Choose a reason for hiding this comment

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

make_response isn't necessary to return as jsonify already returns a response object.

    return jsonify(f"planet{planet_id} successfully deleted"), 200


@planets_bp.route("/<planet_id>/moons", methods=["GET"])
def read_moons_for_planet(planet_id):
planet = validate_planet(planet_id)
moons_response = []
for moon in planet.moons:
moons_response.append(moon.to_json())
return make_response(jsonify(moons_response), 200)
Comment on lines +77 to +83

Choose a reason for hiding this comment

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

We can use list comprehension to create moons_response:

moons_response = [moon.to_json() for moon in planet.moons]


@planets_bp.route("/<planet_id>/moons", methods=["POST"])
def write_moon_to_planet(planet_id):
planet = validate_planet(planet_id)
request_body = request.get_json()
new_moon = Moon(name=request_body["name"], planet=planet)

db.session.add(new_moon)
db.session.commit()

return make_response(jsonify(f"Moon {new_moon.name} of the planet {new_moon.planet.name} successfully created"), 201)
Comment on lines +85 to +94

Choose a reason for hiding this comment

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

👍

Choose a reason for hiding this comment

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

The previous comment about not needing make_response can also apply here.

75 changes: 75 additions & 0 deletions app/routes/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from app import db

Choose a reason for hiding this comment

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

Comments from planet_routes.py can also apply to this file.

from app.models.planet import Planet
from flask import Blueprint, jsonify, abort, make_response, request
from .helper import validate_planet

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

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

try:
new_planet = Planet(name = request_body["name"], description = request_body["description"], moons = request_body["moons"])
db.session.add(new_planet)
db.session.commit()
except:
abort(make_response(jsonify({"message":f"invalid input"}), 400))
return make_response(jsonify(f"Planet {new_planet.name} successfully created"), 201)


@planet_bp.route("", methods=["GET"])
def read_all_planets():


name_query = request.args.get("name")
moons_query = request.args.get("moons")
if name_query:
planets = Planet.query.filter_by(name = name_query)
elif moons_query:
planets = Planet.query.filter_by(moons = moons_query)
else:
planets = Planet.query.all()
planets_response = []
try:
for planet in planets:
planets_response.append(planet.to_json())

except:
abort(make_response(jsonify(f"planet not found"), 404))
return make_response(jsonify(planets_response),200)




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


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

request_body = request.get_json()
try:
planet.name = request_body["name"]
planet.description = request_body["description"]
planet.moons = request_body["moons"]
except:
abort(make_response(jsonify(f"invalid data"), 400))

db.session.commit()

return jsonify(planet.to_json(), 200)

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

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

return make_response(jsonify(f"planet{planet_id} successfully deleted"), 200)

Empty file added app/tests/__init__.py
Empty file.
34 changes: 34 additions & 0 deletions app/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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):
Mercury = Planet(name = "Mercury", description = "This is the first planet", moons = 2)
Venus = Planet(name = "Venus", description = "named after the Roman goddess of love and beauty", moons = 0)

db.session.add_all([Mercury,Venus])
db.session.commit()
Comment on lines +29 to +34

Choose a reason for hiding this comment

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

👍

Nice work! My one critique is about Mercury and Venus variables looking like classes. Capitalizing the first letter in a name is reserved for class names like, Planet. To avoid confusion and follow pep8 guidelines, let's continue to practice lowercase for our variable names.

Empty file added app/tests/moon_routes.py
Empty file.
37 changes: 37 additions & 0 deletions app/tests/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@


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 == []

def test_get_book_by_id(client, two_saved_planets):

response = client.get('/planets/2')
response_body = response.get_json()

assert response.status_code == 200
assert response_body == {
"id": 2,
"name": "Venus",
"description": "named after the Roman goddess of love and beauty",
"moons" : 0
}


def test_create_one_planet(client):
# Act
response = client.post("/planets", json={
"name": "Asimov",
"description": "This planet named after Isaac Asimov, the other of Lucky Starr book series",
"moons" : 4
})
response_body = response.get_json()

# Assert
assert response.status_code == 201
assert response_body == "Planet Asimov successfully created"
37 changes: 37 additions & 0 deletions app/tests/test_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@

Choose a reason for hiding this comment

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

👍


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 == []

def test_get_book_by_id(client, two_saved_planets):

response = client.get('/planets/2')
response_body = response.get_json()

assert response.status_code == 200
assert response_body == {
"id": 2,
"name": "Venus",
"description": "named after the Roman goddess of love and beauty",
"moons" : 0
}


def test_create_one_planet(client):
# Act
response = client.post("/planets", json={
"name": "Asimov",
"description": "This planet named after Isaac Asimov, the other of Lucky Starr book series",
"moons" : 4
})
response_body = response.get_json()

# Assert
assert response.status_code == 201
assert response_body == "Planet Asimov successfully 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.
Loading