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

migrate to sqlalchemy 2.0, update poetry versions and bump to python 3.11 #25

Merged
merged 3 commits into from
Feb 4, 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
2 changes: 1 addition & 1 deletion .github/workflows/build_docker_image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: "3.10"
python-version: "3.11"

- name: Generate projects from templates using cookiecutter
# minimal_project folder
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/manual_build_docker_image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: "3.10"
python-version: "3.11"

- name: Generate projects from templates using cookiecutter
# minimal_project folder
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: "3.10"
python-version: "3.11"

# Below will create fresh template in path: minimal_project
- name: Generate project from template using cookiecutter
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
"""init_user_model

Revision ID: d1252175c146
Revision ID: 07c71f4389b6
Revises:
Create Date: 2022-05-07 15:45:09.674258
Create Date: 2023-02-04 23:40:00.426237

"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql


# revision identifiers, used by Alembic.
revision = "d1252175c146"
revision = "07c71f4389b6"
down_revision = None
branch_labels = None
depends_on = None
Expand All @@ -20,7 +20,7 @@ def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"user_model",
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("id", sa.UUID(), nullable=False),
sa.Column("email", sa.String(length=254), nullable=False),
sa.Column("hashed_password", sa.String(length=128), nullable=False),
sa.PrimaryKeyConstraint("id"),
Expand Down
5 changes: 2 additions & 3 deletions {{cookiecutter.project_name}}/app/api/deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,11 @@ async def get_session() -> AsyncGenerator[AsyncSession, None]:
async def get_current_user(
session: AsyncSession = Depends(get_session), token: str = Depends(reusable_oauth2)
) -> User:

try:
payload = jwt.decode(
token, config.settings.SECRET_KEY, algorithms=[security.JWT_ALGORITHM]
)
except (jwt.DecodeError):
except jwt.DecodeError:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Could not validate credentials.",
Expand All @@ -48,7 +47,7 @@ async def get_current_user(
)

result = await session.execute(select(User).where(User.id == token_data.sub))
user: User | None = result.scalars().first()
user = result.scalars().first()

if not user:
raise HTTPException(status_code=404, detail="User not found.")
Expand Down
4 changes: 2 additions & 2 deletions {{cookiecutter.project_name}}/app/api/endpoints/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ async def login_access_token(
"""OAuth2 compatible token, get an access token for future requests using username and password"""

result = await session.execute(select(User).where(User.email == form_data.username))
user: User | None = result.scalars().first()
user = result.scalars().first()

if user is None:
raise HTTPException(status_code=400, detail="Incorrect email or password")
Expand Down Expand Up @@ -69,7 +69,7 @@ async def refresh_token(
)

result = await session.execute(select(User).where(User.id == token_data.sub))
user: User | None = result.scalars().first()
user = result.scalars().first()

if user is None:
raise HTTPException(status_code=404, detail="User not found")
Expand Down
5 changes: 3 additions & 2 deletions {{cookiecutter.project_name}}/app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@
Note, complex types like lists are read as json-encoded strings.
"""

import tomllib
from pathlib import Path
from typing import Literal

import toml
from pydantic import AnyHttpUrl, BaseSettings, EmailStr, PostgresDsn, validator

PROJECT_DIR = Path(__file__).parent.parent.parent
PYPROJECT_CONTENT = toml.load(f"{PROJECT_DIR}/pyproject.toml")["tool"]["poetry"]
with open(f"{PROJECT_DIR}/pyproject.toml", "rb") as f:
PYPROJECT_CONTENT = tomllib.load(f)["tool"]["poetry"]


class Settings(BaseSettings):
Expand Down
14 changes: 6 additions & 8 deletions {{cookiecutter.project_name}}/app/core/session.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""SQLAlchemy async engine and sessions tools"""
"""
SQLAlchemy async engine and sessions tools

