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

Change how we run migrations or initialize the database on a fresh installs #132

Merged
merged 1 commit into from
Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ dist
*.pickle
.env
venv
.idea
14 changes: 11 additions & 3 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,18 @@ WORKDIR /app
ENV PATH="${PATH}:/root/.local/bin"
ENV PYTHONPATH=.

RUN mkdir scripts

COPY requirements.txt .
RUN pip install --upgrade pip
RUN pip install -r requirements.txt
COPY pyproject.toml .
COPY alembic.ini.example alembic.ini
COPY scripts/dev-entry.sh scripts/dev-entry.sh

COPY src/ .
# Dev only
COPY .env .

RUN pip install --upgrade pip
RUN pip install .

EXPOSE 5000
CMD ["/bin/sh", "./scripts/dev-entry.sh"]
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[alembic]
# path to migration scripts
script_location = migrations
script_location = src/appointment/migrations

# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
Expand All @@ -12,7 +12,7 @@ file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(re

# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .
prepend_sys_path = src/appointment

# timezone to use when rendering the date within the migration file
# as well as the filename.
Expand Down
20 changes: 15 additions & 5 deletions backend/deploy.dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,24 @@ WORKDIR /app
ENV PATH="${PATH}:/root/.local/bin"
ENV PYTHONPATH=.

RUN mkdir scripts

COPY requirements.txt .
RUN pip install --upgrade pip
RUN pip install -r requirements.txt
COPY pyproject.toml .
COPY alembic.ini.example alembic.ini
COPY scripts/entry.sh scripts/entry.sh

COPY src/ ./src
COPY scripts/ ./scripts
# Needed for deploy, we don't have a volume attached
COPY src .

RUN pip install --upgrade pip
RUN pip install .

RUN cp ./src/alembic.ini.example ./src/alembic.ini
# install removes the src file and installs the application as /app/appointment
# that's fine, but uhh let's add this hack to line it up with our dev environment.
# I'll buy whoever fixes this a coffee.
RUN mkdir src
RUN ln -s /app/appointment src/appointment

EXPOSE 5000
CMD ["/bin/sh", "./scripts/entry.sh"]
3 changes: 3 additions & 0 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ description = "Backend component to Thunderbird Appointment"
requires-python = ">3.10"
dynamic = ["dependencies"]

[project.scripts]
run-command = "src.appointment.main:cli"

[project.urls]
homepage = "https://appointment.day"
repository = "https://github.com/thundernest/appointment.git"
Expand Down
9 changes: 2 additions & 7 deletions backend/scripts/dev-entry.sh
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
#!/bin/sh

cd src
echo 'Starting migrations...'
alembic current
alembic upgrade head
echo 'Finished migrations!'
cd ../
run-command update-db

uvicorn src.main:app --reload --host 0.0.0.0 --port 8090
uvicorn --factory src.appointment.main:server --reload --host 0.0.0.0 --port 8090
9 changes: 2 additions & 7 deletions backend/scripts/entry.sh
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
#!/bin/sh

cd src
echo 'Starting migrations...'
alembic current
alembic upgrade head
echo 'Finished migrations!'
cd ../
run-command update-db

uvicorn src.main:app --host 0.0.0.0 --port 5000
uvicorn --factory src.appointment.main:server --host 0.0.0.0 --port 5000
37 changes: 37 additions & 0 deletions backend/src/appointment/commands/update_db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import os

from ..database import models
from ..database.database import engine
from alembic.runtime import migration


def run():
print("Checking if we have a fresh database...")

# then, load the Alembic configuration and generate the
# version table, "stamping" it with the most recent rev:
from alembic import command
from alembic.config import Config

# TODO: Does this work on stage?
alembic_cfg = Config("./alembic.ini")

# If we have our database url env variable set, use that instead!
if os.getenv("DATABASE_URL"):
alembic_cfg.set_main_option("sqlalchemy.url", os.getenv("DATABASE_URL"))

with engine.begin() as connection:
context = migration.MigrationContext.configure(connection)
# Returns a tuple, empty if there's no revisions saved
revisions = context.get_current_heads()

# If we have no revisions, then fully create the database from the model metadata,
# and set our revision number to the latest revision. Otherwise run any new migrations
if len(revisions) == 0:
print("Initializing database, and setting it to the latest revision")
models.Base.metadata.create_all(bind=engine)
command.stamp(alembic_cfg, "head")
else:
print("Database already initialized, running migrations")
command.upgrade(alembic_cfg, 'head')

Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from email.mime.text import MIMEText
from fastapi.templating import Jinja2Templates

templates = Jinja2Templates("src/templates/email")
templates = Jinja2Templates("src/appointment/templates/email")


def get_template(template_name) -> "jinja2.Template":
Expand Down
File renamed without changes.
88 changes: 51 additions & 37 deletions backend/src/main.py → backend/src/appointment/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,21 +48,6 @@

logging.debug("Logger started!")

# database
from .database import models
from .database.database import engine

models.Base.metadata.create_all(bind=engine)

# extra routes
from .routes import api
from .routes import account
from .routes import google
from .routes import schedule

# init app
app = FastAPI()

if os.getenv("SENTRY_DSN") != "" or os.getenv("SENTRY_DSN") is not None:
sentry_sdk.init(
dsn=os.getenv("SENTRY_DSN"),
Expand All @@ -73,29 +58,58 @@
environment=os.getenv("APP_ENV", "dev"),
)

# allow requests from own frontend running on a different port
app.add_middleware(
CORSMiddleware,
# Work around for now :)
allow_origins=[
os.getenv("FRONTEND_URL", "http://localhost:8080"),
"https://accounts.google.com",
"https://www.googleapis.com/auth/calendar",
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

def server():
"""
Main function for the fast api server
"""
# extra routes
from .routes import api
from .routes import account
from .routes import google
from .routes import schedule

# init app
app = FastAPI()

# allow requests from own frontend running on a different port
app.add_middleware(
CORSMiddleware,
# Work around for now :)
allow_origins=[
os.getenv("FRONTEND_URL", "http://localhost:8080"),
"https://accounts.google.com",
"https://www.googleapis.com/auth/calendar",
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

@app.exception_handler(RefreshError)
async def catch_google_refresh_errors(request, exc):
"""Catch google refresh errors, and use our error instead."""
return await http_exception_handler(request, APIGoogleRefreshError())

# Mix in our extra routes
app.include_router(api.router)
app.include_router(account.router, prefix="/account")
app.include_router(google.router, prefix="/google")
app.include_router(schedule.router, prefix="/schedule")

return app


@app.exception_handler(RefreshError)
async def catch_google_refresh_errors(request, exc):
"""Catch google refresh errors, and use our error instead."""
return await http_exception_handler(request, APIGoogleRefreshError())
def cli():
"""
A very simple cli handler
"""
if len(sys.argv) < 2:
print("No command specified")
return

command = sys.argv[1:]

# Mix in our extra routes
app.include_router(api.router)
app.include_router(account.router, prefix="/account")
app.include_router(google.router, prefix="/google")
app.include_router(schedule.router, prefix="/schedule")
if command[0] == 'update-db':
from .commands import update_db
update_db.run()
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
# This is ran from src/ so ignore the errors
from secrets import normalize_secrets

import sentry_sdk

# Normalize any AWS secrets
normalize_secrets()

Expand All @@ -32,6 +34,17 @@
# my_important_option = config.get_main_option("my_important_option")
# ... etc.

# Catch any errors that may run during migrations
if os.getenv("SENTRY_DSN") != "" or os.getenv("SENTRY_DSN") is not None:
sentry_sdk.init(
dsn=os.getenv("SENTRY_DSN"),
# Set traces_sample_rate to 1.0 to capture 100%
# of transactions for performance monitoring.
# We recommend adjusting this value in production,
traces_sample_rate=1.0,
environment=os.getenv("APP_ENV", "dev"),
)


def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ def secret():


def upgrade() -> None:
op.drop_column("appointments", "appointment_type")
op.drop_constraint("schedules_ibfk_1", "schedules", type_="foreignkey")
# op.drop_column("appointments", "appointment_type")
#op.drop_constraint("schedules_ibfk_1", "schedules", type_="foreignkey")
op.drop_column("schedules", "appointment_id")
op.add_column("schedules", sa.Column("calendar_id", sa.Integer, sa.ForeignKey("calendars.id")))
op.add_column("schedules", sa.Column("location_type", sa.Enum(LocationType), default=LocationType.online))
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
6 changes: 4 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
services:

backend:
build: ./backend
build:
context: ./backend
dockerfile: ./Dockerfile
ports:
- 8090:8090
volumes:
- ./backend:/app
- ./backend/src:/app/src
environment:
- DATABASE_URL=mysql+mysqldb://tba:tba@mysql:3306/appointment
depends_on:
Expand Down
2 changes: 1 addition & 1 deletion frontend/docker/etc/nginx/conf.d/appointments.conf
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ server {
#access_log /var/log/nginx/host.access.log main;

# Backend API proxy
location /api/v1 {
location ^~ /api/v1/ {
# Remove our fake /api/v1/ prefix for FastAPI
rewrite ^/api/v1/(.*)$ /$1 break;
proxy_pass http://127.0.0.1:5000;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/ScheduleCreation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,7 @@ const saveSchedule = async (withConfirmation = true) => {
// save schedule data
const { data, error } = props.schedule
? await call(`schedule/${props.schedule.id}`).put(obj).json()
: await call("schedule").post(obj).json();
: await call("schedule/").post(obj).json();

if (error.value) {
// error message is in data
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/views/ScheduleView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@ const firstSchedule = computed(() => schedules.value.length > 0
const schedulesReady = ref(false);
const getFirstSchedule = async () => {
calendarEvents.value = [];
const { data } = await call('schedule').get().json();
// trailing slash to prevent fast api redirect which doesn't work great on our container setup
const { data } = await call('schedule/').get().json();
schedules.value = data.value;
};

Expand Down