Skip to content

Commit ce73a8d

Browse files
authored
♻️ CODE REFACTOR (#40)
* 🙈 Updated Gitignore Added .idea to gitignore file * ♻️ REFACTOR CORS ORIGIN VALIDATION in Settings refactored backend_cors_origin field in config to use annotated and BeforeValidator instead of the field validator decorator anymore and changed rstrip to strip in the * ♻️ Switched from using Optional[Type] to Type | None According to best practices and PEP 604, instead of Optional[Type], one should use Type | None, argument backed by [Link](https://discuss.python.org/t/clarification-for-pep-604-is-foo-int-none-to-replace-all-use-of-foo-optional-int/26945)
1 parent 01522d1 commit ce73a8d

File tree

11 files changed

+73
-75
lines changed

11 files changed

+73
-75
lines changed

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ testing-project
44
# poetry.lock
55
dev-link/
66

7-
.DS_Store
7+
.DS_Store
8+
.idea/

{{cookiecutter.project_slug}}/backend/app/app/core/config.py

+21-21
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import secrets
2-
from typing import Any, Dict, List, Optional, Union
2+
from typing import Any, Dict, List, Union, Annotated
33

4-
from pydantic import AnyHttpUrl, EmailStr, HttpUrl, field_validator
4+
from pydantic import AnyHttpUrl, EmailStr, HttpUrl, field_validator, BeforeValidator
55
from pydantic_core.core_schema import ValidationInfo
66
from pydantic_settings import BaseSettings
77

8+
def parse_cors(v: Any) -> list[str] | str:
9+
if isinstance(v, str) and not v.startswith("["):
10+
return [i.strip() for i in v.split(",")]
11+
elif isinstance(v, list | str):
12+
return v
13+
raise ValueError(v)
814

915
class Settings(BaseSettings):
1016
API_V1_STR: str = "/api/v1"
@@ -21,21 +27,15 @@ class Settings(BaseSettings):
2127
# BACKEND_CORS_ORIGINS is a JSON-formatted list of origins
2228
# e.g: '["http://localhost", "http://localhost:4200", "http://localhost:3000", \
2329
# "http://localhost:8080", "http://local.dockertoolbox.tiangolo.com"]'
24-
BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = []
25-
26-
@field_validator("BACKEND_CORS_ORIGINS", mode="before")
27-
def assemble_cors_origins(cls, v: Union[str, List[str]]) -> Union[List[str], str]:
28-
if isinstance(v, str) and not v.startswith("["):
29-
return [i.strip() for i in v.split(",")]
30-
elif isinstance(v, (list, str)):
31-
return v
32-
raise ValueError(v)
30+
BACKEND_CORS_ORIGINS: Annotated[
31+
list[AnyHttpUrl] | str, BeforeValidator(parse_cors)
32+
] = []
3333

3434
PROJECT_NAME: str
35-
SENTRY_DSN: Optional[HttpUrl] = None
35+
SENTRY_DSN: HttpUrl | None = None
3636

3737
@field_validator("SENTRY_DSN", mode="before")
38-
def sentry_dsn_can_be_blank(cls, v: str) -> Optional[str]:
38+
def sentry_dsn_can_be_blank(cls, v: str) -> str | None:
3939
if isinstance(v, str) and len(v) == 0:
4040
return None
4141
return v
@@ -49,16 +49,16 @@ def sentry_dsn_can_be_blank(cls, v: str) -> Optional[str]:
4949
MONGO_DATABASE_URI: str
5050

5151
SMTP_TLS: bool = True
52-
SMTP_PORT: Optional[int] = None
53-
SMTP_HOST: Optional[str] = None
54-
SMTP_USER: Optional[str] = None
55-
SMTP_PASSWORD: Optional[str] = None
56-
EMAILS_FROM_EMAIL: Optional[EmailStr] = None
57-
EMAILS_FROM_NAME: Optional[str] = None
58-
EMAILS_TO_EMAIL: Optional[EmailStr] = None
52+
SMTP_PORT: int = 587
53+
SMTP_HOST: str | None = None
54+
SMTP_USER: str | None = None
55+
SMTP_PASSWORD: str | None = None
56+
EMAILS_FROM_EMAIL: EmailStr | None = None
57+
EMAILS_FROM_NAME: str | None = None
58+
EMAILS_TO_EMAIL: EmailStr | None = None
5959

6060
@field_validator("EMAILS_FROM_NAME")
61-
def get_project_name(cls, v: Optional[str], info: ValidationInfo) -> str:
61+
def get_project_name(cls, v: str | None, info: ValidationInfo) -> str:
6262
if not v:
6363
return info.data["PROJECT_NAME"]
6464
return v

{{cookiecutter.project_slug}}/backend/app/app/core/security.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from datetime import datetime, timedelta
2-
from typing import Any, Union, Optional
2+
from typing import Any, Union
33

44
from jose import jwt
55
from passlib.context import CryptContext
@@ -64,7 +64,7 @@ def create_magic_tokens(*, subject: Union[str, Any], expires_delta: timedelta =
6464
return magic_tokens
6565

6666

67-
def create_new_totp(*, label: str, uri: Optional[str] = None) -> NewTOTP:
67+
def create_new_totp(*, label: str, uri: str | None = None) -> NewTOTP:
6868
if not uri:
6969
totp = totp_factory.new()
7070
else:

{{cookiecutter.project_slug}}/backend/app/app/crud/base.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Dict, Generic, Optional, Type, TypeVar, Union
1+
from typing import Any, Dict, Generic, Type, TypeVar, Union
22

33
from fastapi.encoders import jsonable_encoder
44
from pydantic import BaseModel
@@ -27,20 +27,20 @@ def __init__(self, model: Type[ModelType]):
2727
self.model = model
2828
self.engine: AIOEngine = get_engine()
2929

30-
async def get(self, db: AgnosticDatabase, id: Any) -> Optional[ModelType]:
30+
async def get(self, db: AgnosticDatabase, id: Any) -> ModelType | None:
3131
return await self.engine.find_one(self.model, self.model.id == id)
3232

33-
async def get_multi(self, db: AgnosticDatabase, *, page: int = 0, page_break: bool = False) -> list[ModelType]:
34-
offset = {"skip": page * settings.MULTI_MAX, "limit": settings.MULTI_MAX} if page_break else {}
33+
async def get_multi(self, db: AgnosticDatabase, *, page: int = 0, page_break: bool = False) -> list[ModelType]: # noqa
34+
offset = {"skip": page * settings.MULTI_MAX, "limit": settings.MULTI_MAX} if page_break else {} # noqa
3535
return await self.engine.find(self.model, **offset)
3636

37-
async def create(self, db: AgnosticDatabase, *, obj_in: CreateSchemaType) -> ModelType:
37+
async def create(self, db: AgnosticDatabase, *, obj_in: CreateSchemaType) -> ModelType: # noqa
3838
obj_in_data = jsonable_encoder(obj_in)
3939
db_obj = self.model(**obj_in_data) # type: ignore
4040
return await self.engine.save(db_obj)
4141

4242
async def update(
43-
self, db: AgnosticDatabase, *, db_obj: ModelType, obj_in: Union[UpdateSchemaType, Dict[str, Any]]
43+
self, db: AgnosticDatabase, *, db_obj: ModelType, obj_in: Union[UpdateSchemaType, Dict[str, Any]] # noqa
4444
) -> ModelType:
4545
obj_data = jsonable_encoder(db_obj)
4646
if isinstance(obj_in, dict):

{{cookiecutter.project_slug}}/backend/app/app/crud/crud_user.py

+12-12
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Dict, Optional, Union
1+
from typing import Any, Dict, Union
22

33
from motor.core import AgnosticDatabase
44

@@ -11,22 +11,22 @@
1111

1212
# ODM, Schema, Schema
1313
class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]):
14-
async def get_by_email(self, db: AgnosticDatabase, *, email: str) -> Optional[User]:
14+
async def get_by_email(self, db: AgnosticDatabase, *, email: str) -> User | None: # noqa
1515
return await self.engine.find_one(User, User.email == email)
1616

17-
async def create(self, db: AgnosticDatabase, *, obj_in: UserCreate) -> User:
17+
async def create(self, db: AgnosticDatabase, *, obj_in: UserCreate) -> User: # noqa
1818
# TODO: Figure out what happens when you have a unique key like 'email'
1919
user = {
2020
**obj_in.model_dump(),
2121
"email": obj_in.email,
22-
"hashed_password": get_password_hash(obj_in.password) if obj_in.password is not None else None,
22+
"hashed_password": get_password_hash(obj_in.password) if obj_in.password is not None else None, # noqa
2323
"full_name": obj_in.full_name,
2424
"is_superuser": obj_in.is_superuser,
2525
}
2626

2727
return await self.engine.save(User(**user))
2828

29-
async def update(self, db: AgnosticDatabase, *, db_obj: User, obj_in: Union[UserUpdate, Dict[str, Any]]) -> User:
29+
async def update(self, db: AgnosticDatabase, *, db_obj: User, obj_in: Union[UserUpdate, Dict[str, Any]]) -> User: # noqa
3030
if isinstance(obj_in, dict):
3131
update_data = obj_in
3232
else:
@@ -39,39 +39,39 @@ async def update(self, db: AgnosticDatabase, *, db_obj: User, obj_in: Union[User
3939
update_data["email_validated"] = False
4040
return await super().update(db, db_obj=db_obj, obj_in=update_data)
4141

42-
async def authenticate(self, db: AgnosticDatabase, *, email: str, password: str) -> Optional[User]:
42+
async def authenticate(self, db: AgnosticDatabase, *, email: str, password: str) -> User | None: # noqa
4343
user = await self.get_by_email(db, email=email)
4444
if not user:
4545
return None
46-
if not verify_password(plain_password=password, hashed_password=user.hashed_password):
46+
if not verify_password(plain_password=password, hashed_password=user.hashed_password): # noqa
4747
return None
4848
return user
4949

50-
async def validate_email(self, db: AgnosticDatabase, *, db_obj: User) -> User:
50+
async def validate_email(self, db: AgnosticDatabase, *, db_obj: User) -> User: # noqa
5151
obj_in = UserUpdate(**UserInDB.model_validate(db_obj).model_dump())
5252
obj_in.email_validated = True
5353
return await self.update(db=db, db_obj=db_obj, obj_in=obj_in)
5454

55-
async def activate_totp(self, db: AgnosticDatabase, *, db_obj: User, totp_in: NewTOTP) -> User:
55+
async def activate_totp(self, db: AgnosticDatabase, *, db_obj: User, totp_in: NewTOTP) -> User: # noqa
5656
obj_in = UserUpdate(**UserInDB.model_validate(db_obj).model_dump())
5757
obj_in = obj_in.model_dump(exclude_unset=True)
5858
obj_in["totp_secret"] = totp_in.secret
5959
return await self.update(db=db, db_obj=db_obj, obj_in=obj_in)
6060

61-
async def deactivate_totp(self, db: AgnosticDatabase, *, db_obj: User) -> User:
61+
async def deactivate_totp(self, db: AgnosticDatabase, *, db_obj: User) -> User: # noqa
6262
obj_in = UserUpdate(**UserInDB.model_validate(db_obj).model_dump())
6363
obj_in = obj_in.model_dump(exclude_unset=True)
6464
obj_in["totp_secret"] = None
6565
obj_in["totp_counter"] = None
6666
return await self.update(db=db, db_obj=db_obj, obj_in=obj_in)
6767

68-
async def update_totp_counter(self, db: AgnosticDatabase, *, db_obj: User, new_counter: int) -> User:
68+
async def update_totp_counter(self, db: AgnosticDatabase, *, db_obj: User, new_counter: int) -> User: # noqa
6969
obj_in = UserUpdate(**UserInDB.model_validate(db_obj).model_dump())
7070
obj_in = obj_in.model_dump(exclude_unset=True)
7171
obj_in["totp_counter"] = new_counter
7272
return await self.update(db=db, db_obj=db_obj, obj_in=obj_in)
7373

74-
async def toggle_user_state(self, db: AgnosticDatabase, *, obj_in: Union[UserUpdate, Dict[str, Any]]) -> User:
74+
async def toggle_user_state(self, db: AgnosticDatabase, *, obj_in: Union[UserUpdate, Dict[str, Any]]) -> User: # noqa
7575
db_obj = await self.get_by_email(db, email=obj_in.email)
7676
if not db_obj:
7777
return None

{{cookiecutter.project_slug}}/backend/app/app/main.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ async def app_init(app: FastAPI):
2323
app.add_middleware(
2424
CORSMiddleware,
2525
# Trailing slash causes CORS failures from these supported domains
26-
allow_origins=[str(origin).rstrip("/") for origin in settings.BACKEND_CORS_ORIGINS],
26+
allow_origins=[str(origin).strip("/") for origin in settings.BACKEND_CORS_ORIGINS], # noqa
2727
allow_credentials=True,
2828
allow_methods=["*"],
2929
allow_headers=["*"],
3030
)
31+

{{cookiecutter.project_slug}}/backend/app/app/models/user.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from __future__ import annotations
2-
from typing import TYPE_CHECKING, Any, Optional
2+
from typing import TYPE_CHECKING, Any
33
from datetime import datetime
44
from pydantic import EmailStr
55
from odmantic import ObjectId, Field
@@ -21,7 +21,7 @@ class User(Base):
2121
email: EmailStr
2222
hashed_password: Any = Field(default=None)
2323
totp_secret: Any = Field(default=None)
24-
totp_counter: Optional[int] = Field(default=None)
24+
totp_counter: int | None = Field(default=None)
2525
email_validated: bool = Field(default=False)
2626
is_active: bool = Field(default=False)
2727
is_superuser: bool = Field(default=False)

{{cookiecutter.project_slug}}/backend/app/app/schemas/base_schema.py

+6-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from __future__ import annotations
22
from pydantic import ConfigDict, BaseModel, Field
3-
from typing import Optional
43
from uuid import UUID
54
from datetime import date, datetime
65
import json
@@ -11,7 +10,7 @@
1110
class BaseSchema(BaseModel):
1211
@property
1312
def as_db_dict(self):
14-
to_db = self.model_dump(exclude_defaults=True, exclude_none=True, exclude={"identifier", "id"})
13+
to_db = self.model_dump(exclude_defaults=True, exclude_none=True, exclude={"identifier", "id"}) # noqa
1514
for key in ["id", "identifier"]:
1615
if key in self.model_dump().keys():
1716
to_db[key] = self.model_dump()[key].hex
@@ -21,15 +20,15 @@ def as_db_dict(self):
2120
class MetadataBaseSchema(BaseSchema):
2221
# Receive via API
2322
# https://www.dublincore.org/specifications/dublin-core/dcmi-terms/#section-3
24-
title: Optional[str] = Field(None, description="A human-readable title given to the resource.")
25-
description: Optional[str] = Field(
23+
title: str | None = Field(None, description="A human-readable title given to the resource.") # noqa
24+
description: str | None = Field(
2625
None,
2726
description="A short description of the resource.",
2827
)
29-
isActive: Optional[bool] = Field(default=True, description="Whether the resource is still actively maintained.")
30-
isPrivate: Optional[bool] = Field(
28+
isActive: bool | None = Field(default=True, description="Whether the resource is still actively maintained.") # noqa
29+
isPrivate: bool | None = Field(
3130
default=True,
32-
description="Whether the resource is private to team members with appropriate authorisation.",
31+
description="Whether the resource is private to team members with appropriate authorisation.", # noqa
3332
)
3433

3534

{{cookiecutter.project_slug}}/backend/app/app/schemas/token.py

+7-8
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
from typing import Optional
21
from pydantic import BaseModel, ConfigDict, SecretStr
32
from odmantic import Model, ObjectId
43

54

65
class RefreshTokenBase(BaseModel):
76
token: SecretStr
8-
authenticates: Optional[Model] = None
7+
authenticates: Model | None = None
98

109

1110
class RefreshTokenCreate(RefreshTokenBase):
@@ -22,19 +21,19 @@ class RefreshToken(RefreshTokenUpdate):
2221

2322
class Token(BaseModel):
2423
access_token: SecretStr
25-
refresh_token: Optional[SecretStr] = None
24+
refresh_token: SecretStr | None = None
2625
token_type: str
2726

2827

2928
class TokenPayload(BaseModel):
30-
sub: Optional[ObjectId] = None
31-
refresh: Optional[bool] = False
32-
totp: Optional[bool] = False
29+
sub: ObjectId | None = None
30+
refresh: bool | None = False
31+
totp: bool | None = False
3332

3433

3534
class MagicTokenPayload(BaseModel):
36-
sub: Optional[ObjectId] = None
37-
fingerprint: Optional[ObjectId] = None
35+
sub: ObjectId | None = None
36+
fingerprint: ObjectId | None = None
3837

3938

4039
class WebToken(BaseModel):
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
from typing import Optional
21
from pydantic import BaseModel, SecretStr
32

43

54
class NewTOTP(BaseModel):
6-
secret: Optional[SecretStr] = None
5+
secret: SecretStr | None = None
76
key: str
87
uri: str
98

109

1110
class EnableTOTP(BaseModel):
1211
claim: str
1312
uri: str
14-
password: Optional[SecretStr] = None
13+
password: SecretStr | None = None

{{cookiecutter.project_slug}}/backend/app/app/schemas/user.py

+11-12
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from typing import Optional
21
from typing_extensions import Annotated
32
from pydantic import BaseModel, ConfigDict, Field, EmailStr, StringConstraints, field_validator, SecretStr
43
from odmantic import ObjectId
@@ -11,27 +10,27 @@ class UserLogin(BaseModel):
1110

1211
# Shared properties
1312
class UserBase(BaseModel):
14-
email: Optional[EmailStr] = None
15-
email_validated: Optional[bool] = False
16-
is_active: Optional[bool] = True
17-
is_superuser: Optional[bool] = False
13+
email: EmailStr | None = None
14+
email_validated: bool | None = False
15+
is_active: bool | None = True
16+
is_superuser: bool | None = False
1817
full_name: str = ""
1918

2019

2120
# Properties to receive via API on creation
2221
class UserCreate(UserBase):
2322
email: EmailStr
24-
password: Optional[Annotated[str, StringConstraints(min_length=8, max_length=64)]] = None
23+
password: Annotated[str | None, StringConstraints(min_length=8, max_length=64)] = None # noqa
2524

2625

2726
# Properties to receive via API on update
2827
class UserUpdate(UserBase):
29-
original: Optional[Annotated[str, StringConstraints(min_length=8, max_length=64)]] = None
30-
password: Optional[Annotated[str, StringConstraints(min_length=8, max_length=64)]] = None
28+
original: Annotated[str | None, StringConstraints(min_length=8, max_length=64)] = None # noqa
29+
password: Annotated[str | None, StringConstraints(min_length=8, max_length=64)] = None # noqa
3130

3231

3332
class UserInDBBase(UserBase):
34-
id: Optional[ObjectId] = None
33+
id: ObjectId | None = None
3534
model_config = ConfigDict(from_attributes=True)
3635

3736

@@ -56,6 +55,6 @@ def evaluate_totp_secret(cls, totp_secret):
5655

5756
# Additional properties stored in DB
5857
class UserInDB(UserInDBBase):
59-
hashed_password: Optional[SecretStr] = None
60-
totp_secret: Optional[SecretStr] = None
61-
totp_counter: Optional[int] = None
58+
hashed_password: SecretStr | None = None
59+
totp_secret: SecretStr | None = None
60+
totp_counter: int | None = None

0 commit comments

Comments
 (0)