Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
8695060
wave_01 Planet Class created
MelodyW2022 Dec 14, 2022
5fa6a0f
wave_01 completed
MelodyW2022 Dec 14, 2022
3fb0a61
Added single planet endpoint and handled invalid planet id type and n…
ctlaultdel Dec 15, 2022
978e247
connected the app to a db. created GET /planets, GET /planets/<planet…
dnabilali Dec 21, 2022
578b182
Added update_planet functionality for /planets/<planet_id> endpoint
ctlaultdel Dec 21, 2022
33981b5
Added delete_planet functionality for /planets/<planet_id> endpoint
ctlaultdel Dec 21, 2022
bfd24a2
Refactored parts of update_planet() commented, issue with getting it …
ctlaultdel Dec 21, 2022
80f74a3
Updated model - plural table name and float dtype for mass attribute
ctlaultdel Dec 21, 2022
74f7424
New migration for model updated with plural table name and float dtyp…
ctlaultdel Dec 21, 2022
eda1e3a
Merge pull request #1 from dnabilali/LA-mass-dtype-issue
dnabilali Dec 22, 2022
b91ca43
changed migrate instance to allow detecting changes in data types. cr…
dnabilali Dec 22, 2022
b66defe
Merge branch 'main' of https://github.com/dnabilali/solar-system-api
dnabilali Dec 22, 2022
e5b67ae
changed tale name to planets. all columns are not allowed to be null.…
dnabilali Dec 22, 2022
3cb1e06
Updated display_all_planets to include query param for planet name
ctlaultdel Dec 22, 2022
ad45454
Refactored query params in display_all_planets and added a helper fun…
ctlaultdel Dec 23, 2022
847b34c
Switched function name from process_args() to process_kwargs since mo…
ctlaultdel Dec 23, 2022
d36a932
Merge pull request #2 from dnabilali/LA-wave05-query-params
dnabilali Jan 3, 2023
268d981
Create .env file, and tests folder with files
MelodyW2022 Jan 3, 2023
3177d39
created 3 fixtures:app, client, two_saved_planets.updated app/__init_…
dnabilali Jan 3, 2023
732b3c0
added db.session.refresh in conftest.py
dnabilali Jan 3, 2023
92b2283
Merge pull request #3 from dnabilali/test_setup
ctlaultdel Jan 3, 2023
39e731f
Planets for database
ctlaultdel Jan 4, 2023
a8ee6e4
Added get_all_attrs() method to Planet class
ctlaultdel Jan 4, 2023
b06da98
Updated sort by attribute feature in display_all_planets() and proces…
ctlaultdel Jan 4, 2023
502a61a
Merge pull request #4 from dnabilali/LA-wave05-query-params
dnabilali Jan 4, 2023
4226c76
added tests for get /planet/<id> with an empty db, update planet succ…
dnabilali Jan 4, 2023
aa928ce
test: Create test_get_one_planet, test_create_one_planet and test_cre…
MelodyW2022 Jan 4, 2023
f2b90c5
Merge pull request #5 from dnabilali/DALIA-tests
MelodyW2022 Jan 4, 2023
6b26ea9
Added unit tests 3, 6, and 3 edge cases for get method with query kwa…
ctlaultdel Jan 4, 2023
692f4c3
Merge pull request #6 from dnabilali/LA-tests-3-6-and-3-edge-cases
dnabilali Jan 4, 2023
8e00dbd
Merge branch 'main' into MW_wave06
dnabilali Jan 4, 2023
84ebb93
Merge pull request #7 from dnabilali/MW_wave06
dnabilali Jan 4, 2023
a2c0dee
Added to_dict() method to Planet class
ctlaultdel Jan 4, 2023
9223075
Refactored routes to make use of Planet.to_dict() method
ctlaultdel Jan 4, 2023
3625bbf
Added unit test to test if dict returned by Planet.to_dict() method i…
ctlaultdel Jan 4, 2023
1c455f2
added Planet classmethod from_dict and wrote tests for it in test_mod…
dnabilali Jan 4, 2023
8a0a9fb
Moved Planet method to_dict unit test here
ctlaultdel Jan 4, 2023
dc67112
Deleted Planet method to_dict unit test
ctlaultdel Jan 4, 2023
5d51c1f
Merge pull request #8 from dnabilali/LA-wave07-to-dict-refactor
dnabilali Jan 4, 2023
3456111
Added @classmethod decorator to get_all_attrs() - consistent and clea…
ctlaultdel Jan 5, 2023
ceb9869
rearrange the tests by Get Post Put Delete and validate_model()
MelodyW2022 Jan 5, 2023
0bbabab
refactor: Create 3 new tests on validate_model()
MelodyW2022 Jan 5, 2023
94f050b
refactor: Create 4 new tests for Put method
MelodyW2022 Jan 5, 2023
c3c6cec
refactor: Create 3 new tests for Delete method
MelodyW2022 Jan 5, 2023
d8fe508
Correct the return message of the Put end point
MelodyW2022 Jan 5, 2023
75e8927
corrected syntax on line 169
MelodyW2022 Jan 5, 2023
ce7af0b
Merge pull request #9 from dnabilali/MW_wave07
ctlaultdel Jan 5, 2023
795ada1
Created Moon class and moons table
ctlaultdel Jan 5, 2023
a3ef874
Imported Moons class and registered moons_bp
ctlaultdel Jan 5, 2023
54e7aa3
Added moons relationship to planets table and class
ctlaultdel Jan 5, 2023
a577c63
restructured separate routes - moons and planets
ctlaultdel Jan 5, 2023
461fd90
got new moon routes working
ctlaultdel Jan 5, 2023
b6cd501
corrected conflict
ctlaultdel Jan 5, 2023
679dc0a
corrected merge conflicts
ctlaultdel Jan 5, 2023
ce4c5c9
Merge pull request #10 from dnabilali/wave8-moon-model-and-routes
ctlaultdel Jan 5, 2023
2bfa6b5
added attributes:size, description and discovery_date to the moon mod…
dnabilali Jan 6, 2023
7613730
updated POST planets/<planet_id>/moons to reflect changes made in the…
dnabilali Jan 6, 2023
553caea
Merge pull request #11 from dnabilali/DALIA_/planets/<planet_id>/moons
MelodyW2022 Jan 6, 2023
8f342db
feat: Create nested route - Get all moons by planed_id
MelodyW2022 Jan 6, 2023
62ec446
Merge pull request #12 from dnabilali/MW_get_all_moons
ctlaultdel Jan 6, 2023
d04aa82
re-initialized migrations
ctlaultdel Jan 6, 2023
67b832b
added from_dict moon class method
ctlaultdel Jan 6, 2023
d9f96e2
refactored code to make use of Moon.from_dict()
ctlaultdel Jan 6, 2023
45f64cd
small refactors
ctlaultdel Jan 6, 2023
f1de80f
Merge pull request #13 from dnabilali/LA-refactor-moon-from-dict-and-…
ctlaultdel Jan 6, 2023
3efb4e0
correct typo to description
MelodyW2022 Jan 9, 2023
e3b398c
didn't update moon model or planet model, only add GET moon by id end…
MelodyW2022 Jan 9, 2023
346386c
corrct the assert response_body part
MelodyW2022 Jan 9, 2023
e8c25ab
Merge pull request #14 from dnabilali/MW_08
dnabilali Jan 9, 2023
ab7ca3b
gunicorn to the requirements.t
MelodyW2022 Jan 9, 2023
28dbcf0
Create a Procfile for Heroku
MelodyW2022 Jan 9, 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
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()'
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(compare_type=True)
load_dotenv()

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

