Skip to content

Commit d1dd328

Browse files
authored
Merge pull request #25 from rafsaf/sqlalchemy_2_0_migration
update poetry versions and bump to python 3.11
2 parents 42859ba + 3594ef6 commit d1dd328

File tree

17 files changed

+912
-765
lines changed

17 files changed

+912
-765
lines changed

.github/workflows/build_docker_image.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
- name: Set up Python
2020
uses: actions/setup-python@v2
2121
with:
22-
python-version: "3.10"
22+
python-version: "3.11"
2323

2424
- name: Generate projects from templates using cookiecutter
2525
# minimal_project folder

.github/workflows/manual_build_docker_image.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
- name: Set up Python
1818
uses: actions/setup-python@v2
1919
with:
20-
python-version: "3.10"
20+
python-version: "3.11"
2121

2222
- name: Generate projects from templates using cookiecutter
2323
# minimal_project folder

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
- name: Set up Python
2525
uses: actions/setup-python@v2
2626
with:
27-
python-version: "3.10"
27+
python-version: "3.11"
2828

2929
# Below will create fresh template in path: minimal_project
3030
- name: Generate project from template using cookiecutter

{{cookiecutter.project_name}}/alembic/versions/2022050745_init_user_model_d1252175c146.py renamed to {{cookiecutter.project_name}}/alembic/versions/2023020440_init_user_model_07c71f4389b6.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
"""init_user_model
22
3-
Revision ID: d1252175c146
3+
Revision ID: 07c71f4389b6
44
Revises:
5-
Create Date: 2022-05-07 15:45:09.674258
5+
Create Date: 2023-02-04 23:40:00.426237
66
77
"""
88
from alembic import op
99
import sqlalchemy as sa
10-
from sqlalchemy.dialects import postgresql
10+
1111

