From 3d4894cda972130d002e682935df5a1c9ce7a408 Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Fri, 22 Aug 2025 18:38:51 -0400 Subject: [PATCH] Added in alembic for migrations. Signed-off-by: Cliff Hill --- .flake8 | 1 + backend/Dockerfile | 11 +- backend/alembic.ini | 147 ++++++++++++++++++ backend/migrations/README | 1 + backend/migrations/env.py | 82 ++++++++++ backend/migrations/script.py.mako | 28 ++++ .../8e7c02c0ca4c_initial_migration.py | 34 ++++ backend/poetry.lock | 47 +++++- backend/pyproject.toml | 1 + backend/src/backend/main.py | 9 +- backend/src/backend/models.py | 11 +- backend/src/backend/sql.py | 85 +++++++--- backend/tests/test_models.py | 18 --- compose.dev.yml | 3 + 14 files changed, 431 insertions(+), 47 deletions(-) create mode 100644 backend/alembic.ini create mode 100644 backend/migrations/README create mode 100644 backend/migrations/env.py create mode 100644 backend/migrations/script.py.mako create mode 100644 backend/migrations/versions/8e7c02c0ca4c_initial_migration.py delete mode 100644 backend/tests/test_models.py diff --git a/.flake8 b/.flake8 index 17d09db1..1d069559 100644 --- a/.flake8 +++ b/.flake8 @@ -7,3 +7,4 @@ docstring-convention = google per-file-ignores = tests/*:S101,backend/tests/*:S101 rst-roles = class,const,func,meth,mod,ref rst-directives = deprecated +exclude = backend/migrations/versions/* diff --git a/backend/Dockerfile b/backend/Dockerfile index 5fb0a7e0..58297bec 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -27,6 +27,9 @@ COPY pyproject.toml poetry.lock ./ # Add the poetry plugin for exporting dependencies RUN poetry self add poetry-plugin-export + +# Force the requirements.txt to be generated without cache +ADD "https://www.random.org/cgi-bin/randbyte?nbytes=10&format=h" skipcache # Export dependencies to requirements.txt # This will create a requirements.txt file with the dependencies listed in pyproject.toml RUN poetry export -f requirements.txt --output requirements.txt --without-hashes @@ -48,7 +51,13 @@ RUN pip install --no-cache-dir -r requirements.txt # Copy the project source code COPY src/ src/ +# Copy the Alembic configuration and migrations +COPY alembic.ini alembic.ini +COPY migrations/ migrations/ + +COPY .env .env + # Start fastapi application using uvicorn EXPOSE 8000 ENV PYTHONPATH=/app/src -CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] \ No newline at end of file +CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] diff --git a/backend/alembic.ini b/backend/alembic.ini new file mode 100644 index 00000000..035f57b3 --- /dev/null +++ b/backend/alembic.ini @@ -0,0 +1,147 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts. +# this is typically a path given in POSIX (e.g. forward slashes) +# format, relative to the token %(here)s which refers to the location of this +# ini file +script_location = %(here)s/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 +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. for multiple paths, the path separator +# is defined by "path_separator" below. +prepend_sys_path = . + + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library. +# Any required deps can installed by adding `alembic[tz]` to the pip requirements +# string value is passed to ZoneInfo() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to /versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "path_separator" +# below. +# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions + +# path_separator; This indicates what character is used to split lists of file +# paths, including version_locations and prepend_sys_path within configparser +# files such as alembic.ini. +# The default rendered in new alembic.ini files is "os", which uses os.pathsep +# to provide os-dependent path splitting. +# +# Note that in order to support legacy alembic.ini files, this default does NOT +# take place if path_separator is not present in alembic.ini. If this +# option is omitted entirely, fallback logic is as follows: +# +# 1. Parsing of the version_locations option falls back to using the legacy +# "version_path_separator" key, which if absent then falls back to the legacy +# behavior of splitting on spaces and/or commas. +# 2. Parsing of the prepend_sys_path option falls back to the legacy +# behavior of splitting on spaces, commas, or colons. +# +# Valid values for path_separator are: +# +# path_separator = : +# path_separator = ; +# path_separator = space +# path_separator = newline +# +# Use os.pathsep. Default configuration used for new projects. +path_separator = os + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +# database URL. This is consumed by the user-maintained env.py script only. +# other means of configuring database URLs may be customized within the env.py +# file. +sqlalchemy.url = driver://user:pass@localhost/dbname + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module +# hooks = ruff +# ruff.type = module +# ruff.module = ruff +# ruff.options = check --fix REVISION_SCRIPT_FILENAME + +# Alternatively, use the exec runner to execute a binary found on your PATH +# hooks = ruff +# ruff.type = exec +# ruff.executable = ruff +# ruff.options = check --fix REVISION_SCRIPT_FILENAME + +# Logging configuration. This is also consumed by the user-maintained +# env.py script only. +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARNING +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARNING +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 diff --git a/backend/migrations/README b/backend/migrations/README new file mode 100644 index 00000000..2500aa1b --- /dev/null +++ b/backend/migrations/README @@ -0,0 +1 @@ +Generic single-database configuration. diff --git a/backend/migrations/env.py b/backend/migrations/env.py new file mode 100644 index 00000000..f2aed8fd --- /dev/null +++ b/backend/migrations/env.py @@ -0,0 +1,82 @@ +"""Alembic configuration for database migrations.""" + +import asyncio +from logging.config import fileConfig +from typing import cast + +from alembic import context +from sqlalchemy import Connection +from sqlalchemy import pool +from sqlalchemy.ext.asyncio import async_engine_from_config + +from backend.db import db_url +from backend.models import Base + + +# 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) # type: ignore + +# add your model's MetaData object here +target_metadata = Base.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") + url = db_url + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def do_run_migrations(connection: Connection) -> None: + """Run migrations with the given connection.""" + context.configure(connection=connection, target_metadata=target_metadata) + + with context.begin_transaction(): + context.run_migrations() + + +async 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. + """ + configuration = cast(dict[str, str], config.get_section(config.config_ini_section)) + configuration["sqlalchemy.url"] = db_url + connectable = async_engine_from_config( + configuration, + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + async with connectable.connect() as connection: + await connection.run_sync(do_run_migrations) + + await connectable.dispose() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + asyncio.run(run_migrations_online()) diff --git a/backend/migrations/script.py.mako b/backend/migrations/script.py.mako new file mode 100644 index 00000000..11016301 --- /dev/null +++ b/backend/migrations/script.py.mako @@ -0,0 +1,28 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + """Upgrade schema.""" + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + """Downgrade schema.""" + ${downgrades if downgrades else "pass"} diff --git a/backend/migrations/versions/8e7c02c0ca4c_initial_migration.py b/backend/migrations/versions/8e7c02c0ca4c_initial_migration.py new file mode 100644 index 00000000..933d6f25 --- /dev/null +++ b/backend/migrations/versions/8e7c02c0ca4c_initial_migration.py @@ -0,0 +1,34 @@ +"""Initial Migration + +Revision ID: 8e7c02c0ca4c +Revises: +Create Date: 2025-08-22 22:08:41.229866 + +""" + +from typing import Sequence +from typing import Union + +import sqlalchemy as sa +from alembic import op + + +# revision identifiers, used by Alembic. +revision: str = "8e7c02c0ca4c" +down_revision: Union[str, Sequence[str], None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/backend/poetry.lock b/backend/poetry.lock index f2679f53..73fe50da 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -45,6 +45,27 @@ files = [ {file = "alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e"}, ] +[[package]] +name = "alembic" +version = "1.16.4" +description = "A database migration tool for SQLAlchemy." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "alembic-1.16.4-py3-none-any.whl", hash = "sha256:b05e51e8e82efc1abd14ba2af6392897e145930c3e0a2faf2b0da2f7f7fd660d"}, + {file = "alembic-1.16.4.tar.gz", hash = "sha256:efab6ada0dd0fae2c92060800e0bf5c1dc26af15a10e02fb4babff164b4725e2"}, +] + +[package.dependencies] +Mako = "*" +SQLAlchemy = ">=1.4.0" +tomli = {version = "*", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.12" + +[package.extras] +tz = ["tzdata"] + [[package]] name = "annotated-types" version = "0.7.0" @@ -1464,6 +1485,26 @@ files = [ {file = "joblib-1.5.1.tar.gz", hash = "sha256:f4f86e351f39fe3d0d32a9f2c3d8af1ee4cec285aafcb27003dda5205576b444"}, ] +[[package]] +name = "mako" +version = "1.3.10" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59"}, + {file = "mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28"}, +] + +[package.dependencies] +MarkupSafe = ">=0.9.2" + +[package.extras] +babel = ["Babel"] +lingua = ["lingua"] +testing = ["pytest"] + [[package]] name = "markdown-it-py" version = "3.0.0" @@ -3289,8 +3330,7 @@ version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" -groups = ["dev"] -markers = "python_full_version <= \"3.11.0a6\"" +groups = ["main", "dev"] files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, @@ -3325,6 +3365,7 @@ files = [ {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] +markers = {main = "python_version < \"3.11\"", dev = "python_full_version <= \"3.11.0a6\""} [[package]] name = "tomlkit" @@ -3780,4 +3821,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.9,<4.0" -content-hash = "5c5e256eb66c6d7520e1fd90000fa7cf5cac1845afe19b381c3cd3f28b699fad" +content-hash = "75ec5ce48a9d43e45fe8abeaa6bed56830c44237fda6eb063bc52255ef4e9c6b" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 57ce75cc..33feb566 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -21,6 +21,7 @@ click = ">=8.0.1" fastapi = {extras = ["standard"], version = "^0.116.1"} asyncpg = "^0.30.0" sqlalchemy = "^2.0.43" +alembic = "^1.16.4" [tool.poetry.group.dev.dependencies] pyyaml = ">=6.0.1" diff --git a/backend/src/backend/main.py b/backend/src/backend/main.py index 282fe9d0..daf56f2f 100644 --- a/backend/src/backend/main.py +++ b/backend/src/backend/main.py @@ -16,7 +16,14 @@ app_configs: dict[str, Any] = {"title": "Numinar Coding Project"} if ENVIRONMENT not in SHOW_DOCS_ENVIRONMENTS: app_configs["openapi_url"] = None # Disable OpenAPI schema -create_db_and_tables() + +async def lifespan(app: FastAPI): + """Lifespan event to create database tables on startup.""" + await create_db_and_tables() + yield # This is where the lifespan ends + + +app_configs["lifespan"] = lifespan app = FastAPI(**app_configs) diff --git a/backend/src/backend/models.py b/backend/src/backend/models.py index c67aa3c4..e83fe4ad 100644 --- a/backend/src/backend/models.py +++ b/backend/src/backend/models.py @@ -59,7 +59,7 @@ class Booking(Base): __tablename__ = "bookings" id: Mapped[int] = mapped_column(primary_key=True, init=False, autoincrement=True) - room_id: Mapped[int] = mapped_column(ForeignKey("room.id")) + room_id: Mapped[int] = mapped_column(ForeignKey("rooms.id")) start_time: Mapped[datetime] end_time: Mapped[datetime] @@ -75,13 +75,14 @@ class Invitee(Base): __tablename__ = "invitees" id: Mapped[int] = mapped_column(primary_key=True, init=False, autoincrement=True) - booking_id: Mapped[int] = mapped_column(ForeignKey("booking.id")) - user_id: Mapped[int] = mapped_column(ForeignKey("user.id")) + booking_id: Mapped[int] = mapped_column(ForeignKey("bookings.id")) + user_id: Mapped[int] = mapped_column(ForeignKey("users.id")) booking: Mapped["Booking"] = relationship(back_populates="invitees", init=False) user: Mapped["User"] = relationship(back_populates="invitees", init=False) -def create_db_and_tables() -> None: +async def create_db_and_tables() -> None: # pragma: no cover """Create the database and tables.""" - Base.metadata.create_all(engine.sync_engine) + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) diff --git a/backend/src/backend/sql.py b/backend/src/backend/sql.py index 498068a3..f55be421 100644 --- a/backend/src/backend/sql.py +++ b/backend/src/backend/sql.py @@ -1,4 +1,10 @@ -"""SQL functions for the Numinar coding project backend.""" +"""SQL functions for the Numinar coding project backend. + +Note: + The session parameter is automatically injected by the `sessionize` decorator. + The type hint for session is ignored to avoid needless complications with + getting the type checkers to accept it. +""" from datetime import datetime from typing import TypedDict @@ -17,35 +23,48 @@ from backend.models import User @sessionize -async def get_users(session: AsyncSession): +async def get_users( + session: AsyncSession = None, # type: ignore +): """Retrieve all users from the database.""" result = await session.execute(select(User)) return result.scalars().all() @sessionize -async def get_user_by_email(session: AsyncSession, email: str): +async def get_user_by_email( + email: str, + session: AsyncSession = None, # type: ignore +): """Retrieve a user by their email address.""" result = await session.execute(select(User).where(User.email == email)) return result.scalar_one_or_none() @sessionize -async def get_rooms(session: AsyncSession): +async def get_rooms( + session: AsyncSession = None, # type: ignore +): """Retrieve all rooms from the database.""" result = await session.execute(select(Room)) return result.scalars().all() @sessionize -async def get_room(session: AsyncSession, room_id: int): +async def get_room( + room_id: int, + session: AsyncSession = None, # type: ignore +): """Retrieve a room by its ID.""" result = await session.execute(select(Room).where(Room.id == room_id)) return result.scalar_one_or_none() @sessionize -async def new_room(session: AsyncSession, room: Room): +async def new_room( + room: Room, + session: AsyncSession = None, # type: ignore +): """Create a new room in the database.""" session.add(room) await session.commit() @@ -56,14 +75,16 @@ class RoomParams(TypedDict): """Parameters for updating a room.""" name: str - location: str | None - equipment: str | None - capacity: int | None + location: str + equipment: str + capacity: int @sessionize async def update_room( - session: AsyncSession, room_id: int, **kwargs: Unpack[RoomParams] + room_id: int, + session: AsyncSession = None, # type: ignore + **kwargs: Unpack[RoomParams], ): """Update an existing room.""" stmt = update(Room).where(Room.id == room_id).values(**kwargs) @@ -73,7 +94,10 @@ async def update_room( @sessionize -async def delete_room(session: AsyncSession, room_id: int): +async def delete_room( + room_id: int, + session: AsyncSession = None, # type: ignore +): """Delete a room from the database.""" stmt = delete(Room).where(Room.id == room_id) await session.execute(stmt) @@ -81,21 +105,30 @@ async def delete_room(session: AsyncSession, room_id: int): @sessionize -async def get_bookings_for_room(session: AsyncSession, room_id: int): +async def get_bookings_for_room( + room_id: int, + session: AsyncSession = None, # type: ignore +): """Retrieve all bookings for a specific room.""" result = await session.execute(select(Booking).where(Booking.room_id == room_id)) return result.scalars().all() @sessionize -async def get_booking(session: AsyncSession, booking_id: int): +async def get_booking( + booking_id: int, + session: AsyncSession = None, # type: ignore +): """Retrieve a booking by its ID.""" result = await session.execute(select(Booking).where(Booking.id == booking_id)) return result.scalar_one_or_none() @sessionize -async def new_booking(session: AsyncSession, booking: Booking): +async def new_booking( + booking: Booking, + session: AsyncSession = None, # type: ignore +): """Create a new booking in the database.""" session.add(booking) await session.commit() @@ -112,7 +145,9 @@ class BookingParams(TypedDict): @sessionize async def update_booking( - session: AsyncSession, booking_id: int, **kwargs: Unpack[BookingParams] + booking_id: int, + session: AsyncSession = None, # type: ignore + **kwargs: Unpack[BookingParams], ): """Update an existing booking.""" stmt = update(Booking).where(Booking.id == booking_id).values(**kwargs) @@ -122,7 +157,10 @@ async def update_booking( @sessionize -async def delete_booking(session: AsyncSession, booking_id: int): +async def delete_booking( + booking_id: int, + session: AsyncSession = None, # type: ignore +): """Delete a booking from the database.""" stmt = delete(Booking).where(Booking.id == booking_id) await session.execute(stmt) @@ -130,7 +168,10 @@ async def delete_booking(session: AsyncSession, booking_id: int): @sessionize -async def get_invitees_for_booking(session: AsyncSession, booking_id: int): +async def get_invitees_for_booking( + booking_id: int, + session: AsyncSession = None, # type: ignore +): """Retrieve all invitees for a specific booking.""" result = await session.execute( select(Invitee.user).where(Invitee.booking_id == booking_id) @@ -139,7 +180,11 @@ async def get_invitees_for_booking(session: AsyncSession, booking_id: int): @sessionize -async def add_invitee_to_booking(session: AsyncSession, booking_id: int, email: str): +async def add_invitee_to_booking( + booking_id: int, + email: str, + session: AsyncSession = None, # type: ignore +): """Add an invitee to a booking.""" user = await get_user_by_email(session, email) if not user: @@ -153,7 +198,9 @@ async def add_invitee_to_booking(session: AsyncSession, booking_id: int, email: @sessionize async def remove_invitee_from_booking( - session: AsyncSession, booking_id: int, email: str + booking_id: int, + email: str, + session: AsyncSession = None, # type: ignore ): """Remove an invitee from a booking.""" user = await get_user_by_email(session, email) diff --git a/backend/tests/test_models.py b/backend/tests/test_models.py deleted file mode 100644 index ee466e91..00000000 --- a/backend/tests/test_models.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Test for the create_db_and_tables function in backend.models.""" - -from unittest.mock import patch - -from backend.models import Base -from backend.models import create_db_and_tables -from backend.models import engine - - -def test_create_db_and_tables() -> None: - """Test the create_db_and_tables function. - - Verifies that the function correctly calls SQLModel.metadata.create_all with - the synchronous engine. - """ - with patch.object(Base.metadata, "create_all") as mock_create_all: - create_db_and_tables() - mock_create_all.assert_called_once_with(engine.sync_engine) diff --git a/compose.dev.yml b/compose.dev.yml index 78cf670f..0e5c484d 100644 --- a/compose.dev.yml +++ b/compose.dev.yml @@ -9,6 +9,9 @@ services: - ./backend/src:/app/src - ./backend/poetry.lock:/app/poetry.lock - ./backend/pyproject.toml:/app/pyproject.toml + - ./backend/.env:/app/.env + - ./backend/alembic.ini:/app/alembic.ini + - ./backend/migrations:/app/migrations environment: ENVIRONMENT: development ports: