diff --git a/LICENSE b/LICENSE index 5352154..29e858b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 Michael Herman +Copyright (c) 2023 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 diff --git a/README.md b/README.md index e5f3bfa..fc6c57b 100644 --- a/README.md +++ b/README.md @@ -21,4 +21,4 @@ Add a song: $ curl -d '{"name":"Midnight Fit", "artist":"Mogwai", "year":"2021"}' -H "Content-Type: application/json" -X POST http://localhost:8004/songs ``` -Get all songs: [http://localhost:8004/songs](http://localhost:8004/songs) +Get all songs: [http://localhost:8004/songs](http://localhost:8004/songs) \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 8be7def..ed0e352 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,7 @@ services: - db db: - image: postgres:13.4 + image: postgres:15.3 expose: - 5432 environment: diff --git a/project/Dockerfile b/project/Dockerfile index 860b01d..f7e7693 100644 --- a/project/Dockerfile +++ b/project/Dockerfile @@ -1,5 +1,5 @@ # pull official base image -FROM python:3.9-slim-buster +FROM python:3.11-slim-buster # set working directory WORKDIR /usr/src/app diff --git a/project/alembic.ini b/project/alembic.ini index 7bbd09d..782a236 100644 --- a/project/alembic.ini +++ b/project/alembic.ini @@ -4,8 +4,9 @@ # path to migration scripts script_location = migrations -# template used to generate migration files -# file_template = %%(rev)s_%%(slug)s +# 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 +# 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. @@ -35,16 +36,23 @@ prepend_sys_path = . # version location specification; This defaults # to migrations/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 "version_path_separator" +# The path separator used here should be the separator specified by "version_path_separator" below. # version_locations = %(here)s/bar:%(here)s/bat:migrations/versions # version path separator; As mentioned above, this is the character used to split -# version_locations. Valid values are: +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: # # version_path_separator = : # version_path_separator = ; # version_path_separator = space -version_path_separator = os # default: use os.pathsep +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +# 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 diff --git a/project/app/db.py b/project/app/db.py index 48f9665..95b7692 100644 --- a/project/app/db.py +++ b/project/app/db.py @@ -1,15 +1,14 @@ import os -from sqlmodel import SQLModel +from sqlmodel import SQLModel, create_engine +from sqlmodel.ext.asyncio.session import AsyncSession, AsyncEngine -from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine from sqlalchemy.orm import sessionmaker DATABASE_URL = os.environ.get("DATABASE_URL") -engine = create_async_engine(DATABASE_URL, echo=True, future=True) - +engine = AsyncEngine(create_engine(DATABASE_URL, echo=True, future=True)) async def init_db(): async with engine.begin() as conn: @@ -22,4 +21,4 @@ async def get_session() -> AsyncSession: engine, class_=AsyncSession, expire_on_commit=False ) async with async_session() as session: - yield session + yield session \ No newline at end of file diff --git a/project/app/main.py b/project/app/main.py index 2b24454..b4d68ae 100644 --- a/project/app/main.py +++ b/project/app/main.py @@ -1,8 +1,8 @@ from fastapi import Depends, FastAPI -from sqlalchemy.future import select -from sqlalchemy.ext.asyncio import AsyncSession +from sqlmodel import select +from sqlmodel.ext.asyncio.session import AsyncSession -from app.db import get_session +from app.db import get_session, init_db from app.models import Song, SongCreate app = FastAPI() @@ -26,4 +26,4 @@ async def add_song(song: SongCreate, session: AsyncSession = Depends(get_session session.add(song) await session.commit() await session.refresh(song) - return song + return song \ No newline at end of file diff --git a/project/app/models.py b/project/app/models.py index 0f2d7cd..e0953c4 100644 --- a/project/app/models.py +++ b/project/app/models.py @@ -1,6 +1,5 @@ -from typing import Optional - from sqlmodel import SQLModel, Field +from typing import Optional class SongBase(SQLModel): @@ -10,8 +9,8 @@ class SongBase(SQLModel): class Song(SongBase, table=True): - id: int = Field(default=None, primary_key=True) + id: int = Field(default=None, nullable=False, primary_key=True) class SongCreate(SongBase): - pass + pass \ No newline at end of file diff --git a/project/migrations/env.py b/project/migrations/env.py index 3180608..f17f0a5 100644 --- a/project/migrations/env.py +++ b/project/migrations/env.py @@ -1,14 +1,15 @@ import asyncio from logging.config import fileConfig -from sqlalchemy import engine_from_config from sqlalchemy import pool -from sqlalchemy.ext.asyncio import AsyncEngine -from sqlmodel import SQLModel +from sqlalchemy.engine import Connection +from sqlalchemy.ext.asyncio import async_engine_from_config +from sqlmodel import SQLModel # NEW + from alembic import context -from app.models import Song +from app.models import Song # NEW # this is the Alembic Config object, which provides # access to the values within the .ini file in use. @@ -16,13 +17,14 @@ # Interpret the config file for Python logging. # This line sets up loggers basically. -fileConfig(config.config_file_name) +if config.config_file_name is not None: + fileConfig(config.config_file_name) # add your model's MetaData object here # for 'autogenerate' support # from myapp import mymodel # target_metadata = mymodel.Base.metadata -target_metadata = SQLModel.metadata +target_metadata = SQLModel.metadata # UPDATED # other values from the config, defined by the needs of env.py, # can be acquired: @@ -30,7 +32,7 @@ # ... etc. -def run_migrations_offline(): +def run_migrations_offline() -> None: """Run migrations in 'offline' mode. This configures the context with just a URL @@ -54,34 +56,38 @@ def run_migrations_offline(): context.run_migrations() -def do_run_migrations(connection): +def do_run_migrations(connection: Connection) -> None: 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 +async def run_async_migrations() -> None: + """In this scenario we need to create an Engine and associate a connection with the context. """ - connectable = AsyncEngine( - engine_from_config( - config.get_section(config.config_ini_section), - prefix="sqlalchemy.", - poolclass=pool.NullPool, - future=True, - ) + + connectable = async_engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, ) async with connectable.connect() as connection: await connection.run_sync(do_run_migrations) + await connectable.dispose() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode.""" + + asyncio.run(run_async_migrations()) + if context.is_offline_mode(): run_migrations_offline() else: - asyncio.run(run_migrations_online()) + run_migrations_online() diff --git a/project/migrations/script.py.mako b/project/migrations/script.py.mako index e45068f..09e55d3 100644 --- a/project/migrations/script.py.mako +++ b/project/migrations/script.py.mako @@ -7,7 +7,7 @@ Create Date: ${create_date} """ from alembic import op import sqlalchemy as sa -import sqlmodel +import sqlmodel # NEW ${imports if imports else ""} # revision identifiers, used by Alembic. @@ -17,9 +17,9 @@ branch_labels = ${repr(branch_labels)} depends_on = ${repr(depends_on)} -def upgrade(): +def upgrade() -> None: ${upgrades if upgrades else "pass"} -def downgrade(): +def downgrade() -> None: ${downgrades if downgrades else "pass"} diff --git a/project/migrations/versions/842abcd80d3e_init.py b/project/migrations/versions/842abcd80d3e_init.py new file mode 100644 index 0000000..4522cde --- /dev/null +++ b/project/migrations/versions/842abcd80d3e_init.py @@ -0,0 +1,34 @@ +"""init + +Revision ID: 842abcd80d3e +Revises: +Create Date: 2023-07-10 17:10:45.380832 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel # NEW + + +# revision identifiers, used by Alembic. +revision = '842abcd80d3e' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('song', + sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('artist', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('id', sa.Integer(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('song') + # ### end Alembic commands ### diff --git a/project/migrations/versions/f68b489cdeb0_add_year.py b/project/migrations/versions/f68b489cdeb0_add_year.py new file mode 100644 index 0000000..2badcc1 --- /dev/null +++ b/project/migrations/versions/f68b489cdeb0_add_year.py @@ -0,0 +1,31 @@ +"""add year + +Revision ID: f68b489cdeb0 +Revises: 842abcd80d3e +Create Date: 2023-07-10 17:12:35.936572 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel # NEW + + +# revision identifiers, used by Alembic. +revision = 'f68b489cdeb0' +down_revision = '842abcd80d3e' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('song', sa.Column('year', sa.Integer(), nullable=True)) + op.create_index(op.f('ix_song_year'), 'song', ['year'], unique=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_song_year'), table_name='song') + op.drop_column('song', 'year') + # ### end Alembic commands ### diff --git a/project/requirements.txt b/project/requirements.txt index 5b650d0..003e864 100644 --- a/project/requirements.txt +++ b/project/requirements.txt @@ -1,5 +1,5 @@ -alembic==1.7.1 -asyncpg==0.24.0 -fastapi==0.68.1 -sqlmodel==0.0.4 -uvicorn==0.15.0 +alembic==1.11.1 +asyncpg==0.28.0 +fastapi==0.100.0 +sqlmodel==0.0.8 +uvicorn==0.22.0