Skip to content
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
5 changes: 5 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[mypy]
ignore_missing_imports = True
strict_optional = True
warn_redundant_casts = True
warn_unused_ignores = True
2 changes: 1 addition & 1 deletion src/app/api/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ async def get_optional_user(
if token_data is None:
return None

return await get_current_user(token_value, is_deleted=False, db=db)
return await get_current_user(token_value, db=db)

except HTTPException as http_exc:
if http_exc.status_code != 401:
Expand Down
8 changes: 4 additions & 4 deletions src/app/api/paginated.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import TypeVar, Generic, List
from typing import TypeVar, Generic, List, Dict, Any

from pydantic import BaseModel

Expand All @@ -19,7 +19,7 @@ def paginated_response(
crud_data: ListResponse[SchemaType],
page: int,
items_per_page: int
) -> PaginatedListResponse[SchemaType]:
) -> Dict[str, Any]:
"""
Create a paginated response based on the provided data and pagination parameters.

Expand All @@ -34,8 +34,8 @@ def paginated_response(

Returns
-------
PaginatedListResponse[SchemaType]
A structured paginated response containing the list of items, total count, pagination flags, and numbers.
Dict[str, Any]
A structured paginated response dict containing the list of items, total count, pagination flags, and numbers.

Note
----
Expand Down
4 changes: 3 additions & 1 deletion src/app/core/db/models.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import uuid as uuid_pkg
from datetime import datetime
from sqlalchemy import Column, DateTime, Boolean, text
from sqlalchemy.dialects.postgresql import UUID

class UUIDMixin:
uuid: uuid_pkg.UUID = Column(uuid_pkg.UUID(as_uuid=True), primary_key=True, default=uuid_pkg.uuid4, server_default=text("gen_random_uuid()"))
uuid: uuid_pkg.UUID = Column(UUID, primary_key=True, default=uuid_pkg.uuid4, server_default=text("gen_random_uuid()"))


class TimestampMixin:
created_at: datetime = Column(DateTime, default=datetime.utcnow, server_default=text("current_timestamp(0)"))
Expand Down
6 changes: 6 additions & 0 deletions src/app/core/exceptions/cache_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,9 @@ class InvalidRequestError(Exception):
def __init__(self, message="Type of request not supported."):
self.message = message
super().__init__(self.message)


class MissingClientError(Exception):
def __init__(self, message="Client is None."):
self.message = message
super().__init__(self.message)
3 changes: 2 additions & 1 deletion src/app/core/exceptions/http_exceptions.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from http import HTTPStatus
from fastapi import HTTPException, status

class CustomException(HTTPException):
def __init__(self, status_code: int = status.HTTP_500_INTERNAL_SERVER_ERROR, detail: str | None = None):
if not detail:
detail = status_code.description
detail = HTTPStatus(status_code).description
super().__init__(status_code=status_code, detail=detail)


Expand Down
14 changes: 10 additions & 4 deletions src/app/core/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,29 @@ class TimestampSchema(BaseModel):
updated_at: datetime = Field(default=None)

@field_serializer("created_at")
def serialize_dt(self, created_at: datetime | None, _info):
return created_at.isoformat()
def serialize_dt(self, created_at: datetime | None, _info) -> str | None:
if created_at is not None:
return created_at.isoformat()

return None

@field_serializer("updated_at")
def serialize_updated_at(self, updated_at: datetime | None, _info):
def serialize_updated_at(self, updated_at: datetime | None, _info) -> str | None:
if updated_at is not None:
return updated_at.isoformat()

return None

class PersistentDeletion(BaseModel):
deleted_at: datetime | None = Field(default=None)
is_deleted: bool = False

@field_serializer('deleted_at')
def serialize_dates(self, deleted_at: datetime | None, _info):
def serialize_dates(self, deleted_at: datetime | None, _info) -> str | None:
if deleted_at is not None:
return deleted_at.isoformat()

return None


# -------------- token --------------
Expand Down
28 changes: 17 additions & 11 deletions src/app/core/utils/cache.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Callable, Union, List, Dict, Any
from typing import Callable, Union, Tuple, List, Dict, Any
import functools
import json
import re
Expand All @@ -7,21 +7,21 @@
from fastapi.encoders import jsonable_encoder
from redis.asyncio import Redis, ConnectionPool

from app.core.exceptions.cache_exceptions import CacheIdentificationInferenceError, InvalidRequestError
from app.core.exceptions.cache_exceptions import CacheIdentificationInferenceError, InvalidRequestError, MissingClientError

pool: ConnectionPool | None = None
client: Redis | None = None

def _infer_resource_id(kwargs: Dict[str, Any], resource_id_type: Union[type, str]) -> Union[None, int, str]:
def _infer_resource_id(kwargs: Dict[str, Any], resource_id_type: Union[type, Tuple[type, ...]]) -> Union[None, int, str]:
"""
Infer the resource ID from a dictionary of keyword arguments.

