🎨 Format files with pre-commit and Ruff (#611)

This commit is contained in:
Sebastián Ramírez
2024-02-25 19:39:33 +01:00
committed by GitHub
parent 2802a4df9e
commit 0cc802eec8
35 changed files with 156 additions and 163 deletions

View File

@@ -1 +1 @@
Generic single-database configuration.
Generic single-database configuration.

View File

@@ -1,10 +1,8 @@
from __future__ import with_statement
import os
from logging.config import fileConfig
from alembic import context
from sqlalchemy import engine_from_config, pool
from logging.config import fileConfig
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
@@ -69,7 +67,9 @@ def run_migrations_online():
configuration = config.get_section(config.config_ini_section)
configuration["sqlalchemy.url"] = get_url()
connectable = engine_from_config(
configuration, prefix="sqlalchemy.", poolclass=pool.NullPool,
configuration,
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:

View File

@@ -1,17 +1,16 @@
"""Initialize models
Revision ID: e2412789c190
Revises:
Revises:
Create Date: 2023-11-24 22:55:43.195942
"""
from alembic import op
import sqlalchemy as sa
import sqlmodel.sql.sqltypes
from alembic import op
# revision identifiers, used by Alembic.
revision = 'e2412789c190'
revision = "e2412789c190"
down_revision = None
branch_labels = None
depends_on = None
@@ -19,30 +18,37 @@ depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('user',
sa.Column('email', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('is_active', sa.Boolean(), nullable=False),
sa.Column('is_superuser', sa.Boolean(), nullable=False),
sa.Column('full_name', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('hashed_password', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.PrimaryKeyConstraint('id')
op.create_table(
"user",
sa.Column("email", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("is_active", sa.Boolean(), nullable=False),
sa.Column("is_superuser", sa.Boolean(), nullable=False),
sa.Column("full_name", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column(
"hashed_password", sqlmodel.sql.sqltypes.AutoString(), nullable=False
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=True)
op.create_table('item',
sa.Column('description', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('owner_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['owner_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
op.create_index(op.f("ix_user_email"), "user", ["email"], unique=True)
op.create_table(
"item",
sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("title", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("owner_id", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(
["owner_id"],
["user.id"],
),
sa.PrimaryKeyConstraint("id"),
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('item')
op.drop_index(op.f('ix_user_email'), table_name='user')
op.drop_table('user')
op.drop_table("item")
op.drop_index(op.f("ix_user_email"), table_name="user")
op.drop_table("user")
# ### end Alembic commands ###

View File

@@ -1,10 +1,10 @@
from typing import Any
from fastapi import APIRouter, HTTPException
from sqlmodel import select, func
from sqlmodel import func, select
from app.api.deps import CurrentUser, SessionDep
from app.models import Item, ItemCreate, ItemOut, ItemUpdate, Message, ItemsOut
from app.models import Item, ItemCreate, ItemOut, ItemsOut, ItemUpdate, Message
router = APIRouter()
@@ -22,7 +22,7 @@ def read_items(
if current_user.is_superuser:
statement = select(Item).offset(skip).limit(limit)
items = session.exec(statement).all()
items = session.exec(statement).all()
else:
statement = (
select(Item)

View File

@@ -1,7 +1,7 @@
from typing import Any, List
from typing import Any
from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import select, func
from sqlmodel import func, select
from app import crud
from app.api.deps import (
@@ -28,9 +28,7 @@ router = APIRouter()
@router.get(
"/",
dependencies=[Depends(get_current_active_superuser)],
response_model=UsersOut
"/", dependencies=[Depends(get_current_active_superuser)], response_model=UsersOut
)
def read_users(session: SessionDep, skip: int = 0, limit: int = 100) -> Any:
"""

View File

@@ -1,4 +1,5 @@
from typing import Annotated, Generator
from collections.abc import Generator
from typing import Annotated
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer

View File

@@ -1,5 +1,5 @@
import secrets
from typing import Any, Dict, List, Optional, Union
from typing import Any
from pydantic import AnyHttpUrl, BaseSettings, EmailStr, HttpUrl, PostgresDsn, validator
@@ -14,21 +14,21 @@ class Settings(BaseSettings):
# BACKEND_CORS_ORIGINS is a JSON-formatted list of origins
# e.g: '["http://localhost", "http://localhost:4200", "http://localhost:3000", \
# "http://localhost:8080", "http://local.dockertoolbox.tiangolo.com"]'
BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = []
BACKEND_CORS_ORIGINS: list[AnyHttpUrl] = []
@validator("BACKEND_CORS_ORIGINS", pre=True)
def assemble_cors_origins(cls, v: Union[str, List[str]]) -> Union[List[str], str]:
def assemble_cors_origins(cls, v: str | list[str]) -> list[str] | str:
if isinstance(v, str) and not v.startswith("["):
return [i.strip() for i in v.split(",")]
elif isinstance(v, (list, str)):
elif isinstance(v, list | str):
return v
raise ValueError(v)
PROJECT_NAME: str
SENTRY_DSN: Optional[HttpUrl] = None
SENTRY_DSN: HttpUrl | None = None
@validator("SENTRY_DSN", pre=True)
def sentry_dsn_can_be_blank(cls, v: str) -> Optional[str]:
def sentry_dsn_can_be_blank(cls, v: str) -> str | None:
if len(v) == 0:
return None
return v
@@ -37,10 +37,10 @@ class Settings(BaseSettings):
POSTGRES_USER: str
POSTGRES_PASSWORD: str
POSTGRES_DB: str
SQLALCHEMY_DATABASE_URI: Optional[PostgresDsn] = None
SQLALCHEMY_DATABASE_URI: PostgresDsn | None = None
@validator("SQLALCHEMY_DATABASE_URI", pre=True)
def assemble_db_connection(cls, v: Optional[str], values: Dict[str, Any]) -> Any:
def assemble_db_connection(cls, v: str | None, values: dict[str, Any]) -> Any:
if isinstance(v, str):
return v
return PostgresDsn.build(
@@ -52,15 +52,15 @@ class Settings(BaseSettings):
)
SMTP_TLS: bool = True
SMTP_PORT: Optional[int] = None
SMTP_HOST: Optional[str] = None
SMTP_USER: Optional[str] = None
SMTP_PASSWORD: Optional[str] = None
EMAILS_FROM_EMAIL: Optional[EmailStr] = None
EMAILS_FROM_NAME: Optional[str] = None
SMTP_PORT: int | None = None
SMTP_HOST: str | None = None
SMTP_USER: str | None = None
SMTP_PASSWORD: str | None = None
EMAILS_FROM_EMAIL: EmailStr | None = None
EMAILS_FROM_NAME: str | None = None
@validator("EMAILS_FROM_NAME")
def get_project_name(cls, v: Optional[str], values: Dict[str, Any]) -> str:
def get_project_name(cls, v: str | None, values: dict[str, Any]) -> str:
if not v:
return values["PROJECT_NAME"]
return v
@@ -70,7 +70,7 @@ class Settings(BaseSettings):
EMAILS_ENABLED: bool = False
@validator("EMAILS_ENABLED", pre=True)
def get_emails_enabled(cls, v: bool, values: Dict[str, Any]) -> bool:
def get_emails_enabled(cls, v: bool, values: dict[str, Any]) -> bool:
return bool(
values.get("SMTP_HOST")
and values.get("SMTP_PORT")

View File

@@ -1,5 +1,5 @@
from datetime import datetime, timedelta
from typing import Any, Union
from typing import Any
from jose import jwt
from passlib.context import CryptContext
@@ -12,9 +12,7 @@ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
ALGORITHM = "HS256"
def create_access_token(
subject: Union[str, Any], expires_delta: timedelta = None
) -> str:
def create_access_token(subject: str | Any, expires_delta: timedelta = None) -> str:
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:

View File

@@ -1,16 +1,15 @@
from .crud_item import item
from .crud_user import user
# For a new basic set of CRUD operations you could just do
# from .base import CRUDBase
# from app.models.item import Item
# from app.schemas.item import ItemCreate, ItemUpdate
# item = CRUDBase[Item, ItemCreate, ItemUpdate](Item)
from sqlmodel import Session, select
from app.core.security import get_password_hash, verify_password
from app.models import UserCreate, User
from app.models import User, UserCreate
from .crud_item import item as item
from .crud_user import user as user
def create_user(*, session: Session, user_create: UserCreate) -> User:
@@ -30,9 +29,9 @@ def get_user_by_email(*, session: Session, email: str) -> User | None:
def authenticate(*, session: Session, email: str, password: str) -> User | None:
user = get_user_by_email(session=session, email=email)
if not user:
db_user = get_user_by_email(session=session, email=email)
if not db_user:
return None
if not verify_password(password, user.hashed_password):
if not verify_password(password, db_user.hashed_password):
return None
return user
return db_user

View File

@@ -1,4 +1,4 @@
from typing import Any, Dict, Generic, List, Optional, Type, TypeVar, Union
from typing import Any, Generic, TypeVar
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
@@ -10,7 +10,7 @@ UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)
class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
def __init__(self, model: Type[ModelType]):
def __init__(self, model: type[ModelType]):
"""
CRUD object with default methods to Create, Read, Update, Delete (CRUD).
@@ -21,7 +21,7 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
"""
self.model = model
def get(self, db: Session, id: Any) -> Optional[ModelType]:
def get(self, db: Session, id: Any) -> ModelType | None:
return db.query(self.model).filter(self.model.id == id).first()
def create(self, db: Session, *, obj_in: CreateSchemaType) -> ModelType:
@@ -37,7 +37,7 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
db: Session,
*,
db_obj: ModelType,
obj_in: Union[UpdateSchemaType, Dict[str, Any]]
obj_in: UpdateSchemaType | dict[str, Any],
) -> ModelType:
obj_data = jsonable_encoder(db_obj)
if isinstance(obj_in, dict):

View File

@@ -1,5 +1,3 @@
from typing import List
from fastapi.encoders import jsonable_encoder
from sqlalchemy.orm import Session
@@ -21,7 +19,7 @@ class CRUDItem(CRUDBase[Item, ItemCreate, ItemUpdate]):
def get_multi_by_owner(
self, db: Session, *, owner_id: int, skip: int = 0, limit: int = 100
) -> List[Item]:
) -> list[Item]:
return (
db.query(self.model)
.filter(Item.owner_id == owner_id)

View File

@@ -1,4 +1,4 @@
from typing import Any, Dict, Optional, Union
from typing import Any
from sqlalchemy.orm import Session
@@ -9,7 +9,7 @@ from app.schemas.user import UserCreate, UserUpdate
class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]):
def get_by_email(self, db: Session, *, email: str) -> Optional[User]:
def get_by_email(self, db: Session, *, email: str) -> User | None:
return db.query(User).filter(User.email == email).first()
def create(self, db: Session, *, obj_in: UserCreate) -> User:
@@ -25,7 +25,7 @@ class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]):
return db_obj
def update(
self, db: Session, *, db_obj: User, obj_in: Union[UserUpdate, Dict[str, Any]]
self, db: Session, *, db_obj: User, obj_in: UserUpdate | dict[str, Any]
) -> User:
if isinstance(obj_in, dict):
update_data = obj_in
@@ -37,7 +37,7 @@ class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]):
update_data["hashed_password"] = hashed_password
return super().update(db, db_obj=db_obj, obj_in=update_data)
def authenticate(self, db: Session, *, email: str, password: str) -> Optional[User]:
def authenticate(self, db: Session, *, email: str, password: str) -> User | None:
user = self.get_by_email(db, email=email)
if not user:
return None

View File

@@ -23,4 +23,4 @@
.mj-column-per-100 { width:100% !important; max-width: 100%; }
}</style><style type="text/css"></style></head><body style="background-color:#ffffff;"><div style="background-color:#ffffff;"><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--><div style="Margin:0px auto;max-width:600px;"><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;"><tbody><tr><td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;vertical-align:top;"><!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]--><div class="mj-column-per-100 outlook-group-fix" style="font-size:13px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"><tr><td style="font-size:0px;padding:10px 25px;word-break:break-word;"><p style="border-top:solid 4px #555555;font-size:1;margin:0px auto;width:100%;"></p><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 4px #555555;font-size:1;margin:0px auto;width:550px;" role="presentation" width="550px" ><tr><td style="height:0;line-height:0;"> &nbsp;
</td></tr></table><![endif]--></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:helvetica;font-size:20px;line-height:1;text-align:left;color:#555555;">{{ project_name }} - New Account</div></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#555555;">You have a new account:</div></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#555555;">Username: {{ username }}</div></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#555555;">Password: {{ password }}</div></td></tr><tr><td align="center" vertical-align="middle" style="font-size:0px;padding:50px 0px;word-break:break-word;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;"><tr><td align="center" bgcolor="#414141" role="presentation" style="border:none;border-radius:3px;cursor:auto;padding:10px 25px;background:#414141;" valign="middle"><a href="{{ link }}" style="background:#414141;color:#ffffff;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;Margin:0;text-decoration:none;text-transform:none;" target="_blank">Go to Dashboard</a></td></tr></table></td></tr><tr><td style="font-size:0px;padding:10px 25px;word-break:break-word;"><p style="border-top:solid 2px #555555;font-size:1;margin:0px auto;width:100%;"></p><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 2px #555555;font-size:1;margin:0px auto;width:550px;" role="presentation" width="550px" ><tr><td style="height:0;line-height:0;"> &nbsp;
</td></tr></table><![endif]--></td></tr></table></div><!--[if mso | IE]></td></tr></table><![endif]--></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></div></body></html>
</td></tr></table><![endif]--></td></tr></table></div><!--[if mso | IE]></td></tr></table><![endif]--></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></div></body></html>

View File

@@ -23,4 +23,4 @@
.mj-column-per-100 { width:100% !important; max-width: 100%; }
}</style><style type="text/css"></style></head><body style="background-color:#ffffff;"><div style="background-color:#ffffff;"><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--><div style="Margin:0px auto;max-width:600px;"><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;"><tbody><tr><td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;vertical-align:top;"><!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]--><div class="mj-column-per-100 outlook-group-fix" style="font-size:13px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"><tr><td style="font-size:0px;padding:10px 25px;word-break:break-word;"><p style="border-top:solid 4px #555555;font-size:1;margin:0px auto;width:100%;"></p><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 4px #555555;font-size:1;margin:0px auto;width:550px;" role="presentation" width="550px" ><tr><td style="height:0;line-height:0;"> &nbsp;
</td></tr></table><![endif]--></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:helvetica;font-size:20px;line-height:1;text-align:left;color:#555555;">{{ project_name }} - Password Recovery</div></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#555555;">We received a request to recover the password for user {{ username }} with email {{ email }}</div></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#555555;">Reset your password by clicking the button below:</div></td></tr><tr><td align="center" vertical-align="middle" style="font-size:0px;padding:50px 0px;word-break:break-word;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;"><tr><td align="center" bgcolor="#414141" role="presentation" style="border:none;border-radius:3px;cursor:auto;padding:10px 25px;background:#414141;" valign="middle"><a href="{{ link }}" style="background:#414141;color:#ffffff;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;Margin:0;text-decoration:none;text-transform:none;" target="_blank">Reset Password</a></td></tr></table></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#555555;">Or open the following link:</div></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#555555;"><a href="{{ link }}">{{ link }}</a></div></td></tr><tr><td style="font-size:0px;padding:10px 25px;word-break:break-word;"><p style="border-top:solid 2px #555555;font-size:1;margin:0px auto;width:100%;"></p><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 2px #555555;font-size:1;margin:0px auto;width:550px;" role="presentation" width="550px" ><tr><td style="height:0;line-height:0;"> &nbsp;
</td></tr></table><![endif]--></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:14px;line-height:1;text-align:left;color:#555555;">The reset password link / button will expire in {{ valid_hours }} hours.</div></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:14px;line-height:1;text-align:left;color:#555555;">If you didn't request a password recovery you can disregard this email.</div></td></tr></table></div><!--[if mso | IE]></td></tr></table><![endif]--></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></div></body></html>
</td></tr></table><![endif]--></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:14px;line-height:1;text-align:left;color:#555555;">The reset password link / button will expire in {{ valid_hours }} hours.</div></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:14px;line-height:1;text-align:left;color:#555555;">If you didn't request a password recovery you can disregard this email.</div></td></tr></table></div><!--[if mso | IE]></td></tr></table><![endif]--></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></div></body></html>

View File

@@ -22,4 +22,4 @@
<![endif]--><!--[if !mso]><!--><link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css"><style type="text/css">@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);</style><!--<![endif]--><style type="text/css">@media only screen and (min-width:480px) {
.mj-column-per-100 { width:100% !important; max-width: 100%; }
}</style><style type="text/css"></style></head><body style="background-color:#ffffff;"><div style="background-color:#ffffff;"><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--><div style="Margin:0px auto;max-width:600px;"><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;"><tbody><tr><td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;vertical-align:top;"><!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]--><div class="mj-column-per-100 outlook-group-fix" style="font-size:13px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"><tr><td style="font-size:0px;padding:10px 25px;word-break:break-word;"><p style="border-top:solid 4px #555555;font-size:1;margin:0px auto;width:100%;"></p><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 4px #555555;font-size:1;margin:0px auto;width:550px;" role="presentation" width="550px" ><tr><td style="height:0;line-height:0;"> &nbsp;
</td></tr></table><![endif]--></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:helvetica;font-size:20px;line-height:1;text-align:left;color:#555555;">{{ project_name }}</div></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#555555;">Test email for: {{ email }}</div></td></tr></table></div><!--[if mso | IE]></td></tr></table><![endif]--></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></div></body></html>
</td></tr></table><![endif]--></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:helvetica;font-size:20px;line-height:1;text-align:left;color:#555555;">{{ project_name }}</div></td></tr><tr><td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#555555;">Test email for: {{ email }}</div></td></tr></table></div><!--[if mso | IE]></td></tr></table><![endif]--></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></div></body></html>

View File

@@ -1,5 +1,3 @@
from typing import Union
from pydantic import EmailStr
from sqlmodel import Field, Relationship, SQLModel
@@ -9,7 +7,7 @@ class UserBase(SQLModel):
email: EmailStr = Field(unique=True, index=True)
is_active: bool = True
is_superuser: bool = False
full_name: Union[str, None] = None
full_name: str | None = None
# Properties to receive via API on creation
@@ -20,18 +18,18 @@ class UserCreate(UserBase):
class UserCreateOpen(SQLModel):
email: EmailStr
password: str
full_name: Union[str, None] = None
full_name: str | None = None
# Properties to receive via API on update, all are optional
class UserUpdate(UserBase):
email: Union[EmailStr, None] = None
password: Union[str, None] = None
email: EmailStr | None = None
password: str | None = None
class UserUpdateMe(SQLModel):
full_name: Union[str, None] = None
email: Union[EmailStr, None] = None
full_name: str | None = None
email: EmailStr | None = None
class UpdatePassword(SQLModel):
@@ -41,7 +39,7 @@ class UpdatePassword(SQLModel):
# Database model, database table inferred from class name
class User(UserBase, table=True):
id: Union[int, None] = Field(default=None, primary_key=True)
id: int | None = Field(default=None, primary_key=True)
hashed_password: str
items: list["Item"] = Relationship(back_populates="owner")
@@ -59,7 +57,7 @@ class UsersOut(SQLModel):
# Shared properties
class ItemBase(SQLModel):
title: str
description: Union[str, None] = None
description: str | None = None
# Properties to receive on item creation
@@ -69,17 +67,15 @@ class ItemCreate(ItemBase):
# Properties to receive on item update
class ItemUpdate(ItemBase):
title: Union[str, None] = None
title: str | None = None
# Database model, database table inferred from class name
class Item(ItemBase, table=True):
id: Union[int, None] = Field(default=None, primary_key=True)
id: int | None = Field(default=None, primary_key=True)
title: str
owner_id: Union[int, None] = Field(
default=None, foreign_key="user.id", nullable=False
)
owner: Union[User, None] = Relationship(back_populates="items")
owner_id: int | None = Field(default=None, foreign_key="user.id", nullable=False)
owner: User | None = Relationship(back_populates="items")
# Properties to return via API, id is always required
@@ -106,7 +102,7 @@ class Token(SQLModel):
# Contents of JWT token
class TokenPayload(SQLModel):
sub: Union[int, None] = None
sub: int | None = None
class NewPassword(SQLModel):

View File

@@ -1,12 +1,10 @@
from typing import Optional
from pydantic import BaseModel
# Shared properties
class ItemBase(BaseModel):
title: Optional[str] = None
description: Optional[str] = None
title: str | None = None
description: str | None = None
# Properties to receive on item creation

View File

@@ -1,5 +1,3 @@
from typing import Optional
from pydantic import BaseModel
@@ -9,4 +7,4 @@ class Token(BaseModel):
class TokenPayload(BaseModel):
sub: Optional[int] = None
sub: int | None = None

View File

@@ -1,14 +1,12 @@
from typing import Optional
from pydantic import BaseModel, EmailStr
# Shared properties
class UserBase(BaseModel):
email: Optional[EmailStr] = None
is_active: Optional[bool] = True
email: EmailStr | None = None
is_active: bool | None = True
is_superuser: bool = False
full_name: Optional[str] = None
full_name: str | None = None
# Properties to receive via API on creation
@@ -19,11 +17,11 @@ class UserCreate(UserBase):
# Properties to receive via API on update
class UserUpdate(UserBase):
password: Optional[str] = None
password: str | None = None
class UserInDBBase(UserBase):
id: Optional[int] = None
id: int | None = None
class Config:
orm_mode = True

View File

@@ -1,12 +1,10 @@
from typing import Dict
from fastapi.testclient import TestClient
from app.core.config import settings
def test_celery_worker_test(
client: TestClient, superuser_token_headers: Dict[str, str]
client: TestClient, superuser_token_headers: dict[str, str]
) -> None:
data = {"message": "test"}
r = client.post(

View File

@@ -10,7 +10,9 @@ def test_create_item(
) -> None:
data = {"title": "Foo", "description": "Fighters"}
response = client.post(
f"{settings.API_V1_STR}/items/", headers=superuser_token_headers, json=data,
f"{settings.API_V1_STR}/items/",
headers=superuser_token_headers,
json=data,
)
assert response.status_code == 200
content = response.json()
@@ -25,7 +27,8 @@ def test_read_item(
) -> None:
item = create_random_item(db)
response = client.get(
f"{settings.API_V1_STR}/items/{item.id}", headers=superuser_token_headers,
f"{settings.API_V1_STR}/items/{item.id}",
headers=superuser_token_headers,
)
assert response.status_code == 200
content = response.json()

View File

@@ -1,5 +1,3 @@
from typing import Dict
from fastapi.testclient import TestClient
from app.core.config import settings
@@ -18,10 +16,11 @@ def test_get_access_token(client: TestClient) -> None:
def test_use_access_token(
client: TestClient, superuser_token_headers: Dict[str, str]
client: TestClient, superuser_token_headers: dict[str, str]
) -> None:
r = client.post(
f"{settings.API_V1_STR}/login/test-token", headers=superuser_token_headers,
f"{settings.API_V1_STR}/login/test-token",
headers=superuser_token_headers,
)
result = r.json()
assert r.status_code == 200

View File

@@ -1,5 +1,3 @@
from typing import Dict
from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
@@ -10,7 +8,7 @@ from app.tests.utils.utils import random_email, random_lower_string
def test_get_users_superuser_me(
client: TestClient, superuser_token_headers: Dict[str, str]
client: TestClient, superuser_token_headers: dict[str, str]
) -> None:
r = client.get(f"{settings.API_V1_STR}/users/me", headers=superuser_token_headers)
current_user = r.json()
@@ -21,7 +19,7 @@ def test_get_users_superuser_me(
def test_get_users_normal_user_me(
client: TestClient, normal_user_token_headers: Dict[str, str]
client: TestClient, normal_user_token_headers: dict[str, str]
) -> None:
r = client.get(f"{settings.API_V1_STR}/users/me", headers=normal_user_token_headers)
current_user = r.json()
@@ -38,7 +36,9 @@ def test_create_user_new_email(
password = random_lower_string()
data = {"email": username, "password": password}
r = client.post(
f"{settings.API_V1_STR}/users/", headers=superuser_token_headers, json=data,
f"{settings.API_V1_STR}/users/",
headers=superuser_token_headers,
json=data,
)
assert 200 <= r.status_code < 300
created_user = r.json()
@@ -56,7 +56,8 @@ def test_get_existing_user(
user = crud.user.create(db, obj_in=user_in)
user_id = user.id
r = client.get(
f"{settings.API_V1_STR}/users/{user_id}", headers=superuser_token_headers,
f"{settings.API_V1_STR}/users/{user_id}",
headers=superuser_token_headers,
)
assert 200 <= r.status_code < 300
api_user = r.json()
@@ -75,7 +76,9 @@ def test_create_user_existing_username(
crud.user.create(db, obj_in=user_in)
data = {"email": username, "password": password}
r = client.post(
f"{settings.API_V1_STR}/users/", headers=superuser_token_headers, json=data,
f"{settings.API_V1_STR}/users/",
headers=superuser_token_headers,
json=data,
)
created_user = r.json()
assert r.status_code == 400
@@ -83,13 +86,15 @@ def test_create_user_existing_username(
def test_create_user_by_normal_user(
client: TestClient, normal_user_token_headers: Dict[str, str]
client: TestClient, normal_user_token_headers: dict[str, str]
) -> None:
username = random_email()
password = random_lower_string()
data = {"email": username, "password": password}
r = client.post(
f"{settings.API_V1_STR}/users/", headers=normal_user_token_headers, json=data,
f"{settings.API_V1_STR}/users/",
headers=normal_user_token_headers,
json=data,
)
assert r.status_code == 400

View File

@@ -1,4 +1,4 @@
from typing import Dict, Generator
from collections.abc import Generator
import pytest
from fastapi.testclient import TestClient
@@ -24,12 +24,12 @@ def client() -> Generator:
@pytest.fixture(scope="module")
def superuser_token_headers(client: TestClient) -> Dict[str, str]:
def superuser_token_headers(client: TestClient) -> dict[str, str]:
return get_superuser_token_headers(client)
@pytest.fixture(scope="module")
def normal_user_token_headers(client: TestClient, db: Session) -> Dict[str, str]:
def normal_user_token_headers(client: TestClient, db: Session) -> dict[str, str]:
return authentication_token_from_email(
client=client, email=settings.EMAIL_TEST_USER, db=db
)

View File

@@ -1,5 +1,3 @@
from typing import Optional
from sqlalchemy.orm import Session
from app import crud, models
@@ -8,7 +6,7 @@ from app.tests.utils.user import create_random_user
from app.tests.utils.utils import random_lower_string
def create_random_item(db: Session, *, owner_id: Optional[int] = None) -> models.Item:
def create_random_item(db: Session, *, owner_id: int | None = None) -> models.Item:
if owner_id is None:
user = create_random_user(db)
owner_id = user.id

View File

@@ -1,5 +1,3 @@
from typing import Dict
from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
@@ -12,7 +10,7 @@ from app.tests.utils.utils import random_email, random_lower_string
def user_authentication_headers(
*, client: TestClient, email: str, password: str
) -> Dict[str, str]:
) -> dict[str, str]:
data = {"username": email, "password": password}
r = client.post(f"{settings.API_V1_STR}/login/access-token", data=data)
@@ -32,7 +30,7 @@ def create_random_user(db: Session) -> User:
def authentication_token_from_email(
*, client: TestClient, email: str, db: Session
) -> Dict[str, str]:
) -> dict[str, str]:
"""
Return a valid token for the user with given email.

View File

@@ -1,6 +1,5 @@
import random
import string
from typing import Dict
from fastapi.testclient import TestClient
@@ -15,7 +14,7 @@ def random_email() -> str:
return f"{random_lower_string()}@{random_lower_string()}.com"
def get_superuser_token_headers(client: TestClient) -> Dict[str, str]:
def get_superuser_token_headers(client: TestClient) -> dict[str, str]:
login_data = {
"username": settings.FIRST_SUPERUSER,
"password": settings.FIRST_SUPERUSER_PASSWORD,

View File

@@ -1,7 +1,7 @@
import logging
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any, Dict, Optional
from typing import Any
import emails
from emails.template import JinjaTemplate
@@ -14,8 +14,9 @@ def send_email(
email_to: str,
subject_template: str = "",
html_template: str = "",
environment: Dict[str, Any] = {},
environment: dict[str, Any] | None = None,
) -> None:
current_environment = environment or {}
assert settings.EMAILS_ENABLED, "no provided configuration for email variables"
message = emails.Message(
subject=JinjaTemplate(subject_template),
@@ -29,7 +30,7 @@ def send_email(
smtp_options["user"] = settings.SMTP_USER
if settings.SMTP_PASSWORD:
smtp_options["password"] = settings.SMTP_PASSWORD
response = message.send(to=email_to, render=environment, smtp=smtp_options)
response = message.send(to=email_to, render=current_environment, smtp=smtp_options)
logging.info(f"send email result: {response}")
@@ -42,7 +43,7 @@ def send_test_email(email_to: str) -> None:
email_to=email_to,
subject_template=subject,
html_template=template_str,
environment={"project_name": settings.PROJECT_NAME, "email": email_to},
current_environment={"project_name": settings.PROJECT_NAME, "email": email_to},
)
@@ -57,7 +58,7 @@ def send_reset_password_email(email_to: str, email: str, token: str) -> None:
email_to=email_to,
subject_template=subject,
html_template=template_str,
environment={
current_environment={
"project_name": settings.PROJECT_NAME,
"username": email,
"email": email_to,
@@ -77,7 +78,7 @@ def send_new_account_email(email_to: str, username: str, password: str) -> None:
email_to=email_to,
subject_template=subject,
html_template=template_str,
environment={
current_environment={
"project_name": settings.PROJECT_NAME,
"username": username,
"password": password,
@@ -93,12 +94,14 @@ def generate_password_reset_token(email: str) -> str:
expires = now + delta
exp = expires.timestamp()
encoded_jwt = jwt.encode(
{"exp": exp, "nbf": now, "sub": email}, settings.SECRET_KEY, algorithm="HS256",
{"exp": exp, "nbf": now, "sub": email},
settings.SECRET_KEY,
algorithm="HS256",
)
return encoded_jwt
def verify_password_reset_token(token: str) -> Optional[str]:
def verify_password_reset_token(token: str) -> str | None:
try:
decoded_token = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
return decoded_token["email"]