From f0b7a90b405662fd3bd456ca4dd0b2659d1794b9 Mon Sep 17 00:00:00 2001 From: Drew Bednar Date: Sun, 13 Feb 2022 13:23:40 -0500 Subject: [PATCH] Basic fastapi but sync model --- README.md | 0 fastapi-sqlmodel-alembic/.gitignore | 2 + fastapi-sqlmodel-alembic/LICENSE | 21 +++++++ fastapi-sqlmodel-alembic/README.md | 62 +++++++++++++++++++ fastapi-sqlmodel-alembic/docker-compose.yml | 24 +++++++ .../project/.dockerignore | 2 + fastapi-sqlmodel-alembic/project/Dockerfile | 22 +++++++ .../project/app/__init__.py | 0 fastapi-sqlmodel-alembic/project/app/db.py | 22 +++++++ fastapi-sqlmodel-alembic/project/app/main.py | 39 ++++++++++++ .../project/app/models.py | 28 +++++++++ .../project/requirements.txt | 4 ++ 12 files changed, 226 insertions(+) create mode 100644 README.md create mode 100644 fastapi-sqlmodel-alembic/.gitignore create mode 100644 fastapi-sqlmodel-alembic/LICENSE create mode 100644 fastapi-sqlmodel-alembic/README.md create mode 100644 fastapi-sqlmodel-alembic/docker-compose.yml create mode 100644 fastapi-sqlmodel-alembic/project/.dockerignore create mode 100644 fastapi-sqlmodel-alembic/project/Dockerfile create mode 100644 fastapi-sqlmodel-alembic/project/app/__init__.py create mode 100644 fastapi-sqlmodel-alembic/project/app/db.py create mode 100644 fastapi-sqlmodel-alembic/project/app/main.py create mode 100644 fastapi-sqlmodel-alembic/project/app/models.py create mode 100644 fastapi-sqlmodel-alembic/project/requirements.txt diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/fastapi-sqlmodel-alembic/.gitignore b/fastapi-sqlmodel-alembic/.gitignore new file mode 100644 index 0000000..9072039 --- /dev/null +++ b/fastapi-sqlmodel-alembic/.gitignore @@ -0,0 +1,2 @@ +__pycache__ +env diff --git a/fastapi-sqlmodel-alembic/LICENSE b/fastapi-sqlmodel-alembic/LICENSE new file mode 100644 index 0000000..5352154 --- /dev/null +++ b/fastapi-sqlmodel-alembic/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Michael Herman + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/fastapi-sqlmodel-alembic/README.md b/fastapi-sqlmodel-alembic/README.md new file mode 100644 index 0000000..d4b17c0 --- /dev/null +++ b/fastapi-sqlmodel-alembic/README.md @@ -0,0 +1,62 @@ +# FastAPI + SQLModel + Alembic + +Following the https://testdriven.io/blog/fastapi-sqlmodel/ blog post + +## Docker Containers + +Bring it up +``` +docker-compose up -d --build +``` + +Shut it down +``` +docker-compose down -v +``` +The -v removes names volumes declared in the `volumes` section of the compose +file and anonymous volumes attached to containers + +### Logs + +``` +docker-compose logs web +``` +You'll see what is logged to stdout. Should include your new models on startup. + +or + +``` +docker-compose logs db +``` + +### PSQL for working in your DB +``` +docker-compose exec db psql --username=postgres --dbname=foo +``` + +``` +foo=# \dt + List of relations + Schema | Name | Type | Owner +--------+------+-------+---------- + public | song | table | postgres +``` + +## Interactions with API + +### Add a song + +``` +curl -d '{"name":"Midnight Fit", "artist":"Mogwai"}' \ +-H "Content-Type: application/json" -X POST http://localhost:8004/songs +``` +Response: +``` +{"id":1,"artist":"Mogwai","name":"Midnight Fit"}% +``` + + +## Sources Root in Pycharm + +I marked "project" directory as the "Sources Root". PyCharm uses the source roots as the starting point for +resolving imports. \ No newline at end of file diff --git a/fastapi-sqlmodel-alembic/docker-compose.yml b/fastapi-sqlmodel-alembic/docker-compose.yml new file mode 100644 index 0000000..c83f69f --- /dev/null +++ b/fastapi-sqlmodel-alembic/docker-compose.yml @@ -0,0 +1,24 @@ +version: '3.8' + +services: + + web: + build: ./project + command: uvicorn app.main:app --reload --workers 1 --host 0.0.0.0 --port 8000 + volumes: + - ./project:/usr/src/app + ports: + - 8004:8000 + environment: + - DATABASE_URL=postgresql://postgres:postgres@db:5432/foo + depends_on: + - db + + db: + image: postgres:13.4 + expose: + - 5432 + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - POSTGRES_DB=foo diff --git a/fastapi-sqlmodel-alembic/project/.dockerignore b/fastapi-sqlmodel-alembic/project/.dockerignore new file mode 100644 index 0000000..6e19512 --- /dev/null +++ b/fastapi-sqlmodel-alembic/project/.dockerignore @@ -0,0 +1,2 @@ +.dockerignore +Dockerfile diff --git a/fastapi-sqlmodel-alembic/project/Dockerfile b/fastapi-sqlmodel-alembic/project/Dockerfile new file mode 100644 index 0000000..860b01d --- /dev/null +++ b/fastapi-sqlmodel-alembic/project/Dockerfile @@ -0,0 +1,22 @@ +# pull official base image +FROM python:3.9-slim-buster + +# set working directory +WORKDIR /usr/src/app + +# set environment variables +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +# install system dependencies +RUN apt-get update \ + && apt-get -y install netcat gcc postgresql \ + && apt-get clean + +# install python dependencies +RUN pip install --upgrade pip +COPY ./requirements.txt . +RUN pip install -r requirements.txt + +# add app +COPY . . diff --git a/fastapi-sqlmodel-alembic/project/app/__init__.py b/fastapi-sqlmodel-alembic/project/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fastapi-sqlmodel-alembic/project/app/db.py b/fastapi-sqlmodel-alembic/project/app/db.py new file mode 100644 index 0000000..949e991 --- /dev/null +++ b/fastapi-sqlmodel-alembic/project/app/db.py @@ -0,0 +1,22 @@ +import os + +from sqlmodel import create_engine, SQLModel, Session + + +DATABASE_URL = os.environ.get("DATABASE_URL") + + +# The major differences between SQLModel's create_engine and SQLAlchemy's +# version is that the SQLModel version adds type annotations (for editor support) +# and enables the SQLAlchemy "2.0" style of engines and connections. +engine = create_engine(DATABASE_URL, echo=True) # Will want a "debug mode where it doesn't echo for prod + + +def init_db(): + SQLModel.metadata.create_all(engine) + + +def get_session(): + """A coroutine function for getting a database session.""" + with Session(engine) as session: + yield session diff --git a/fastapi-sqlmodel-alembic/project/app/main.py b/fastapi-sqlmodel-alembic/project/app/main.py new file mode 100644 index 0000000..74fd66c --- /dev/null +++ b/fastapi-sqlmodel-alembic/project/app/main.py @@ -0,0 +1,39 @@ +from fastapi import Depends, FastAPI +from sqlalchemy import select +from sqlmodel import Session + +from app.db import get_session, init_db +from app.models import Song, SongCreate + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + """ + It's worth noting that from app.models import Song is required. + Without it, the song table will not be created. Kind of like + how I have to do my flask sql alchemy models + """ + init_db() + + +@app.get("/ping") +async def pong(): + return {"ping": "pong!"} + + +@app.get("/songs", response_model=list[Song]) +def get_songs(session: Session = Depends(get_session)): + result = session.execute(select(Song)) + songs = result.scalars().all() + return [Song(name=song.name, artist=song.artist, id=song.id) for song in songs] + + +@app.post("/songs") +def add_song(song: SongCreate, session: Session = Depends(get_session)): + song = Song(name=song.name, artist=song.artist) + session.add(song) + session.commit() + session.refresh(song) + return song diff --git a/fastapi-sqlmodel-alembic/project/app/models.py b/fastapi-sqlmodel-alembic/project/app/models.py new file mode 100644 index 0000000..5810faa --- /dev/null +++ b/fastapi-sqlmodel-alembic/project/app/models.py @@ -0,0 +1,28 @@ +from sqlmodel import SQLModel, Field + + +class SongBase(SQLModel): + """ + SongBase is the base model that the others inherit from. + It has two properties, name and artist, both of which are strings. + This is a data-only model since it lacks table=True, which means + that it's only used as a pydantic model + """ + name: str + artist: str + + +class Song(SongBase, table=True): + """ + Song, meanwhile, adds an id property to the base model. It's a table model, + so it's a pydantic and SQLAlchemy model. It represents a database table. + """ + id: int = Field(default=None, primary_key=True) + + +class SongCreate(SongBase): + """ + SongCreate is a data-only, pydantic model + that will be used to create new song instances. + """ + pass diff --git a/fastapi-sqlmodel-alembic/project/requirements.txt b/fastapi-sqlmodel-alembic/project/requirements.txt new file mode 100644 index 0000000..0074634 --- /dev/null +++ b/fastapi-sqlmodel-alembic/project/requirements.txt @@ -0,0 +1,4 @@ +fastapi==0.68.1 +psycopg2-binary==2.9.1 +sqlmodel==0.0.4 +uvicorn==0.15.0