Parameters
----------
kwargs: Dict[str, Any]
A dictionary of keyword arguments.
resource_id_type: Union[type, str]
The expected type of the resource ID, which can be an integer (int) or a string (str).
resource_id_type: Union[type, Tuple[type, ...]]
The expected type of the resource ID, which can be integer (int) or a string (str).

Returns
-------
Expand All @@ -30,8 +30,8 @@ def _infer_resource_id(kwargs: Dict[str, Any], resource_id_type: Union[type, str

Note
----
- When `resource_id_type` is 'int', the function looks for an argument with the key 'id'.
- When `resource_id_type` is 'str', it attempts to infer the resource ID as a string.
- When `resource_id_type` is `int`, the function looks for an argument with the key 'id'.
- When `resource_id_type` is `str`, it attempts to infer the resource ID as a string.
"""
resource_id = None
for arg_name, arg_value in kwargs.items():
Expand Down Expand Up @@ -177,7 +177,10 @@ async def _delete_keys_by_pattern(pattern: str):
- Be cautious with patterns that could match a large number of keys, as deleting
many keys simultaneously may impact the performance of the Redis server.
"""
cursor = "0"
if client is None:
raise MissingClientError

cursor = -1
while cursor != 0:
cursor, keys = await client.scan(cursor, match=pattern, count=100)
if keys:
Expand All @@ -188,7 +191,7 @@ def cache(
key_prefix: str,
resource_id_name: Any = None,
expiration: int = 3600,
resource_id_type: Union[type, List[type]] = int,
resource_id_type: Union[type, Tuple[type, ...]] = int,
to_invalidate_extra: Dict[str, Any] | None = None,
pattern_to_invalidate_extra: List[str] | None = None
) -> Callable:
Expand All @@ -207,8 +210,8 @@ def cache(
otherwise, the resource ID is inferred from the function's arguments.
expiration: int, optional
The expiration time for the cached data in seconds. Defaults to 3600 seconds (1 hour).
resource_id_type: Union[type, List[type]], optional
The expected type of the resource ID. This can be a single type (e.g., int) or a list of types (e.g., [int, str]).
resource_id_type: Union[type, Tuple[type, ...]], default int
The expected type of the resource ID. This can be a single type (e.g., int) or a tuple of types (e.g., (int, str)).
Defaults to int. This is used only if resource_id_name is not provided.
to_invalidate_extra: Dict[str, Any] | None, optional
A dictionary where keys are cache key prefixes and values are templates for cache key suffixes.
Expand Down Expand Up @@ -286,6 +289,9 @@ async def update_item(request: Request, item_id: int, data: dict, user_id: int):
def wrapper(func: Callable) -> Callable:
@functools.wraps(func)
async def inner(request: Request, *args, **kwargs) -> Response:
if client is None:
raise MissingClientError

if resource_id_name:
resource_id = kwargs[resource_id_name]
else:
Expand Down
3 changes: 2 additions & 1 deletion src/app/crud/crud_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
_auto_detect_join_condition,
_add_column_with_prefix
)
from app.core.db.database import Base

ModelType = TypeVar("ModelType")
ModelType = TypeVar("ModelType", bound=Base)
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)
UpdateSchemaInternalType = TypeVar("UpdateSchemaInternalType", bound=BaseModel)
Expand Down
2 changes: 1 addition & 1 deletion src/app/schemas/post.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class PostCreateInternal(PostCreate):
created_by_user_id: int


class PostUpdate(PostBase):
class PostUpdate(BaseModel):
model_config = ConfigDict(extra='forbid')

title: Annotated[
Expand Down
1 change: 0 additions & 1 deletion src/app/schemas/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ class User(TimestampSchema, UserBase, UUIDSchema, PersistentDeletion):

class UserRead(BaseModel):
id: int
tier_id: int

name: Annotated[
str,
Expand Down