app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

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

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

from .routes.planets_routes import planets_bp
from .routes.moons_routes import moons_bp
app.register_blueprint(planets_bp)
app.register_blueprint(moons_bp)

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

class Moon(db.Model):
__tablename__ = 'moons'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String, nullable=False)
size = db.Column(db.Float, nullable=False)
description = db.Column(db.String, nullable=False)
discovery_date = db.Column(db.DateTime, nullable=False)
planet = db.relationship("Planet", back_populates="moons")
planet_id = db.Column(db.Integer, db.ForeignKey("planets.id"))

@classmethod
def get_all_attrs(cls):
"""
Returns all existing attributes (list) in Planet class
"""
return [attr for attr in dir(cls) if callable(attr)]

@classmethod
def from_dict(cls, dict, planet, planet_id):
return Moon(
name=dict["name"],
size=dict["size"],
description=dict["description"],
discovery_date=dict["discovery_date"],
planet=planet,
planet_id=planet_id,
)

def to_dict(self):
"""
Returns dictionary containing Planet instance data
"""
return {
"id": self.id,
"name": self.name,
"size": self.size,
"description": self.description,
"discovery_date": self.discovery_date,
"planet_id": self.planet_id,
#"planet": self.planet.name,
}

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

class Planet(db.Model):
__tablename__ = 'planets'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String, nullable=False)
description = db.Column(db.String, nullable=False)
mass = db.Column(db.Float, nullable=False)
moons = db.relationship("Moon", back_populates="planet")

