Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f445de9
defines planet class
pizzagirl1 Apr 21, 2023
e3c87f1
adds list of planets
pizzagirl1 Apr 21, 2023
73993bb
adds missing commas to list of planets
pizzagirl1 Apr 21, 2023
1d266a2
adds blueprint
pizzagirl1 Apr 21, 2023
469adc3
minor changes
pizzagirl1 Apr 24, 2023
fb3c2bf
reads one planet and handles invalid and non existent id
Ljpeg Apr 24, 2023
d8f2596
Merge branch 'main' of https://github.com/LCanCode/solar-system-api
Ljpeg Apr 24, 2023
8b27493
adds decorator to line 85
Ljpeg Apr 24, 2023
9edb99a
adds to_dict function to planet class and deletes planets.py
pizzagirl1 Apr 24, 2023
10309dc
add Planet model and imports migrations folder
Ljpeg Apr 28, 2023
689428c
changes GET method to use list comprehension
pizzagirl1 Apr 30, 2023
12d5e02
removes old Planet class in favor of Model
pizzagirl1 Apr 30, 2023
99fa769
defines create method for Planet
pizzagirl1 May 1, 2023
7ea730a
creates READ ALL planets endpoint
pizzagirl1 May 1, 2023
0582bb4
adds route for get_one_planet
Ljpeg May 2, 2023
479455b
adds replace_planet route
Ljpeg May 2, 2023
73f5bc9
adds delete_planet route
Ljpeg May 2, 2023
8a0cae7
sets up dotenv
pizzagirl1 May 3, 2023
770cfe6
adds test folder
pizzagirl1 May 3, 2023
04af34d
adds test get all with no records
pizzagirl1 May 3, 2023
76af12e
adds test for one planet
pizzagirl1 May 3, 2023
fbf1316
finishes tests for all CRUD methods
pizzagirl1 May 3, 2023
ef7e859
updates walla-walla description
pizzagirl1 May 3, 2023
268cca6
creates fixture for walla-walla planet
pizzagirl1 May 3, 2023
50d9560
refactors with to_dict, updates solar_day
Ljpeg May 4, 2023
ef75126
creates test_to_dict_no_missing_info test passes
Ljpeg May 4, 2023
414cfb4
adds from_dict class method and updated post to include it
Ljpeg May 4, 2023
e357b16
updates validate_planet to validate_models, updates deleteget put rou…
Ljpeg May 4, 2023
d4d8805
moves original planet list to seed py file
Ljpeg May 4, 2023
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
26 changes: 25 additions & 1 deletion app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,31 @@
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nice job placing this above the conditional so we don't repeat code!


return app
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)

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

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, nullable=False)
solar_day=db.Column(db.Float, nullable=False)

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 idea to make these attributes non-nullable! That being said, it can be beneficial to have some nullable attributes. It all boils down to how you want to design your particular class though!


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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

👍

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

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

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 job putting together your planet class, your planet list and your planet blueprint!



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

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

return model

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 job updating the validate model method to be generic and applicable to any class!




@planets_bp.route("", methods=["POST"])
def create_planet():
request_body = request.get_json()
new_planet = Planet.from_dict(request_body)
db.session.add(new_planet)
db.session.commit()

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

When you created your Planet class, you made each of the attributes non-nullable, which means that each one needs to be present when an instance of that class is created! With that in mind, it might be a good idea to include some level of error handling when your planet is created in case any of the attributes aren't present!


@planets_bp.route("", methods=["GET"])
def get_all_planets():
planets = Planet.query.all()
results = [planet.to_dict() for planet in planets]

return jsonify(results)

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

return planet.to_dict()

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

planet.name = planet_to_update["name"]
planet.description = planet_to_update["description"]
planet.solar_day = planet_to_update["solar_day"]

db.session.commit()

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Similarly as before, this method would be a good candidate for error handling as well.



@planets_bp.route("/<planet_id>", methods=["DELETE"])
def delete_planet(planet_id):
planet_to_delete = validate_model(Planet, planet_id)
db.session.delete(planet_to_delete)
db.session.commit()

return make_response(jsonify(f"Planet {planet_to_delete.name} was deleted."))
11 changes: 11 additions & 0 deletions app/seed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# planets = [
# Planet(1, "Mercury", "smallest planet", 176.0),
# Planet(2, "Venus", "planet of love", 243.0),
# Planet(3, "Earth", "home planet", 1.0),
# Planet(4, "Mars", "red planet", 1.25),
# Planet(5, "Jupiter", "largest planet", 0.42),
# Planet(6, "Saturn", "ring planet", .45),
# Planet(7, "Uranus", "coldest planet", .71),
# Planet(8, "Neptune", "not visible to the naked eye", .67),
# Planet(9, "Pluto", "unqualified planet", 6.375)
# ]
1 change: 1 addition & 0 deletions migrations/README
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Single-database configuration for Flask.
50 changes: 50 additions & 0 deletions migrations/alembic.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# 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,flask_migrate

[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

[logger_flask_migrate]
level = INFO
handlers =
qualname = flask_migrate

[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
91 changes: 91 additions & 0 deletions migrations/env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from __future__ import with_statement

import logging
from logging.config import fileConfig

from flask import current_app

from alembic import context

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config

# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')

# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
config.set_main_option(
'sqlalchemy.url',
str(current_app.extensions['migrate'].db.get_engine().url).replace(
'%', '%%'))
target_metadata = current_app.extensions['migrate'].db.metadata

# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.


def run_migrations_offline():
"""Run migrations in 'offline' mode.

This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.

Calls to context.execute() here emit the given string to the
script output.

"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url, target_metadata=target_metadata, literal_binds=True
)

with context.begin_transaction():
context.run_migrations()


def run_migrations_online():
"""Run migrations in 'online' mode.

In this scenario we need to create an Engine
and associate a connection with the context.

"""

# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')

connectable = current_app.extensions['migrate'].db.get_engine()

with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
process_revision_directives=process_revision_directives,
**current_app.extensions['migrate'].configure_args
)

with context.begin_transaction():
context.run_migrations()


if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
24 changes: 24 additions & 0 deletions migrations/script.py.mako
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""${message}

Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}

"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}

# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}


def upgrade():
${upgrades if upgrades else "pass"}


def downgrade():
${downgrades if downgrades else "pass"}
34 changes: 34 additions & 0 deletions migrations/versions/595fe7f869fa_adds_planet_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""adds Planet model

Revision ID: 595fe7f869fa
Revises:
Create Date: 2023-04-28 13:12:50.013892

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '595fe7f869fa'
down_revision = None
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('planet',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('name', sa.String(), nullable=False),
sa.Column('description', sa.String(), nullable=False),
sa.Column('solar_day', sa.Float(), nullable=False),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('planet')
# ### end Alembic commands ###
Empty file added tests/__init__.py
Empty file.
51 changes: 51 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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_planets(app):
nebula = Planet(
name="Nebula",
description="Fake planet for testing purposes",
solar_day=420.0
)

gamora = Planet(
name="Gamora",
description="Do not visit. Not a real place.",
solar_day=666.0
)

db.session.add_all([nebula, gamora])
db.session.commit()

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 job creating a custom fixture to work with multiple planets!


@pytest.fixture
def walla_walla():
return {
"name": "Walla-Walla",
"description": "A bad place.",
"solar_day": 0.5

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Love this!

}
Loading