diff --git a/backend/.env b/backend/.env new file mode 100644 index 0000000..2033f79 --- /dev/null +++ b/backend/.env @@ -0,0 +1 @@ +DATABASE_URL=postgresql://user:password@localhost:5432/mydatabase diff --git a/backend/Makefile b/backend/Makefile new file mode 100644 index 0000000..e69de29 diff --git a/backend/alembic.ini b/backend/alembic.ini new file mode 100644 index 0000000..692d73a --- /dev/null +++ b/backend/alembic.ini @@ -0,0 +1,141 @@ +# 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/alembic + +# 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 = postgresql://user:password@localhost:5432/mydatabase + + +[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 exec runner, execute a binary +# hooks = ruff +# ruff.type = exec +# ruff.executable = %(here)s/.venv/bin/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/alembic/README b/backend/alembic/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/backend/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/backend/alembic/__pycache__/env.cpython-313.pyc b/backend/alembic/__pycache__/env.cpython-313.pyc new file mode 100644 index 0000000..d9d3a30 Binary files /dev/null and b/backend/alembic/__pycache__/env.cpython-313.pyc differ diff --git a/backend/alembic/env.py b/backend/alembic/env.py new file mode 100644 index 0000000..2a48bbb --- /dev/null +++ b/backend/alembic/env.py @@ -0,0 +1,74 @@ +from logging.config import fileConfig +from app.database import Base +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +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. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +target_metadata = Base.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure(connection=connection, target_metadata=target_metadata) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/backend/alembic/script.py.mako b/backend/alembic/script.py.mako new file mode 100644 index 0000000..480b130 --- /dev/null +++ b/backend/alembic/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, 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/alembic/versions/__pycache__/a3ac646e53a8_init.cpython-313.pyc b/backend/alembic/versions/__pycache__/a3ac646e53a8_init.cpython-313.pyc new file mode 100644 index 0000000..cebecc6 Binary files /dev/null and b/backend/alembic/versions/__pycache__/a3ac646e53a8_init.cpython-313.pyc differ diff --git a/backend/alembic/versions/a3ac646e53a8_init.py b/backend/alembic/versions/a3ac646e53a8_init.py new file mode 100644 index 0000000..10a722d --- /dev/null +++ b/backend/alembic/versions/a3ac646e53a8_init.py @@ -0,0 +1,41 @@ +"""init + +Revision ID: a3ac646e53a8 +Revises: +Create Date: 2025-06-04 21:36:22.283823 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'a3ac646e53a8' +down_revision: Union[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! ### + op.drop_index(op.f('ix_items_id'), table_name='items') + op.drop_index(op.f('ix_items_name'), table_name='items') + op.drop_table('items') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('items', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('description', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('items_pkey')) + ) + op.create_index(op.f('ix_items_name'), 'items', ['name'], unique=False) + op.create_index(op.f('ix_items_id'), 'items', ['id'], unique=False) + # ### end Alembic commands ### diff --git a/backend/app/__pycache__/crud.cpython-313.pyc b/backend/app/__pycache__/crud.cpython-313.pyc index ab6117a..79c89c3 100644 Binary files a/backend/app/__pycache__/crud.cpython-313.pyc and b/backend/app/__pycache__/crud.cpython-313.pyc differ diff --git a/backend/app/__pycache__/database.cpython-313.pyc b/backend/app/__pycache__/database.cpython-313.pyc index 6f29c09..58007e1 100644 Binary files a/backend/app/__pycache__/database.cpython-313.pyc and b/backend/app/__pycache__/database.cpython-313.pyc differ diff --git a/backend/app/__pycache__/main.cpython-313.pyc b/backend/app/__pycache__/main.cpython-313.pyc index 05b2ec2..50919a1 100644 Binary files a/backend/app/__pycache__/main.cpython-313.pyc and b/backend/app/__pycache__/main.cpython-313.pyc differ diff --git a/backend/app/__pycache__/models.cpython-313.pyc b/backend/app/__pycache__/models.cpython-313.pyc index bb40868..41f01e8 100644 Binary files a/backend/app/__pycache__/models.cpython-313.pyc and b/backend/app/__pycache__/models.cpython-313.pyc differ diff --git a/backend/app/__pycache__/schemas.cpython-313.pyc b/backend/app/__pycache__/schemas.cpython-313.pyc index e0ca0df..6127c00 100644 Binary files a/backend/app/__pycache__/schemas.cpython-313.pyc and b/backend/app/__pycache__/schemas.cpython-313.pyc differ diff --git a/backend/app/crud.py b/backend/app/crud.py index c2fb5dc..deeb3e0 100644 --- a/backend/app/crud.py +++ b/backend/app/crud.py @@ -10,7 +10,7 @@ def get_items(db: Session, skip: int = 0, limit: int = 10): return db.query(models.Item).offset(skip).limit(limit).all() -def create_item(db: Session, item: schemas.ItemCreate): +def create_item(db: Session, item: schemas.Item): db_item = models.Item(**item.model_dump()) db.add(db_item) db.commit() diff --git a/backend/app/database.py b/backend/app/database.py index 22c96b1..4b658b2 100644 --- a/backend/app/database.py +++ b/backend/app/database.py @@ -1,10 +1,10 @@ from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker, declarative_base import os +from dotenv import load_dotenv -DATABASE_URL = os.getenv( - "DATABASE_URL", "postgresql://user:password@db:5432/mydatabase" -) +env = load_dotenv() +DATABASE_URL = os.getenv("DATABASE_URL", "") engine = create_engine(DATABASE_URL) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) diff --git a/backend/app/main.py b/backend/app/main.py index cbdb685..0937884 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -23,7 +23,7 @@ def health_check(): @app.post("/items/", response_model=schemas.Item) -def create_item(item: schemas.ItemCreate, db: Session = Depends(get_db)): +def create_item(item: schemas.Item, db: Session = Depends(get_db)): return crud.create_item(db, item) diff --git a/backend/app/schemas.py b/backend/app/schemas.py index c900f57..161b8f4 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -4,10 +4,7 @@ from pydantic import BaseModel class ItemBase(BaseModel): name: str description: str | None = None - - -class ItemCreate(ItemBase): - pass + body: str class Item(ItemBase): diff --git a/backend/docs/migration.md b/backend/docs/migration.md new file mode 100644 index 0000000..66e907d --- /dev/null +++ b/backend/docs/migration.md @@ -0,0 +1,13 @@ +# Database Migration with Alembic + +## Install Alembic + +## Init Alembic + +## Migration + +```bash + +alembic revision --autogenerate -m "add items table" +alembic upgrade head +``` diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 8ff85c1..83b6e76 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -5,6 +5,8 @@ description = "Add your description here" readme = "README.md" requires-python = ">=3.11" dependencies = [ + "alembic>=1.16.1", + "dotenv>=0.9.9", "fastapi[standard]>=0.115.12", "psycopg2-binary>=2.9.10", "sqlalchemy>=2.0.41", diff --git a/backend/uv.lock b/backend/uv.lock index f8fcb49..7c09e9c 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -2,6 +2,20 @@ version = 1 revision = 2 requires-python = ">=3.11" +[[package]] +name = "alembic" +version = "1.16.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/89/bfb4fe86e3fc3972d35431af7bedbc60fa606e8b17196704a1747f7aa4c3/alembic-1.16.1.tar.gz", hash = "sha256:43d37ba24b3d17bc1eb1024fe0f51cd1dc95aeb5464594a02c6bb9ca9864bfa4", size = 1955006, upload-time = "2025-05-21T23:11:05.991Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/59/565286efff3692c5716c212202af61466480f6357c4ae3089d4453bff1f3/alembic-1.16.1-py3-none-any.whl", hash = "sha256:0cdd48acada30d93aa1035767d67dff25702f8de74d7c3919f2e8492c8db2e67", size = 242488, upload-time = "2025-05-21T23:11:07.783Z" }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -64,6 +78,17 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632, upload-time = "2024-10-05T20:14:57.687Z" }, ] +[[package]] +name = "dotenv" +version = "0.9.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dotenv" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892, upload-time = "2025-02-19T22:15:01.647Z" }, +] + [[package]] name = "email-validator" version = "2.2.0" @@ -250,6 +275,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "mako" +version = "1.3.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, +] + [[package]] name = "markdown-it-py" version = "3.0.0" @@ -536,6 +573,8 @@ name = "server" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "alembic" }, + { name = "dotenv" }, { name = "fastapi", extra = ["standard"] }, { name = "psycopg2-binary" }, { name = "sqlalchemy" }, @@ -544,6 +583,8 @@ dependencies = [ [package.metadata] requires-dist = [ + { name = "alembic", specifier = ">=1.16.1" }, + { name = "dotenv", specifier = ">=0.9.9" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.115.12" }, { name = "psycopg2-binary", specifier = ">=2.9.10" }, { name = "sqlalchemy", specifier = ">=2.0.41" },