Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 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
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
23 changes: 23 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,30 @@
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

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

from .routes import planets_bp
app.register_blueprint(planets_bp)

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

# planets = [Planet(1, "Neptune", "thick, windy", 1.024e26 ),
# Planet(2, "Mars", "dusty, cold desert",6.39e23),
# Planet(3, "Earth", "rocky, terrestrial, full of life",5.972e24)
# ]

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)
# gravity = db.Column(db.Float, nullable=False)
# dist_sun = db.Column(db.Float, nullable=False)
# n_moons = db.Column(db.Integer, nullable=False)
# water
# temperature
# atmosphere

def get_all_attrs():
"""
Returns all existing attributes (list) in Planet class
"""
return [attr for attr in dir(Planet) if not attr.startswith('__')]
150 changes: 149 additions & 1 deletion app/routes.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,150 @@
from flask import Blueprint
from app import db
from flask import Blueprint, jsonify, abort, make_response, request
from .models.planet import Planet


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


# ~~~~~~ Helper functions ~~~~~
# TO DO: Create separate helper function module or add as class method #
def validate_id(planet_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:
planet_id_int = int(planet_id)
except:
# handling invalid planet id type
abort(make_response({"message": f"{planet_id} is an invalid planet id"}, 400))

# return planet data if id in db
planet = Planet.query.get(planet_id)

# handle nonexistant planet id
if not planet:
abort(make_response({"message": f"{planet_id} not found"}, 404))
return planet

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


@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({
"id": planet.id,
"name": planet.name,
"description": planet.description,
"mass": planet.mass
})
return jsonify(response_planets)


# ~~~~~~ Single planet endpoint ~~~~~~
@planets_bp.route("/<planet_id>",methods=["GET"])
def display_planet(planet_id):
valid_planet = validate_id(planet_id)
return {
"id": valid_planet.id,
"name": valid_planet.name,
"description": valid_planet.description,
"mass": valid_planet.mass,
}


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

if "name" not in request_body or "description" not in request_body \
or "mass" not in request_body:
abort(make_response({"message" : \
"Failed to create a planet because the name and/or description \
and/or mass are missing"}, 400))

new_planet = Planet(
name=request_body["name"],
description=request_body["description"],
mass=request_body["mass"])

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_id(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} Updated Successfully"}, 200
)


@planets_bp.route("/<planet_id>", methods=["DELETE"])
def delete_planet(planet_id):
planet = validate_id(planet_id)
db.session.delete(planet)
db.session.commit()
return make_response(
{"message": f"planet #{planet_id} has been deleted successfully"}, 200
)
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
96 changes: 96 additions & 0 deletions migrations/env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
from __future__ import with_statement

import logging
from logging.config import fileConfig

from sqlalchemy import engine_from_config
from sqlalchemy import pool
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.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 = engine_from_config(
config.get_section(config.config_ini_section),
prefix='sqlalchemy.',
poolclass=pool.NullPool,
)

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"}
Loading