@classmethod
def get_all_attrs(cls):
"""
Returns all existing attributes (list) in Planet class
"""
return [attr for attr in dir(Planet) if callable(attr)]

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

def to_dict(self):
"""
Returns dictionary containing Planet instance data
"""
return {
"id": self.id,
"name": self.name,
"description": self.description,
"mass": self.mass
}
# @classmethod
# def get_planet_by_id(cls, id):
# return cls.query.get(id)

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

This file was deleted.

Empty file added app/routes/__init__.py
Empty file.
42 changes: 42 additions & 0 deletions app/routes/moons_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from app import db
from flask import Blueprint, jsonify, abort, make_response, request
from ..models.moon import Moon
from ..models.planet import Planet
from app.routes.planets_routes import validate_model
moons_bp = Blueprint("moons", __name__, url_prefix = "/moons")

# ~~~~~~ Planet Routes ~~~~~
@moons_bp.route("", methods=["GET"])
def display_all_moons():
moons = Moon.query.all()
# fill http response
response_moons = []
for moon in moons:
response_moons.append(moon.to_dict())
return jsonify(response_moons)

@moons_bp.route("/<moon_id>",methods=["GET"])
def display_moon(moon_id):
moon = validate_model(Moon, moon_id)
return moon.to_dict()


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

#required request body: name, description, size, discovery_date, planet_id
attribute_requirements = ["name", "description", "size", "discovery_date", "planet_id"]

for req in attribute_requirements:
if req not in request_body:
abort(make_response({
"message" : f"Failed to create a planet because {req} missing"
}, 400))

planet_id = request_body["planet_id"]
planet = validate_model(Planet, planet_id)
new_moon = Moon.from_dict(request_body, planet, planet_id)
db.session.add(new_moon)
db.session.commit()
return make_response({"message": "moon has been created successfully"}, 201)
163 changes: 163 additions & 0 deletions app/routes/planets_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
from app import db
from flask import Blueprint, jsonify, abort, make_response, request
from app.models.planet import Planet
from app.models.moon import Moon


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

# ~~~~~~ Validation Checkers ~~~~~
def validate_model(cls, model_id):
"""
Checks if planet id is valid and returns error messages for invalid inputs
:params:
- planet_id (int)
:returns:
- planet (object) if valid planet id valid
"""
try:
model_id = int(model_id)
except:
# handling invalid planet id type
abort(make_response({"message":f"{cls.__name__} {model_id} invalid"}, 400))
# return planet data if id in db
model = cls.query.get(model_id)
# handle nonexistant planet id
if not model:
abort(make_response({"message":f"{cls.__name__} {model_id} not found"}, 404))
return model