from typing import TYPE_CHECKING
https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html
"""

from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm.session import sessionmaker
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine

from app.core import config

Expand All @@ -14,7 +15,4 @@


async_engine = create_async_engine(sqlalchemy_database_uri, pool_pre_ping=True)
async_session = sessionmaker(async_engine, expire_on_commit=False, class_=AsyncSession) # type: ignore

if TYPE_CHECKING:
async_session: sessionmaker[AsyncSession] # type: ignore
async_session = async_sessionmaker(async_engine, expire_on_commit=False)
2 changes: 1 addition & 1 deletion {{cookiecutter.project_name}}/app/initial_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ async def main() -> None:
result = await session.execute(
select(User).where(User.email == config.settings.FIRST_SUPERUSER_EMAIL)
)
user: User | None = result.scalars().first()
user = result.scalars().first()

if user is None:
new_superuser = User(
Expand Down
27 changes: 11 additions & 16 deletions {{cookiecutter.project_name}}/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,24 @@
# apply all migrations
alembic upgrade head
"""

import uuid
from dataclasses import dataclass, field

from sqlalchemy import Column, String
from sqlalchemy import String
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import registry
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column


Base = registry()
class Base(DeclarativeBase):
pass


@Base.mapped
@dataclass
class User:
class User(Base):
__tablename__ = "user_model"
__sa_dataclass_metadata_key__ = "sa"

id: uuid.UUID = field(
init=False,
default_factory=uuid.uuid4,
metadata={"sa": Column(UUID(as_uuid=True), primary_key=True)},
id: Mapped[str] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
email: str = field(
metadata={"sa": Column(String(254), nullable=False, unique=True, index=True)}
email: Mapped[str] = mapped_column(
String(254), nullable=False, unique=True, index=True
)
hashed_password: str = field(metadata={"sa": Column(String(128), nullable=False)})
hashed_password: Mapped[str] = mapped_column(String(128), nullable=False)
6 changes: 2 additions & 4 deletions {{cookiecutter.project_name}}/app/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import asyncio
from typing import AsyncGenerator
from uuid import UUID

import pytest
import pytest_asyncio
Expand All @@ -13,7 +12,7 @@
from app.main import app
from app.models import Base, User

default_user_id = UUID("b75365d9-7bf9-4f54-add5-aeab333a087b")
default_user_id = "b75365d9-7bf9-4f54-add5-aeab333a087b"
default_user_email = "geralt@wiedzmin.pl"
default_user_password = "geralt"
default_user_password_hash = security.get_password_hash(default_user_password)
Expand All @@ -34,7 +33,6 @@ def event_loop():
async def test_db_setup_sessionmaker():
# assert if we use TEST_DB URL for 100%
assert config.settings.ENVIRONMENT == "PYTEST"
assert str(async_engine.url) == config.settings.TEST_SQLALCHEMY_DATABASE_URI

# always drop and create test db tables between tests session
async with async_engine.begin() as conn:
Expand Down Expand Up @@ -66,7 +64,7 @@ async def default_user(test_db_setup_sessionmaker) -> User:
result = await session.execute(
select(User).where(User.email == default_user_email)
)
user: User | None = result.scalars().first()
user = result.scalars().first()
if user is None:
new_user = User(
email=default_user_email,
Expand Down
4 changes: 2 additions & 2 deletions {{cookiecutter.project_name}}/app/tests/test_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ async def test_reset_current_user_password(
)
assert response.status_code == 200
result = await session.execute(select(User).where(User.id == default_user_id))
user: User | None = result.scalars().first()
user = result.scalars().first()
assert user is not None
assert user.hashed_password != default_user_password_hash

Expand All @@ -62,5 +62,5 @@ async def test_register_new_user(
)
assert response.status_code == 200
result = await session.execute(select(User).where(User.email == "qwe@example.com"))
user: User | None = result.scalars().first()
user = result.scalars().first()
assert user is not None
8 changes: 6 additions & 2 deletions {{cookiecutter.project_name}}/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ services:
restart: unless-stopped
image: postgres:latest
volumes:
- ./default_database_data/db:/var/lib/postgresql/data
- default_database_data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=${DEFAULT_DATABASE_DB}
- POSTGRES_USER=${DEFAULT_DATABASE_USER}
Expand All @@ -25,7 +25,7 @@ services:
restart: unless-stopped
image: postgres:latest
volumes:
- ./test_database_data/db:/var/lib/postgresql/data
- test_database_data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=${TEST_DATABASE_DB}
- POSTGRES_USER=${TEST_DATABASE_USER}
Expand All @@ -34,3 +34,7 @@ services:
- .env
ports:
- "${TEST_DATABASE_PORT}:5432"

volumes:
test_database_data:
default_database_data:
Loading