From 034283ad4f8b06a1036b06765481a60281526718 Mon Sep 17 00:00:00 2001 From: Sarah Koch Date: Fri, 18 Oct 2024 11:54:20 -0700 Subject: [PATCH 01/14] Passes Wave 1. Created virtual environment, installed dependencies, defined Planet class and hardcoded a list of instances, and created an endpoint to get all existing planets. Co-authored-by: Charday Neal --- app/__init__.py | 3 +++ app/planets.py | 13 +++++++++++++ app/routes.py | 16 ++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 app/planets.py diff --git a/app/__init__.py b/app/__init__.py index 70b4cabfe..4cb489d0f 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,7 +1,10 @@ from flask import Flask +from .routes import planets_bp def create_app(test_config=None): app = Flask(__name__) + app.register_blueprint(planets_bp) + return app diff --git a/app/planets.py b/app/planets.py new file mode 100644 index 000000000..c955faa03 --- /dev/null +++ b/app/planets.py @@ -0,0 +1,13 @@ +class Planet: + def __init__(self, id, name, description, moons): + self.id = id + self.name = name + self.description = description + self.moons = moons + +planets = [ + Planet(1, "Mercury", "Small, hot, fast", []), + Planet(2, "Venus", "Thick atmosphere", []), + Planet(3, "Earth", "Supports life, very watery", ["Moon"]), + Planet(4, "Mars", "Red planet, biggest volcano", ["Phobos", "Deimos"]) +] \ No newline at end of file diff --git a/app/routes.py b/app/routes.py index 8e9dfe684..b8825b53c 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,2 +1,18 @@ from flask import Blueprint +from .planets import planets +planets_bp = Blueprint('planets_bp', __name__, url_prefix='/planets') + +@planets_bp.get('') +def get_all_planets(): + planets_list = [] + for planet in planets: + planets_list.append( + { + 'id': planet.id, + 'title': planet.name, + 'description': planet.description, + 'moons': planet.moons + } + ) + return planets_list \ No newline at end of file From 11b0eab37c8da5c50e24390c8405ee6e04d77e3a Mon Sep 17 00:00:00 2001 From: Charday Neal Date: Mon, 21 Oct 2024 12:00:39 -0700 Subject: [PATCH 02/14] Complete wave 02. Add new get endpoint to planets blueprint --- app/planets.py | 10 +++++++++- app/routes.py | 32 +++++++++++++++++++++----------- requirements.txt | 9 +++++++-- 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/app/planets.py b/app/planets.py index c955faa03..c57cbf375 100644 --- a/app/planets.py +++ b/app/planets.py @@ -5,9 +5,17 @@ def __init__(self, id, name, description, moons): self.description = description self.moons = moons + def to_dict(self): + return { + "id": self.id, + "name": self.name, + "description": self.description, + "moons": self.moons + } + planets = [ Planet(1, "Mercury", "Small, hot, fast", []), Planet(2, "Venus", "Thick atmosphere", []), Planet(3, "Earth", "Supports life, very watery", ["Moon"]), Planet(4, "Mars", "Red planet, biggest volcano", ["Phobos", "Deimos"]) -] \ No newline at end of file +] diff --git a/app/routes.py b/app/routes.py index b8825b53c..d0b07e540 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,18 +1,28 @@ -from flask import Blueprint +from flask import Blueprint, abort, make_response from .planets import planets planets_bp = Blueprint('planets_bp', __name__, url_prefix='/planets') @planets_bp.get('') def get_all_planets(): - planets_list = [] + return [planet.to_dict() for planet in planets], 200 + +@planets_bp.get('/') +def get_one_planet(planet_id): + planet = validate_planet(planet_id) + + return planet.to_dict(), 200 + +def validate_planet(planet_id): + try: + planet_id = int(planet_id) + except ValueError: + response = {"message": f"planet {planet_id} invalid"} + abort(make_response(response, 400)) + for planet in planets: - planets_list.append( - { - 'id': planet.id, - 'title': planet.name, - 'description': planet.description, - 'moons': planet.moons - } - ) - return planets_list \ No newline at end of file + if planet.id == planet_id: + return planet + + response = {"message": f"planet {planet_id} not found"} + abort(make_response(response, 404)) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 24c7e56f8..4973997aa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,17 +1,21 @@ alembic==1.13.1 autopep8==1.5.5 -blinker==1.7 +blinker==1.7.0 certifi==2020.12.5 chardet==4.0.0 click==8.1.7 +coverage==7.6.4 Flask==3.0.2 Flask-Migrate==4.0.5 Flask-SQLAlchemy==3.1.1 idna==2.10 +iniconfig==2.0.0 itsdangerous==2.1.2 Jinja2==3.1.3 Mako==1.1.4 MarkupSafe==2.1.5 +packaging==24.1 +pluggy==1.5.0 psycopg2-binary==2.9.9 pycodestyle==2.6.0 pytest==8.0.0 @@ -23,5 +27,6 @@ requests==2.25.1 six==1.15.0 SQLAlchemy==2.0.25 toml==0.10.2 +typing_extensions==4.12.2 urllib3==1.26.4 -Werkzeug==3.0.1 \ No newline at end of file +Werkzeug==3.0.1 From 70f0a3ac23610361d31e6a96b871c5644ee2a977 Mon Sep 17 00:00:00 2001 From: Priyanka Date: Fri, 25 Oct 2024 12:03:34 -0700 Subject: [PATCH 03/14] Correct code for wave1 and wave2 from PR feedback --- app/__init__.py | 2 +- app/models/__init__.py | 0 app/{planets.py => models/planet.py} | 0 app/routes/__init__.py | 0 app/{routes.py => routes/planet_routes.py} | 12 +++++------- requirements.txt | 9 ++------- 6 files changed, 8 insertions(+), 15 deletions(-) create mode 100644 app/models/__init__.py rename app/{planets.py => models/planet.py} (100%) create mode 100644 app/routes/__init__.py rename app/{routes.py => routes/planet_routes.py} (59%) diff --git a/app/__init__.py b/app/__init__.py index 4cb489d0f..6642cb6bf 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,5 +1,5 @@ from flask import Flask -from .routes import planets_bp +from .routes.planet_routes import planets_bp def create_app(test_config=None): diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/app/planets.py b/app/models/planet.py similarity index 100% rename from app/planets.py rename to app/models/planet.py diff --git a/app/routes/__init__.py b/app/routes/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/app/routes.py b/app/routes/planet_routes.py similarity index 59% rename from app/routes.py rename to app/routes/planet_routes.py index d0b07e540..60ded3fa1 100644 --- a/app/routes.py +++ b/app/routes/planet_routes.py @@ -1,28 +1,26 @@ from flask import Blueprint, abort, make_response -from .planets import planets +from app.models.planet import planets planets_bp = Blueprint('planets_bp', __name__, url_prefix='/planets') @planets_bp.get('') def get_all_planets(): - return [planet.to_dict() for planet in planets], 200 + return [planet.to_dict() for planet in planets] @planets_bp.get('/') def get_one_planet(planet_id): planet = validate_planet(planet_id) - return planet.to_dict(), 200 + return planet.to_dict() def validate_planet(planet_id): try: planet_id = int(planet_id) except ValueError: - response = {"message": f"planet {planet_id} invalid"} - abort(make_response(response, 400)) + abort(make_response({"message": f"planet {planet_id} invalid"}, 400)) for planet in planets: if planet.id == planet_id: return planet - response = {"message": f"planet {planet_id} not found"} - abort(make_response(response, 404)) \ No newline at end of file + abort(make_response({"message": f"planet {planet_id} not found"}, 404)) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 4973997aa..24c7e56f8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,21 +1,17 @@ alembic==1.13.1 autopep8==1.5.5 -blinker==1.7.0 +blinker==1.7 certifi==2020.12.5 chardet==4.0.0 click==8.1.7 -coverage==7.6.4 Flask==3.0.2 Flask-Migrate==4.0.5 Flask-SQLAlchemy==3.1.1 idna==2.10 -iniconfig==2.0.0 itsdangerous==2.1.2 Jinja2==3.1.3 Mako==1.1.4 MarkupSafe==2.1.5 -packaging==24.1 -pluggy==1.5.0 psycopg2-binary==2.9.9 pycodestyle==2.6.0 pytest==8.0.0 @@ -27,6 +23,5 @@ requests==2.25.1 six==1.15.0 SQLAlchemy==2.0.25 toml==0.10.2 -typing_extensions==4.12.2 urllib3==1.26.4 -Werkzeug==3.0.1 +Werkzeug==3.0.1 \ No newline at end of file From 0e7c2a91d14ab2fafbebf006f031d6f4a42e091d Mon Sep 17 00:00:00 2001 From: Priyanka Date: Fri, 25 Oct 2024 12:45:08 -0700 Subject: [PATCH 04/14] Removed hardcoded data. Created project database. Connected the database and Flask. Defined Book Model. Imported model explicitly. Completed initial setup of the database on the Flask end. Generated and applied migration files adding Book model. --- app/__init__.py | 10 ++++++-- app/db.py | 7 ++++++ app/models/base.py | 4 +++ app/models/planet.py | 49 +++++++++++++++++++++++-------------- app/routes/planet_routes.py | 34 ++++++++++++------------- 5 files changed, 66 insertions(+), 38 deletions(-) create mode 100644 app/db.py create mode 100644 app/models/base.py diff --git a/app/__init__.py b/app/__init__.py index 6642cb6bf..9fbd3e292 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,10 +1,16 @@ from flask import Flask +from .db import db, migrate from .routes.planet_routes import planets_bp - def create_app(test_config=None): app = Flask(__name__) + app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql+psycopg2://postgres:postgres@localhost:5432/solar_system_development' + + db.init_app(app) + migrate.init_app(app, db) + app.register_blueprint(planets_bp) - return app + return app \ No newline at end of file diff --git a/app/db.py b/app/db.py new file mode 100644 index 000000000..17243b274 --- /dev/null +++ b/app/db.py @@ -0,0 +1,7 @@ +from flask_sqlalchemy import SQLAlchemy +from flask_migrate import Migrate +from .models.base import Base + + +db = SQLAlchemy(model_class=Base) +migrate = Migrate() \ No newline at end of file diff --git a/app/models/base.py b/app/models/base.py new file mode 100644 index 000000000..f75ec448b --- /dev/null +++ b/app/models/base.py @@ -0,0 +1,4 @@ +from sqlalchemy.orm import DeclarativeBase + +class Base(DeclarativeBase): + pass diff --git a/app/models/planet.py b/app/models/planet.py index c57cbf375..6e18001f2 100644 --- a/app/models/planet.py +++ b/app/models/planet.py @@ -1,21 +1,32 @@ -class Planet: - def __init__(self, id, name, description, moons): - self.id = id - self.name = name - self.description = description - self.moons = moons +from app.db import db +from sqlalchemy.orm import Mapped, mapped_column - def to_dict(self): - return { - "id": self.id, - "name": self.name, - "description": self.description, - "moons": self.moons - } +# class Planet: +# def __init__(self, id, name, description, moons): +# self.id = id +# self.name = name +# self.description = description +# self.moons = moons + + # vvv don't delete, we'll use this vvv + # def to_dict(self): + # return { + # "id": self.id, + # "name": self.name, + # "description": self.description, + # "moons": self.moons + # } + +# planets = [ +# Planet(1, "Mercury", "Small, hot, fast", []), +# Planet(2, "Venus", "Thick atmosphere", []), +# Planet(3, "Earth", "Supports life, very watery", ["Moon"]), +# Planet(4, "Mars", "Red planet, biggest volcano", ["Phobos", "Deimos"]) +# ] + +class Planet(db.Model): + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + name: Mapped[str] + description: Mapped[str] + number_of_moons: Mapped[int] -planets = [ - Planet(1, "Mercury", "Small, hot, fast", []), - Planet(2, "Venus", "Thick atmosphere", []), - Planet(3, "Earth", "Supports life, very watery", ["Moon"]), - Planet(4, "Mars", "Red planet, biggest volcano", ["Phobos", "Deimos"]) -] diff --git a/app/routes/planet_routes.py b/app/routes/planet_routes.py index 60ded3fa1..cb58537e3 100644 --- a/app/routes/planet_routes.py +++ b/app/routes/planet_routes.py @@ -1,26 +1,26 @@ from flask import Blueprint, abort, make_response -from app.models.planet import planets +from ..models.planet import Planet planets_bp = Blueprint('planets_bp', __name__, url_prefix='/planets') -@planets_bp.get('') -def get_all_planets(): - return [planet.to_dict() for planet in planets] +# @planets_bp.get('') +# def get_all_planets(): +# return [planet.to_dict() for planet in planets] -@planets_bp.get('/') -def get_one_planet(planet_id): - planet = validate_planet(planet_id) +# @planets_bp.get('/') +# def get_one_planet(planet_id): +# planet = validate_planet(planet_id) - return planet.to_dict() +# return planet.to_dict() -def validate_planet(planet_id): - try: - planet_id = int(planet_id) - except ValueError: - abort(make_response({"message": f"planet {planet_id} invalid"}, 400)) +# def validate_planet(planet_id): +# try: +# planet_id = int(planet_id) +# except ValueError: +# abort(make_response({"message": f"planet {planet_id} invalid"}, 400)) - for planet in planets: - if planet.id == planet_id: - return planet +# for planet in planets: +# if planet.id == planet_id: +# return planet - abort(make_response({"message": f"planet {planet_id} not found"}, 404)) \ No newline at end of file +# abort(make_response({"message": f"planet {planet_id} not found"}, 404)) \ No newline at end of file From db19c6686bc2250748318205826862ccf973f58c Mon Sep 17 00:00:00 2001 From: Priyanka Date: Fri, 25 Oct 2024 15:24:44 -0700 Subject: [PATCH 05/14] Initialize migration, generate migration and apply migration to database --- migrations/README | 1 + migrations/alembic.ini | 50 ++++++++ migrations/env.py | 113 ++++++++++++++++++ migrations/script.py.mako | 24 ++++ .../6e0689b72307_add_planets_class.py | 34 ++++++ 5 files changed, 222 insertions(+) create mode 100644 migrations/README create mode 100644 migrations/alembic.ini create mode 100644 migrations/env.py create mode 100644 migrations/script.py.mako create mode 100644 migrations/versions/6e0689b72307_add_planets_class.py diff --git a/migrations/README b/migrations/README new file mode 100644 index 000000000..0e0484415 --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 000000000..ec9d45c26 --- /dev/null +++ b/migrations/alembic.ini @@ -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 diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 000000000..4c9709271 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,113 @@ +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') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except (TypeError, AttributeError): + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# 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', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# 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 get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +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=get_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.') + + conf_args = current_app.extensions['migrate'].configure_args + if conf_args.get("process_revision_directives") is None: + conf_args["process_revision_directives"] = process_revision_directives + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + **conf_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 000000000..2c0156303 --- /dev/null +++ b/migrations/script.py.mako @@ -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"} diff --git a/migrations/versions/6e0689b72307_add_planets_class.py b/migrations/versions/6e0689b72307_add_planets_class.py new file mode 100644 index 000000000..f351f7648 --- /dev/null +++ b/migrations/versions/6e0689b72307_add_planets_class.py @@ -0,0 +1,34 @@ +"""Add planets class + +Revision ID: 6e0689b72307 +Revises: +Create Date: 2024-10-25 15:17:01.101835 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '6e0689b72307' +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('number_of_moons', sa.Integer(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('planet') + # ### end Alembic commands ### From 21981416624a24c907d57740ad06301e63e56d22 Mon Sep 17 00:00:00 2001 From: Priyanka Date: Fri, 25 Oct 2024 16:17:17 -0700 Subject: [PATCH 06/14] Complete wave 3 implementation. --- app/models/planet.py | 8 +++++++- app/routes/planet_routes.py | 26 ++++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/app/models/planet.py b/app/models/planet.py index 6e18001f2..66996101c 100644 --- a/app/models/planet.py +++ b/app/models/planet.py @@ -8,7 +8,6 @@ # self.description = description # self.moons = moons - # vvv don't delete, we'll use this vvv # def to_dict(self): # return { # "id": self.id, @@ -30,3 +29,10 @@ class Planet(db.Model): description: Mapped[str] number_of_moons: Mapped[int] + def to_dict(self): + return { + "id": self.id, + "name": self.name, + "description": self.description, + "number_of_moons": self.number_of_moons + } \ No newline at end of file diff --git a/app/routes/planet_routes.py b/app/routes/planet_routes.py index cb58537e3..71af3c5b6 100644 --- a/app/routes/planet_routes.py +++ b/app/routes/planet_routes.py @@ -1,5 +1,6 @@ -from flask import Blueprint, abort, make_response +from flask import Blueprint, abort, make_response, request from ..models.planet import Planet +from ..db import db planets_bp = Blueprint('planets_bp', __name__, url_prefix='/planets') @@ -23,4 +24,25 @@ # if planet.id == planet_id: # return planet -# abort(make_response({"message": f"planet {planet_id} not found"}, 404)) \ No newline at end of file +# abort(make_response({"message": f"planet {planet_id} not found"}, 404)) + +@planets_bp.post("") +def create_planet(): + request_body = request.get_json() + + name = request_body["name"] + description = request_body["description"] + number_of_moons = request_body["number_of_moons"] + new_planet = Planet(name=name, description=description, number_of_moons=number_of_moons) + + db.session.add(new_planet) + db.session.commit() + + return new_planet.to_dict(), 201 + +@planets_bp.get("") +def get_all_planets(): + query = db.select(Planet).order_by(Planet.id) + planets = db.session.scalars(query) + + return [planet.to_dict() for planet in planets] \ No newline at end of file From 7ac8d4d629f40ac51687c5afa7c30dfda019b5b6 Mon Sep 17 00:00:00 2001 From: Priyanka Date: Fri, 25 Oct 2024 16:55:18 -0700 Subject: [PATCH 07/14] Implement validate_planet function to use in get_one_planet endpoint. --- app/routes/planet_routes.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/app/routes/planet_routes.py b/app/routes/planet_routes.py index 71af3c5b6..b95585fe9 100644 --- a/app/routes/planet_routes.py +++ b/app/routes/planet_routes.py @@ -45,4 +45,26 @@ def get_all_planets(): query = db.select(Planet).order_by(Planet.id) planets = db.session.scalars(query) - return [planet.to_dict() for planet in planets] \ No newline at end of file + return [planet.to_dict() for planet in planets] + +@planets_bp.get('/') +def get_one_planet(planet_id): + return validate_planet(planet_id).to_dict() + +def validate_planet(planet_id): + try: + planet_id = int(planet_id) + except ValueError: + abort(make_response({"message": f"planet {planet_id} invalid"}, 400)) + + # for planet in planets: + # if planet.id == planet_id: + # return planet + + # abort(make_response({"message": f"planet {planet_id} not found"}, 404)) + + planet = db.session.get(Planet, planet_id) + if planet is None: + abort(make_response({"message": f"planet {planet_id} not found"}, 404)) + + return planet \ No newline at end of file From 517a1e2f13df4da222471fd8d96a604c4986be11 Mon Sep 17 00:00:00 2001 From: Sarah Koch Date: Mon, 28 Oct 2024 16:21:12 -0700 Subject: [PATCH 08/14] Updated code to comply with PEP8 standards. Changed import statements to use absolute imports. Reorganized import statements, grouped by source and sorted alphabetically within groups. Removed old, unused code. Ensured consistency in the use of single and double quotes in strings. --- app/__init__.py | 8 ++++---- app/db.py | 3 +-- app/models/planet.py | 24 +----------------------- app/routes/planet_routes.py | 36 ++++-------------------------------- 4 files changed, 10 insertions(+), 61 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 9fbd3e292..4731cb489 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,12 +1,12 @@ from flask import Flask -from .db import db, migrate -from .routes.planet_routes import planets_bp +from app.db import db, migrate +from app.routes.planet_routes import planets_bp def create_app(test_config=None): app = Flask(__name__) - app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False - app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql+psycopg2://postgres:postgres@localhost:5432/solar_system_development' + app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False + app.config["SQLALCHEMY_DATABASE_URI"] = "postgresql+psycopg2://postgres:postgres@localhost:5432/solar_system_development" db.init_app(app) migrate.init_app(app, db) diff --git a/app/db.py b/app/db.py index 17243b274..24dc45f31 100644 --- a/app/db.py +++ b/app/db.py @@ -1,7 +1,6 @@ from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate -from .models.base import Base - +from app.models.base import Base db = SQLAlchemy(model_class=Base) migrate = Migrate() \ No newline at end of file diff --git a/app/models/planet.py b/app/models/planet.py index 66996101c..bd7cdff83 100644 --- a/app/models/planet.py +++ b/app/models/planet.py @@ -1,27 +1,5 @@ -from app.db import db from sqlalchemy.orm import Mapped, mapped_column - -# class Planet: -# def __init__(self, id, name, description, moons): -# self.id = id -# self.name = name -# self.description = description -# self.moons = moons - - # def to_dict(self): - # return { - # "id": self.id, - # "name": self.name, - # "description": self.description, - # "moons": self.moons - # } - -# planets = [ -# Planet(1, "Mercury", "Small, hot, fast", []), -# Planet(2, "Venus", "Thick atmosphere", []), -# Planet(3, "Earth", "Supports life, very watery", ["Moon"]), -# Planet(4, "Mars", "Red planet, biggest volcano", ["Phobos", "Deimos"]) -# ] +from app.db import db class Planet(db.Model): id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) diff --git a/app/routes/planet_routes.py b/app/routes/planet_routes.py index b95585fe9..33d01198b 100644 --- a/app/routes/planet_routes.py +++ b/app/routes/planet_routes.py @@ -1,30 +1,8 @@ from flask import Blueprint, abort, make_response, request -from ..models.planet import Planet -from ..db import db +from app.db import db +from app.models.planet import Planet -planets_bp = Blueprint('planets_bp', __name__, url_prefix='/planets') - -# @planets_bp.get('') -# def get_all_planets(): -# return [planet.to_dict() for planet in planets] - -# @planets_bp.get('/') -# def get_one_planet(planet_id): -# planet = validate_planet(planet_id) - -# return planet.to_dict() - -# def validate_planet(planet_id): -# try: -# planet_id = int(planet_id) -# except ValueError: -# abort(make_response({"message": f"planet {planet_id} invalid"}, 400)) - -# for planet in planets: -# if planet.id == planet_id: -# return planet - -# abort(make_response({"message": f"planet {planet_id} not found"}, 404)) +planets_bp = Blueprint("planets_bp", __name__, url_prefix="/planets") @planets_bp.post("") def create_planet(): @@ -47,7 +25,7 @@ def get_all_planets(): return [planet.to_dict() for planet in planets] -@planets_bp.get('/') +@planets_bp.get("/") def get_one_planet(planet_id): return validate_planet(planet_id).to_dict() @@ -57,12 +35,6 @@ def validate_planet(planet_id): except ValueError: abort(make_response({"message": f"planet {planet_id} invalid"}, 400)) - # for planet in planets: - # if planet.id == planet_id: - # return planet - - # abort(make_response({"message": f"planet {planet_id} not found"}, 404)) - planet = db.session.get(Planet, planet_id) if planet is None: abort(make_response({"message": f"planet {planet_id} not found"}, 404)) From babd8378064d900d9566952748d01694db740f5a Mon Sep 17 00:00:00 2001 From: Sarah Koch Date: Tue, 29 Oct 2024 12:01:48 -0700 Subject: [PATCH 09/14] Updated validation to access database. Added functionality to update and delete a planet. Co-authored-by: Pri Co-authored-by: Charday Neal --- app/routes/planet_routes.py | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/app/routes/planet_routes.py b/app/routes/planet_routes.py index 33d01198b..4d44eefbf 100644 --- a/app/routes/planet_routes.py +++ b/app/routes/planet_routes.py @@ -1,4 +1,4 @@ -from flask import Blueprint, abort, make_response, request +from flask import Blueprint, abort, make_response, request, Response from app.db import db from app.models.planet import Planet @@ -29,14 +29,38 @@ def get_all_planets(): def get_one_planet(planet_id): return validate_planet(planet_id).to_dict() +@planets_bp.put("/") +def update_planet(planet_id): + planet = validate_planet(planet_id) + request_body = request.get_json() + + planet.name = request_body["name"] + planet.description = request_body["description"] + planet.number_of_moons = request_body["number_of_moons"] + + db.session.commit() + + return Response(status=204, mimetype='application/json') + +@planets_bp.delete("/") +def delete_planet(planet_id): + planet = validate_planet(planet_id) + + db.session.delete(planet) + db.session.commit() + + return Response(status=204, mimetype='application/json') + def validate_planet(planet_id): try: planet_id = int(planet_id) except ValueError: abort(make_response({"message": f"planet {planet_id} invalid"}, 400)) - planet = db.session.get(Planet, planet_id) - if planet is None: + query = db.select(Planet).where(Planet.id == planet_id) + planet = db.session.scalar(query) + + if not planet: abort(make_response({"message": f"planet {planet_id} not found"}, 404)) return planet \ No newline at end of file From ac86dfe9fdb798e99d53ba45c5eefb36dddb98c2 Mon Sep 17 00:00:00 2001 From: Charday Neal Date: Wed, 30 Oct 2024 11:56:27 -0700 Subject: [PATCH 10/14] Refactor code. Update get_all_planets route to consider query params. Add seed data for database. --- app/routes/planet_routes.py | 20 +++++++++++++++++--- seed.py | 19 +++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 seed.py diff --git a/app/routes/planet_routes.py b/app/routes/planet_routes.py index 4d44eefbf..aa1e4e9f5 100644 --- a/app/routes/planet_routes.py +++ b/app/routes/planet_routes.py @@ -20,7 +20,21 @@ def create_planet(): @planets_bp.get("") def get_all_planets(): - query = db.select(Planet).order_by(Planet.id) + description_param = request.args.get("description") + number_of_moons_param = request.args.get("number_of_moons") + + query = db.select(Planet) + if description_param: + query = query.where(Planet.description.ilike(f"%{description_param}%")) + + if number_of_moons_param: + try: + number_of_moons_param = int(number_of_moons_param) + query = query.where(Planet.number_of_moons == number_of_moons_param) + except ValueError: + abort(make_response({"message": f"{number_of_moons_param} expected int type"}, 400)) + + query = query.order_by(Planet.id) planets = db.session.scalars(query) return [planet.to_dict() for planet in planets] @@ -40,7 +54,7 @@ def update_planet(planet_id): db.session.commit() - return Response(status=204, mimetype='application/json') + return Response(status=204, mimetype="application/json") @planets_bp.delete("/") def delete_planet(planet_id): @@ -49,7 +63,7 @@ def delete_planet(planet_id): db.session.delete(planet) db.session.commit() - return Response(status=204, mimetype='application/json') + return Response(status=204, mimetype="application/json") def validate_planet(planet_id): try: diff --git a/seed.py b/seed.py new file mode 100644 index 000000000..66ce474d9 --- /dev/null +++ b/seed.py @@ -0,0 +1,19 @@ +from app import create_app, db +from app.models.planet import Planet + +my_app = create_app() +with my_app.app_context(): + db.session.add(Planet(name="Mercury", description="The intelligent strategist, known for her analytical skills and love of books.", number_of_moons=0)), + db.session.add(Planet(name="Venus", description="The charming beauty who believes in love, often the peacemaker among her friends.", number_of_moons=0)), + db.session.add(Planet(name="Earth", description="The cheerful leader who fights for love and justice, often clumsy but always brave.", number_of_moons=1)), + db.session.add(Planet(name="Mars", description="The passionate warrior with a strong sense of justice and a fiery spirit.", number_of_moons=2)), + db.session.add(Planet(name="Jupiter", description="The tough protector with a big heart, skilled in combat and cooking.", number_of_moons=95)), + db.session.add(Planet(name="Saturn", description="The mysterious guardian with powerful abilities, often associated with destruction and rebirth.", number_of_moons=83)), + db.session.add(Planet(name="Uranus", description="The confident and adventurous rebel, known for her speed and agility.", number_of_moons=27)), + db.session.add(Planet(name="Neptune", description="The elegant and artistic fighter, with a deep connection to the ocean and intuition.", number_of_moons=14)), + db.session.add(Planet(name="Pluto", description="The wise time guardian, responsible for protecting the gates of time and space.", number_of_moons=5)), + db.session.add(Planet(name="Eris", description="The energetic and playful guardian, spreading warmth and positivity wherever she goes.", number_of_moons=1)), + db.session.add(Planet(name="Haumea", description="The strategic thinker who uses her intelligence to solve problems and protect her friends.", number_of_moons=2)), + db.session.add(Planet(name="Makemake", description="The fierce protector with strong instincts, known for her loyalty and bravery.", number_of_moons=1)), + db.session.add(Planet(name="Ceres", description="The nurturing spirit who brings harmony and balance, always caring for her friends.", number_of_moons=0)), + db.session.commit() \ No newline at end of file From 7b738e0dfc34a4b2123ac82f70733b02f36bd963 Mon Sep 17 00:00:00 2001 From: Priyanka Date: Thu, 31 Oct 2024 12:23:29 -0700 Subject: [PATCH 11/14] "test_planet_routes" "Add wave 6 fixtures and pytest to validate get one planet-found, get one planet-not found, get all planet-empty-db, get all planet- db has 2 entries, post one planet. Add two_saved_planets to database." --- tests/__init__.py | 0 tests/conftest.py | 44 ++++++++++++++++++++++++++ tests/test_planet_routes.py | 61 +++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_planet_routes.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..55570ba3e --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,44 @@ +import pytest +from app import create_app +from app.db import db +from flask.signals import request_finished +from dotenv import load_dotenv +import os +from app.models.planet import Planet + +load_dotenv() + +@pytest.fixture +def app(): + test_config = { + "TESTING": True, + "SQLALCHEMY_DATABASE_URI": os.environ.get("SQLALCHEMY_TEST_DATABASE_URI") + } + app = create_app(test_config) + + @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): + pluto = Planet(name="Pluto", + description="The wise time guardian, responsible for protecting the gates of time and space.", + number_of_moons=5) + mercury = Planet(name="Mercury", + description="The intelligent strategist, known for her analytical skills and love of books.", + number_of_moons=0) + + db.session.add_all([pluto, mercury]) + db.session.commit() \ No newline at end of file diff --git a/tests/test_planet_routes.py b/tests/test_planet_routes.py new file mode 100644 index 000000000..6d9adfda5 --- /dev/null +++ b/tests/test_planet_routes.py @@ -0,0 +1,61 @@ +def test_get_all_planets_with_no_records(client): + response = client.get("/planets") + response_body = response.get_json() + + assert response.status_code == 200 + assert response_body == [] + +def test_get_one_planet_found(client, two_saved_planets): + response = client.get("/planets/1") + response_body = response.get_json() + + assert response.status_code == 200 + assert response_body == { + "id": 1, + "name": "Pluto", + "description": "The wise time guardian, responsible for protecting the gates of time and space.", + "number_of_moons": 5 + } + +def test_get_one_planet_not_found(client): + response = client.get("/planets/1") + response_body = response.get_json() + + assert response.status_code == 404 + assert response_body == {"message": "planet 1 not found"} + +def test_get_all_planets_succeeds_with_records(client, two_saved_planets): + response = client.get("/planets") + response_body = response.get_json() + + assert response.status_code == 200 + assert response_body == [ + { + "id": 1, + "name": "Pluto", + "description": "The wise time guardian, responsible for protecting the gates of time and space.", + "number_of_moons": 5 + }, + { + "id": 2, + "name": "Mercury", + "description": "The intelligent strategist, known for her analytical skills and love of books.", + "number_of_moons": 0 + } + ] + +def test_create_one_planet(client): + response = client.post("/planets", json={ + "name": "Ceres", + "description": "The nurturing spirit who brings harmony and balance, always caring for her friends.", + "number_of_moons": 0 + }) + response_body = response.get_json() + + assert response.status_code == 201 + assert response_body == { + "id": 1, + "name": "Ceres", + "description": "The nurturing spirit who brings harmony and balance, always caring for her friends.", + "number_of_moons": 0 + } \ No newline at end of file From dd74ea235e16b368ceec82d461995f90b5cd1d9c Mon Sep 17 00:00:00 2001 From: Priyanka Date: Thu, 31 Oct 2024 12:27:14 -0700 Subject: [PATCH 12/14] Add app/__init__.py test_database configuration. --- app/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 4731cb489..d8870145b 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,12 +1,17 @@ from flask import Flask from app.db import db, migrate from app.routes.planet_routes import planets_bp +from app.models import planet +import os -def create_app(test_config=None): +def create_app(config=None): app = Flask(__name__) app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False - app.config["SQLALCHEMY_DATABASE_URI"] = "postgresql+psycopg2://postgres:postgres@localhost:5432/solar_system_development" + app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get("SQLALCHEMY_DATABASE_URI") + + if config: + app.config.update(config) db.init_app(app) migrate.init_app(app, db) From 60bea7471d95a4a2511475bbf81fe4a386076cfb Mon Sep 17 00:00:00 2001 From: Sarah Koch Date: Fri, 1 Nov 2024 00:08:42 -0700 Subject: [PATCH 13/14] Added constants file for storing and importing strings used repeatedly. Added helper functions to get query parameters from the client request, filter the query based on the client parameters, sort the response body by a client-supplied parameter, and supporting validation for data processed. --- app/models/planet.py | 9 ++-- app/routes/planet_routes.py | 89 ++++++++++++++++++++++++++----------- constants.py | 19 ++++++++ tests/test_planet_routes.py | 2 +- 4 files changed, 88 insertions(+), 31 deletions(-) create mode 100644 constants.py diff --git a/app/models/planet.py b/app/models/planet.py index bd7cdff83..ac2a81a64 100644 --- a/app/models/planet.py +++ b/app/models/planet.py @@ -1,5 +1,6 @@ from sqlalchemy.orm import Mapped, mapped_column from app.db import db +from constants import ID, NAME, DESCRIPTION, NUMBER_OF_MOONS class Planet(db.Model): id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) @@ -9,8 +10,8 @@ class Planet(db.Model): def to_dict(self): return { - "id": self.id, - "name": self.name, - "description": self.description, - "number_of_moons": self.number_of_moons + ID: self.id, + NAME: self.name, + DESCRIPTION: self.description, + NUMBER_OF_MOONS: self.number_of_moons } \ No newline at end of file diff --git a/app/routes/planet_routes.py b/app/routes/planet_routes.py index aa1e4e9f5..cca1121a3 100644 --- a/app/routes/planet_routes.py +++ b/app/routes/planet_routes.py @@ -1,6 +1,7 @@ from flask import Blueprint, abort, make_response, request, Response from app.db import db from app.models.planet import Planet +from constants import ID, NAME, DESCRIPTION, NUMBER_OF_MOONS, ORDER_BY, MESSAGE, MIMETYPE_JSON, INVALID, NOT_FOUND planets_bp = Blueprint("planets_bp", __name__, url_prefix="/planets") @@ -8,11 +9,12 @@ def create_planet(): request_body = request.get_json() - name = request_body["name"] - description = request_body["description"] - number_of_moons = request_body["number_of_moons"] - new_planet = Planet(name=name, description=description, number_of_moons=number_of_moons) - + new_planet = Planet( + name=request_body[NAME], + description=request_body[DESCRIPTION], + number_of_moons=request_body[NUMBER_OF_MOONS] + ) + db.session.add(new_planet) db.session.commit() @@ -20,21 +22,11 @@ def create_planet(): @planets_bp.get("") def get_all_planets(): - description_param = request.args.get("description") - number_of_moons_param = request.args.get("number_of_moons") - + query_params = get_query_params() query = db.select(Planet) - if description_param: - query = query.where(Planet.description.ilike(f"%{description_param}%")) - - if number_of_moons_param: - try: - number_of_moons_param = int(number_of_moons_param) - query = query.where(Planet.number_of_moons == number_of_moons_param) - except ValueError: - abort(make_response({"message": f"{number_of_moons_param} expected int type"}, 400)) - - query = query.order_by(Planet.id) + query = filter_query(query, query_params) + query = get_order_by_param(query) + planets = db.session.scalars(query) return [planet.to_dict() for planet in planets] @@ -48,13 +40,13 @@ def update_planet(planet_id): planet = validate_planet(planet_id) request_body = request.get_json() - planet.name = request_body["name"] - planet.description = request_body["description"] - planet.number_of_moons = request_body["number_of_moons"] + planet.name = request_body[NAME] + planet.description = request_body[DESCRIPTION] + planet.number_of_moons = request_body[NUMBER_OF_MOONS] db.session.commit() - return Response(status=204, mimetype="application/json") + return Response(status=204, mimetype=MIMETYPE_JSON) @planets_bp.delete("/") def delete_planet(planet_id): @@ -63,18 +55,63 @@ def delete_planet(planet_id): db.session.delete(planet) db.session.commit() - return Response(status=204, mimetype="application/json") + return Response(status=204, mimetype=MIMETYPE_JSON) + +def filter_query(query, params): + if params[ID]: + query = query.where(Planet.id == validate_cast_type(params[ID], int, ID)) + + if params[NAME]: + query = query.where(Planet.name == params[NAME]) + + if params[DESCRIPTION]: + query = query.where(Planet.description.ilike(f"%{params[DESCRIPTION]}%")) + + if params[NUMBER_OF_MOONS]: + query = query.where(Planet.number_of_moons == validate_cast_type( + params[NUMBER_OF_MOONS], int, NUMBER_OF_MOONS)) + + return query + +def get_query_params(): + return { + ID: request.args.get(ID), + NAME: request.args.get(NAME), + DESCRIPTION: request.args.get(DESCRIPTION), + NUMBER_OF_MOONS: request.args.get(NUMBER_OF_MOONS) + } + +def get_order_by_param(query): + order_by_param = request.args.get(ORDER_BY) + if order_by_param: + query = validate_order_by_param(query, order_by_param) + else: + query = query.order_by(Planet.id) + + return query + +def validate_cast_type(value, target_type, param_name): + try: + return target_type(value) + except (ValueError, TypeError): + abort(make_response({MESSAGE: f"{param_name} '{value}' {INVALID}"}, 400)) + +def validate_order_by_param(query, order_by_param): + try: + return query.order_by(getattr(Planet, order_by_param)) + except AttributeError: + abort(make_response({MESSAGE: f"{ORDER_BY} '{order_by_param}' {INVALID}"}, 400)) def validate_planet(planet_id): try: planet_id = int(planet_id) except ValueError: - abort(make_response({"message": f"planet {planet_id} invalid"}, 400)) + abort(make_response({MESSAGE: f"{ID} {planet_id} {INVALID}"}, 400)) query = db.select(Planet).where(Planet.id == planet_id) planet = db.session.scalar(query) if not planet: - abort(make_response({"message": f"planet {planet_id} not found"}, 404)) + abort(make_response({MESSAGE: f"{ID} {planet_id} {NOT_FOUND}"}, 404)) return planet \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 000000000..84d25d68d --- /dev/null +++ b/constants.py @@ -0,0 +1,19 @@ +''' +Constants for the Solar System API +''' + +# Keys +MESSAGE = "message" +ID = "id" +NAME = "name" +DESCRIPTION = "description" +NUMBER_OF_MOONS = "number_of_moons" +FIELDS = "fields" +ORDER_BY = "order_by" + +# MIME Types +MIMETYPE_JSON = "application/json" + +# Error messages +NOT_FOUND = "not found" +INVALID = "invalid" \ No newline at end of file diff --git a/tests/test_planet_routes.py b/tests/test_planet_routes.py index 6d9adfda5..0b3972233 100644 --- a/tests/test_planet_routes.py +++ b/tests/test_planet_routes.py @@ -22,7 +22,7 @@ def test_get_one_planet_not_found(client): response_body = response.get_json() assert response.status_code == 404 - assert response_body == {"message": "planet 1 not found"} + assert response_body == {"message": "id 1 not found"} def test_get_all_planets_succeeds_with_records(client, two_saved_planets): response = client.get("/planets") From cf0a16b8c952012aa91f9b7f7c7166d0edbed2ce Mon Sep 17 00:00:00 2001 From: Sarah Koch Date: Wed, 6 Nov 2024 11:49:00 -0800 Subject: [PATCH 14/14] added gunicorn to requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 24c7e56f8..4eb8d9cb2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,7 @@ click==8.1.7 Flask==3.0.2 Flask-Migrate==4.0.5 Flask-SQLAlchemy==3.1.1 +gunicorn==23.0.0 idna==2.10 itsdangerous==2.1.2 Jinja2==3.1.3