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()'
27 changes: 27 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,34 @@
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__)

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")

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)


return app

Empty file added app/models/__init__.py
Empty file.
33 changes: 33 additions & 0 deletions app/models/planet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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)
diameter = db.Column(db.String)

def to_dict(self):
return {
"id": self.id,
"name": self.name,
"description": self.description,
"diameter": self.diameter
}

@classmethod
def from_dict(cls, planet_data):
new_planet = Planet(name=planet_data["name"],
description=planet_data["description"],
diameter=planet_data["diameter"])
return new_planet

def update(self, req_body):
try:
self.name = req_body["name"]
self.description = req_body["description"]
self.diameter = req_body["diameter"]
except KeyError as error:
abort(make_response({"message": f"Missing attribute: {error}"},400))

Comment on lines +10 to +32
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Great helper methods!


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


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

@planets_bp.route("", methods=["POST"])
def add_planet():
request_body = request.get_json()
new_planet = Planet.from_dict(request_body)

Comment on lines +10 to +12
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Some some validation on request body would be nice here to ensure required fields are present.

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

return make_response(jsonify(f"Planet {new_planet.name} successfully created"), 201)

@planets_bp.route("", methods=["GET"])
def read_all_planets():
name_query = request.args.get("name")
description_query = request.args.get("description")
diameter_query = request.args.get("diameter")
if name_query:
planets = Planet.query.filter_by(name=name_query)
elif description_query:
planets = Planet.query.filter_by(description=description_query)
elif diameter_query:
planets = Planet.query.filter_by(diameter=diameter_query)
else:
planets = Planet.query.all()

planets_response = [planet.to_dict() for planet in planets]
return jsonify(planets_response)

def validate_model(cls, model_id):
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Good helper function

try:
model_id = int(model_id)
except:
abort(make_response({"message": f"{cls.__name__} {model_id} invalid"}, 400))

model = cls.query.get(model_id)

if not model:
abort(make_response({"message": f"{cls.__name__} {model_id} not found"}, 404))

return model

@planets_bp.route("/<planet_id>", methods = ["GET"])
def read_one_planet(planet_id):
planet = validate_model(Planet,planet_id)
return planet.to_dict()

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

request_body = request.get_json()

planet.update(request_body)
db.session.commit()

return make_response(jsonify(f"Planet #{planet.id} successfully updated"))

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

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

return make_response(jsonify(f"Planet #{planet.id} successfully deleted"))


Comment on lines +54 to +73
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Just noting these functions are untested.




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
ocean_planet = Planet(name="Ocean Planet",
description="watr 4evr",
diameter = "0 miles")
mountain_planet = Planet(name="Mountain Planet",
description="i luv 2 climb rocks",
diameter = "1 mile")

db.session.add_all([ocean_planet, mountain_planet])
db.session.commit()
74 changes: 74 additions & 0 deletions app/tests/test_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
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_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": "Ocean Planet",
"description": "watr 4evr",
"diameter": "0 miles"
}

def test_get_one_planet_with_no_data(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_one_planet_with_invalid_id(client):
# Act
response = client.get("/planets/**")
response_body = response.get_json()

# Assert
assert response.status_code == 400
assert response_body == {"message": "Planet ** invalid"}

def test_get_all_planets(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": "Ocean Planet",
"description": "watr 4evr",
"diameter": "0 miles"
},
{
"id": 2,
"name": "Mountain Planet",
"description": "i luv 2 climb rocks",
"diameter": "1 mile"
}]
assert len(response_body) == 2

def test_create_one_planet(client):
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Having a test with an invalid request body would be nice.

# Act
response = client.post("/planets", json={
"name": "New Planet",
"description": "The Best!",
"diameter": "5 miles"
})
response_body = response.get_json()

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