1212
# revision identifiers, used by Alembic.
13-
revision = "d1252175c146"
13+
revision = "07c71f4389b6"
1414
down_revision = None
1515
branch_labels = None
1616
depends_on = None
@@ -20,7 +20,7 @@ def upgrade():
2020
# ### commands auto generated by Alembic - please adjust! ###
2121
op.create_table(
2222
"user_model",
23-
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
23+
sa.Column("id", sa.UUID(), nullable=False),
2424
sa.Column("email", sa.String(length=254), nullable=False),
2525
sa.Column("hashed_password", sa.String(length=128), nullable=False),
2626
sa.PrimaryKeyConstraint("id"),

{{cookiecutter.project_name}}/app/api/deps.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,11 @@ async def get_session() -> AsyncGenerator[AsyncSession, None]:
2222
async def get_current_user(
2323
session: AsyncSession = Depends(get_session), token: str = Depends(reusable_oauth2)
2424
) -> User:
25-
2625
try:
2726
payload = jwt.decode(
2827
token, config.settings.SECRET_KEY, algorithms=[security.JWT_ALGORITHM]
2928
)
30-
except (jwt.DecodeError):
29+
except jwt.DecodeError:
3130
raise HTTPException(
3231
status_code=status.HTTP_403_FORBIDDEN,
3332
detail="Could not validate credentials.",
@@ -48,7 +47,7 @@ async def get_current_user(
4847
)
4948

5049
result = await session.execute(select(User).where(User.id == token_data.sub))
51-
user: User | None = result.scalars().first()
50+
user = result.scalars().first()
5251

5352
if not user:
5453
raise HTTPException(status_code=404, detail="User not found.")

{{cookiecutter.project_name}}/app/api/endpoints/auth.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ async def login_access_token(
2424
"""OAuth2 compatible token, get an access token for future requests using username and password"""
2525

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

2929
if user is None:
3030
raise HTTPException(status_code=400, detail="Incorrect email or password")
@@ -69,7 +69,7 @@ async def refresh_token(
6969
)
7070

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

7474
if user is None:
7575
raise HTTPException(status_code=404, detail="User not found")

{{cookiecutter.project_name}}/app/core/config.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,15 @@
2121
Note, complex types like lists are read as json-encoded strings.
2222
"""
2323

24+
import tomllib
2425
from pathlib import Path
2526
from typing import Literal
2627

27-
import toml
2828
from pydantic import AnyHttpUrl, BaseSettings, EmailStr, PostgresDsn, validator
2929

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

3334

3435
class Settings(BaseSettings):
Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
"""SQLAlchemy async engine and sessions tools"""
1+
"""
2+
SQLAlchemy async engine and sessions tools
23
3-
from typing import TYPE_CHECKING
4+
https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html
5+
"""
46

5-
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
6-
from sqlalchemy.orm.session import sessionmaker
7+
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
78

89
from app.core import config
910

@@ -14,7 +15,4 @@
1415

1516

1617
async_engine = create_async_engine(sqlalchemy_database_uri, pool_pre_ping=True)
17-
async_session = sessionmaker(async_engine, expire_on_commit=False, class_=AsyncSession) # type: ignore
18-
19-
if TYPE_CHECKING:
20-
async_session: sessionmaker[AsyncSession] # type: ignore
18+
async_session = async_sessionmaker(async_engine, expire_on_commit=False)

{{cookiecutter.project_name}}/app/initial_data.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ async def main() -> None:
2020
result = await session.execute(
2121
select(User).where(User.email == config.settings.FIRST_SUPERUSER_EMAIL)
2222
)
23-
user: User | None = result.scalars().first()
23+
user = result.scalars().first()
2424

2525
if user is None:
2626
new_superuser = User(

{{cookiecutter.project_name}}/app/models.py

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,29 +13,24 @@
1313
# apply all migrations
1414
alembic upgrade head
1515
"""
16-
1716
import uuid
18-
from dataclasses import dataclass, field
1917

20-
from sqlalchemy import Column, String
18+
from sqlalchemy import String
2119
from sqlalchemy.dialects.postgresql import UUID
22-
from sqlalchemy.orm import registry
20+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
21+
2322

24-
Base = registry()
23+
class Base(DeclarativeBase):
24+
pass
2525

2626

27-
@Base.mapped
28-
@dataclass
29-
class User:
27+
class User(Base):
3028
__tablename__ = "user_model"
31-
__sa_dataclass_metadata_key__ = "sa"
3229

33-
id: uuid.UUID = field(
34-
init=False,
35-
default_factory=uuid.uuid4,
36-
metadata={"sa": Column(UUID(as_uuid=True), primary_key=True)},
30+
id: Mapped[str] = mapped_column(
31+
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
3732
)
38-
email: str = field(
39-
metadata={"sa": Column(String(254), nullable=False, unique=True, index=True)}
33+
email: Mapped[str] = mapped_column(
34+
String(254), nullable=False, unique=True, index=True
4035
)
41-
hashed_password: str = field(metadata={"sa": Column(String(128), nullable=False)})
36+
hashed_password: Mapped[str] = mapped_column(String(128), nullable=False)

{{cookiecutter.project_name}}/app/tests/conftest.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import asyncio
22
from typing import AsyncGenerator
3-
from uuid import UUID
43

54
import pytest
65
import pytest_asyncio
@@ -13,7 +12,7 @@
1312
from app.main import app
1413
from app.models import Base, User
1514

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

3937
# always drop and create test db tables between tests session
4038
async with async_engine.begin() as conn:
@@ -66,7 +64,7 @@ async def default_user(test_db_setup_sessionmaker) -> User:
6664
result = await session.execute(
6765
select(User).where(User.email == default_user_email)
6866
)
69-
user: User | None = result.scalars().first()
67+
user = result.scalars().first()
7068
if user is None:
7169
new_user = User(
7270
email=default_user_email,

{{cookiecutter.project_name}}/app/tests/test_users.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ async def test_reset_current_user_password(
4444
)
4545
assert response.status_code == 200
4646
result = await session.execute(select(User).where(User.id == default_user_id))
47-
user: User | None = result.scalars().first()
47+
user = result.scalars().first()
4848
assert user is not None
4949
assert user.hashed_password != default_user_password_hash
5050

@@ -62,5 +62,5 @@ async def test_register_new_user(
6262
)
6363
assert response.status_code == 200
6464
result = await session.execute(select(User).where(User.email == "qwe@example.com"))
65-
user: User | None = result.scalars().first()
65+
user = result.scalars().first()
6666
assert user is not None

{{cookiecutter.project_name}}/docker-compose.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ services:
1111
restart: unless-stopped
1212
image: postgres:latest
1313
volumes:
14-
- ./default_database_data/db:/var/lib/postgresql/data
14+
- default_database_data:/var/lib/postgresql/data
1515
environment:
1616
- POSTGRES_DB=${DEFAULT_DATABASE_DB}
1717
- POSTGRES_USER=${DEFAULT_DATABASE_USER}
@@ -25,7 +25,7 @@ services:
2525
restart: unless-stopped
2626
image: postgres:latest
2727
volumes:
28-
- ./test_database_data/db:/var/lib/postgresql/data
28+
- test_database_data:/var/lib/postgresql/data
2929
environment:
3030
- POSTGRES_DB=${TEST_DATABASE_DB}
3131
- POSTGRES_USER=${TEST_DATABASE_USER}
@@ -34,3 +34,7 @@ services:
3434
- .env
3535
ports:
3636
- "${TEST_DATABASE_PORT}:5432"
37+
38+
volumes:
39+
test_database_data:
40+
default_database_data:

0 commit comments

Comments
 (0)