Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SHARKS - Jande R. and Lindsey S. #7

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
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 import planets_bp
app.register_blueprint(planets_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 setting up the Flask app!

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

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

planet = Planet.query.get(id)

if not planet:
abort(make_response({"message": f"planet {id} not found"}, 404))

return planet
Comment on lines +1 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 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.

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

31 changes: 31 additions & 0 deletions app/models/planet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from app import db
from flask import abort, make_response

class Planet(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String)
description = db.Column(db.String)
circumference = db.Column(db.Integer)
length_of_year= db.Column(db.Integer)
Comment on lines +4 to +9

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.

class Planet(db.Model):
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    name = db.Column(db.String, nullable=False)
    description = db.Column(db.String)
    circumference = db.Column(db.Integer)
    length_of_year= db.Column(db.Integer)


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

#update
def update(self, request_body):
self.name = request_body["name"]
self.description = request_body["description"]
self.circumference = request_body["circumference"]
self.length_of_year = request_body["length_of_year"]

@classmethod
def create(cls, request_body):
new_planet = cls(name = request_body["name"], description = request_body["description"], circumference = request_body["circumference"], length_of_year = request_body["length_of_year"])

return new_planet
Comment on lines +11 to +31

Choose a reason for hiding this comment

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

👍 Nice helper methods and use of a class method!

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


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

# CREATE PLANET
@planets_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 successfully created"), 201 #use make response when you want to return something that is not json
Comment on lines +10 to +19

Choose a reason for hiding this comment

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

Looks great! We could also consider validating the request_body in a separate helper function.

Choose a reason for hiding this comment

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

To add onto your comment about make_response, we can also use it when we want to add additional headers to a response. Here are some examples in the documentation:
https://flask.palletsprojects.com/en/2.0.x/api/#flask.make_response


# GET ALL
@planets_bp.route("", methods = ["GET"])
def get_all_planets():

days_query = request.args.get("length_of_year")
name_query = request.args.get("name")
description_query = request.args.get("description")
circumference_query = request.args.get("circumference")

if days_query:
planets = Planet.query.filter_by(length_of_year = days_query)
elif name_query:
planets = Planet.query.filter_by(name = name_query)
elif description_query:
planets = Planet.query.filter_by(description = description_query)
elif circumference_query:
planets = Planet.query.filter_by(circumference = circumference_query)
else:
planets = Planet.query.all()

planets_response = []

for planet in planets:
planets_response.append(planet.to_json())

return jsonify(planets_response)
Comment on lines +23 to +46

Choose a reason for hiding this comment

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

Loving the exploration with filters here! We can also consider making a helper method (especially class method) in Planet to perform this filtering.


# GET ONE PLANET
@planets_bp.route("/<id>", methods = ["GET"])
def get_one_planet(id):
planet = validate_planet(id)
return jsonify(planet.to_json()), 200
Comment on lines +49 to +52

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





# {
# name:
# description:
# circumference:
# length_of_year:
# }
Comment on lines +56 to +62

Choose a reason for hiding this comment

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

We can get rid of unneeded comments when submitting code for review.


#PUT or update ONE PLANET
@planets_bp.route("/<id>", methods=["PUT"])
def update_planet(id):
planet = validate_planet(id)

request_body = request.get_json()

planet.update(request_body)
# planet.name = request_body["name"]
# planet.description = request_body["description"]
# planet.circumference = request_body["circumference"]
# planet.length_of_year = request_body["length_of_year"]

db.session.commit()

return make_response(f"Planet #{id} succesffully updated"), 200

#MAKE PATCH REQUEST

#DELETE ONE PLANET
@planets_bp.route("/<id>", methods=["DELETE"])
def delete_one_planet(id):
planet = validate_planet(id)

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

return make_response(f"Planet #{id} was successfully deleted"), 200
Comment on lines +84 to +91

Choose a reason for hiding this comment

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

When we pass a string to make_response, Flask will assume that we want to return HTML rather than JSON data. make_response is useful when we want to attach headers to a response but in this case, we're simply returning JSON, so jsonify() is sufficient enough.

return jsonify(f"Planet {id} successfully deleted"), 200

# {"name": "Mercury",
# "description": "made mostly of rocks",
# "circumference": 9522,
# "length_of_year": 88
# }


# {
# "name": Venus,
# "description": most like Earth,
# "circumference": 23617,
# "length_of_year": 225
# }


# {
# "name": "Earth"
# "description": "you are here"
# "circumference": 24889
# "length_of_year": 365
# }


# {
# "name": "Mars",
# "description": "the red planet",
# "circumference": 13256,
# "length_of_year": 687
# }


# {
# "name": "Jupiter",
# "description": "largest planet",
# "circumference": 278985,
# "length_of_year": 4320
# }


# {
# "name": "Saturn",
# "description": "sun's bae with all 7 rings",
# "circumference": 235185,
# "length_of_year": 10620
# }


# {
# "name": "Uranus",
# "description": "can only be seen with a telescope",
# "circumference": 99739,
# "length_of_year": 30240
# }


# {
# "name": "Neptune",
# "description": "it is an intense blue color",
# "circumference": 96645,
# "length_of_year": 59400
# }


# {
# "name": "Pluto",
# "description": "no dwarf in my book",
# "circumference": 7144,
# "length_of_year": 88920
# }
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.models.planet import Planet
from app import create_app
from app import db
from flask.signals import request_finished


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

# everytime you finish a request this removes any saved data (like cleaning our a cache)
@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
mercury = Planet(name="Mercury",
description="made mostly of rocks", circumference=9522, length_of_year = 88)
venus = Planet(name="Venus",
description="most like Earth", circumference=23617, length_of_year=225)

db.session.add_all([mercury, venus])
# Alternatively, we could do# db.session.add(mercury)# db.session.add(venus)
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.

👍 Nice work on setting up the fixtures!

70 changes: 70 additions & 0 deletions app/tests/test_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@

Choose a reason for hiding this comment

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

👍 Nice work on the tests!

def test_get_all_planets_successfully(client):
# Act
response = client.get("/planets")
# response has more than just json in it so we just want the json
response_body = response.get_json()

# Assert
assert response.status_code == 200
assert response_body == [] # comparing to empty list bc we are in our test database



def test_create_one_planet(client):
# Act
response = client.post("/planets", json={
"name": "Goofy",
"description": "goofy",
"circumference": 1,
"length_of_year": 1
})
response_body = response.get_json()

# Assert
assert response.status_code == 201
assert response_body == "Planet Goofy has been successfully created"

def test_get_one_planet_from_fixture_successfully(client, two_saved_planets):
#act
response = client.get("/planets/1")

response_body = response.get_json()
#assert
assert response.status_code == 200
assert response_body == {"name": "Mercury",
"description": "made mostly of rocks",
"circumference": 9522,
"length_of_year": 88,
"id": 1
}

def test_get_one_planet_with_no_data_returns_404(client):
#act
response = client.get("/planets/1")

response_body = response.get_json()
#assert
assert response.status_code == 404
assert response_body == {'message': 'planet 1 not found'}

def test_get_planet_from_fixture_successfully(client, two_saved_planets):
#act
response = client.get("/planets")

response_body = response.get_json()
#assert
assert response.status_code == 200
assert response_body == [{"name": "Mercury",
"description": "made mostly of rocks",
"circumference": 9522,
"length_of_year": 88,
"id": 1
},
{
"name": "Venus",
"description": "most like Earth",
"circumference": 23617,
"length_of_year": 225,
'id': 2,
}]
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