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

Mw crud #3

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
107430c
setup db, env
mckay1818 Jan 3, 2023
5c0b8e1
register customer and video models
mckay1818 Jan 3, 2023
dc6d5fb
create and register blueprints for Video and Customer models
mckay1818 Jan 3, 2023
803b531
create endpoints for reading all or 1 videos, creating new video
mckay1818 Jan 4, 2023
136d9f0
create new endpoint for replacing one video
mckay1818 Jan 4, 2023
c10b5ea
handle video CREATE requests with missing data
mckay1818 Jan 4, 2023
e1cded0
add validation for data in PUT requests
mckay1818 Jan 4, 2023
da9aa3d
create endpoint for deleting video by id
mckay1818 Jan 4, 2023
b77c3af
feat: Create read_all_customers end point
MelodyW2022 Jan 4, 2023
6a98616
feat: Create create_one_customer end point
MelodyW2022 Jan 4, 2023
88b8ac3
Correct the return statement of create_one_customer
MelodyW2022 Jan 4, 2023
13f74a1
extract validation of request body keys from Video class into routes
mckay1818 Jan 5, 2023
08aa6a0
Merge pull request #2 from mckay1818/MW_wave01
mckay1818 Jan 5, 2023
48f63a6
refactored Customer model to automatically assign registered_at time
mckay1818 Jan 5, 2023
3c336db
removed unecessary import
mckay1818 Jan 5, 2023
a93d22b
resolve merge conflicts
mckay1818 Jan 5, 2023
1ff816b
Merge branch 'main' into videos-routes
MelodyW2022 Jan 5, 2023
48f40fb
Merge pull request #1 from mckay1818/videos-routes
MelodyW2022 Jan 5, 2023
d3a90e2
Merge branch 'main' into video_customer_routes_refactoring
MelodyW2022 Jan 5, 2023
281a74b
Merge pull request #3 from mckay1818/video_customer_routes_refactoring
MelodyW2022 Jan 5, 2023
0253c7c
feat: Create 2 GET endpoints and 1 POST endpoint
MelodyW2022 Jan 5, 2023
9b37a6e
feat: Create PUT and DELETE endpoints
MelodyW2022 Jan 5, 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
3 changes: 3 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,8 @@ def create_app(test_config=None):
migrate.init_app(app, db)

#Register Blueprints Here
from .routes import videos_bp, customers_bp
app.register_blueprint(videos_bp)
app.register_blueprint(customers_bp)

return app
21 changes: 21 additions & 0 deletions app/models/customer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,24 @@

class Customer(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String)

registered_at = db.Column(db.DateTime(timezone=True), server_default=db.func.now(), nullable=False)
postal_code = db.Column(db.String)
phone = db.Column(db.String)

def to_dict(self):
return {
"id": self.id,
"name": self.name,
"postal_code": self.postal_code,
"phone": self.phone
}

@classmethod
def from_dict(cls, customer_data):
new_customer = Customer(name=customer_data["name"],
postal_code = customer_data["postal_code"],
phone = customer_data["phone"])
return new_customer

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

class Video(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String)
release_date = db.Column(db.DateTime)
total_inventory = db.Column(db.Integer)

def to_dict(self):
return {
"id": self.id,
"title": self.title,
"release_date": self.release_date,
"total_inventory": self.total_inventory
}

@classmethod
def from_dict(cls, video_data):
new_video = Video(title=video_data["title"], release_date=video_data["release_date"], total_inventory=video_data["total_inventory"])
return new_video


148 changes: 148 additions & 0 deletions app/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
from app import db
from app.models.customer import Customer
from app.models.video import Video
from flask import Blueprint, jsonify, abort, make_response, request

videos_bp = Blueprint("videos", __name__, url_prefix="/videos")
customers_bp = Blueprint("customers", __name__, url_prefix="/customers")

def validate_video_id(video_id):
try:
video_id = int(video_id)
except:
abort(make_response(jsonify({"message": f"Video {video_id} is invalid"}), 400))

video = Video.query.get(video_id)
if not video:
abort(make_response(jsonify({"message": f"Video {video_id} was not found"}), 404))
return video

def validate_model_data_and_create_obj(cls, model_data):
try:
new_obj = cls.from_dict(model_data)
except KeyError as e:
key = str(e).strip("\'")
abort(make_response(jsonify({"details": f"Request body must include {key}."}), 400))
return new_obj

def validate_model(cls, model_id):

try:
model_id = int(model_id)
except:
# handling invalid planet id type
abort(make_response({"message":f"{cls.__name__} {model_id} was 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} was not found"}, 404))
return model

# GET
@customers_bp.route("",methods = ["GET"])
def read_all_customers():
customers = Customer.query.all()
customers_response = []
for customer in customers:
customers_response.append(customer.to_dict())
return jsonify(customers_response)

@customers_bp.route("/<customer_id>", methods=["GET"])
def get_one_customer(customer_id):
customer = validate_model(Customer, customer_id)
return customer.to_dict()

# POST
@customers_bp.route("", methods = ["POST"])
def create_one_customer():
request_body = request.get_json()
new_customer = validate_model_data_and_create_obj(Customer, request_body)

db.session.add(new_customer)
db.session.commit()

return new_customer.to_dict(), 201

# PUT
@customers_bp.route("/<customer_id>", methods=["PUT"])
def update_one_customer(customer_id):
customer = validate_model(Customer, customer_id)
request_body = request.get_json()
try:
customer.name = request_body["name"]
customer.phone = request_body["phone"]
customer.postal_code = request_body["postal_code"]

# #TODO: refactor this out of Video model, into sep fn in routes
except KeyError as e:
key = str(e).strip("\'")
abort(make_response(jsonify({"details": f"Request body must include {key}."}), 400))

db.session.commit()
return {
"id" : customer.id,
"name": customer.name,
"postal_code": customer.postal_code,
"phone": customer.phone
}

# DELETE
@customers_bp.route("/<customer_id>", methods=["DELETE"])
def delete_one_customer(customer_id):
customer = validate_model(Customer,customer_id)
db.session.delete(customer)
db.session.commit()
return customer.to_dict()

@videos_bp.route("", methods=["POST"])
def create_video():
request_body = request.get_json()
new_video = validate_model_data_and_create_obj(Video, request_body)

db.session.add(new_video)
db.session.commit()

return new_video.to_dict(), 201

@videos_bp.route("", methods=["GET"])
def get_all_videos():
#add logic for filtering by query params
videos = Video.query.all()
videos_response = []
for video in videos:
videos_response.append(video.to_dict())
return jsonify(videos_response)

@videos_bp.route("/<video_id>", methods=["GET"])
def get_one_video(video_id):
video = validate_video_id(video_id)
return video.to_dict()

@videos_bp.route("/<video_id>", methods=["PUT"])
def update_one_video(video_id):
video = validate_video_id(video_id)
request_body = request.get_json()
try:
video.title = request_body["title"]
video.release_date = request_body["release_date"]
video.total_inventory = request_body["total_inventory"]

# #TODO: refactor this out of Video model, into sep fn in routes
except KeyError as e:
key = str(e).strip("\'")
abort(make_response(jsonify({"details": f"Request body must include {key}."}), 400))

db.session.commit()
return video.to_dict(), 200

@videos_bp.route("/<video_id>", methods=["DELETE"])
def delete_one_video(video_id):
video = validate_video_id(video_id)

db.session.delete(video)
db.session.commit()

return video.to_dict(), 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