def process_kwargs(queries):
"""
Separate kwargs from HTTP request into separate dicts based on SQLAlchemy query method
:params:
- queries (dict)
:returns:
- attrs (dict): planet class attributes kwargs for filter_by
** name, description, mass
- orderby (dict): method kwargs for order_by
** sort_mass, sort_name
- sels (dict): selected number of results for limit
"""
planet_attrs = Planet.get_all_attrs()
order_methods = ["sort", "desc"]
attrs = {}
orderby = {}
sels = {}
for kwarg in queries:
if kwarg in planet_attrs:
attrs[kwarg] = queries[kwarg]
elif kwarg in order_methods:
orderby[kwarg] = queries[kwarg]
elif kwarg == "limit":
sels[kwarg] = queries[kwarg]
else:
abort(make_response(
{"message" : f"{kwarg} is an invalid query"}, 400
))
return attrs, orderby, sels


# ~~~~~~ Planet Routes ~~~~~
@planets_bp.route("",methods= ["GET"])
def display_all_planets():
# collect query & parse kwargs
planet_query = Planet.query
attrs, orderby, sels = process_kwargs(request.args.to_dict())
if attrs:
# filter by attribute kwargs e.g name=Earth
planet_query = planet_query.filter_by(**attrs)
if "sort" in orderby:
# sort by given attribute e.g.sort=mass
clause = getattr(Planet, orderby["sort"])
if "desc" in orderby:
# sort in descending order e.g.desc=True
planet_query = planet_query.order_by(clause.desc())
else:
# default is asc=True
planet_query = planet_query.order_by(clause.asc())
if sels:
# limit selection of planets to view
planet_query = planet_query.limit(**sels)
# perform query
planets = planet_query.all()
# fill http response
response_planets = []
for planet in planets:
response_planets.append(planet.to_dict())
return jsonify(response_planets)


# ~~~~~~ Single planet endpoint ~~~~~~
@planets_bp.route("/<planet_id>",methods=["GET"])
def display_planet(planet_id):
valid_planet = validate_model(Planet, planet_id)
return valid_planet.to_dict()


@planets_bp.route("", methods=["POST"])
def create_planet():
request_body = request.get_json()
attribute_requirements = ["name", "description", "mass"]
for req in attribute_requirements:
if req not in request_body:
abort(make_response({
"message" : f"Failed to create a planet because {req} missing"
}, 400))
new_planet = Planet.from_dict(request_body)
db.session.add(new_planet)
db.session.commit()
return make_response({"message":"planet has been created successfully"}, 201)


@planets_bp.route("/<planet_id>", methods=["PUT"])
def update_planet(planet_id):
request_body = request.get_json()
planet = validate_model(Planet, planet_id)
planet.name = request_body["name"] if "name" in request_body else planet.name
planet.description = request_body["description"] if "description" in request_body else planet.description
planet.mass = request_body["mass"] if "mass" in request_body else planet.mass
db.session.commit()
return make_response(
{"message": f"Planet #{planet_id} successfully updated"}, 200
)


@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(
{"message": f"planet #{planet_id} has been deleted successfully"}, 200
)


@planets_bp.route("/<planet_id>/moons", methods=["POST"])
def create_moon(planet_id):
planet = validate_model(Planet, planet_id)
request_body = request.get_json()
required_attributes = Moon.get_all_attrs()
# check for all required post attributes in request body
for attr in required_attributes:
if attr not in request_body:
abort(make_response(jsonify({
"message":f"{attr} must be included to add a moon"
}), 400))
new_moon = Moon.from_dict(request_body, planet, planet_id)
db.session.add(new_moon)
db.session.commit()
return make_response({
"message": f"Moon {new_moon.name} for Planet {planet.name} successfully created"
}, 201)


#nested routes GET `/planets/<planet_id>/moons`
@planets_bp.route("/<planet_id>/moons", methods=["GET"])
def read_moons(planet_id):
planet = validate_model(Planet, planet_id)
moons_response = []
for moon in planet.moons:
moons_response.append(moon.to_dict())
return jsonify(moons_response)
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