diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..5a7abc0e --- /dev/null +++ b/mypy.ini @@ -0,0 +1,5 @@ +[mypy] +ignore_missing_imports = True +strict_optional = True +warn_redundant_casts = True +warn_unused_ignores = True diff --git a/src/app/api/dependencies.py b/src/app/api/dependencies.py index 29e8b48b..915229ac 100644 --- a/src/app/api/dependencies.py +++ b/src/app/api/dependencies.py @@ -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: diff --git a/src/app/api/paginated.py b/src/app/api/paginated.py index 803f46b2..10614121 100644 --- a/src/app/api/paginated.py +++ b/src/app/api/paginated.py @@ -1,4 +1,4 @@ -from typing import TypeVar, Generic, List +from typing import TypeVar, Generic, List, Dict, Any from pydantic import BaseModel @@ -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. @@ -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 ---- diff --git a/src/app/core/db/models.py b/src/app/core/db/models.py index 8c6c8cf5..642ae548 100644 --- a/src/app/core/db/models.py +++ b/src/app/core/db/models.py @@ -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)")) diff --git a/src/app/core/exceptions/cache_exceptions.py b/src/app/core/exceptions/cache_exceptions.py index a2b5864b..3a75d9c7 100644 --- a/src/app/core/exceptions/cache_exceptions.py +++ b/src/app/core/exceptions/cache_exceptions.py @@ -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) diff --git a/src/app/core/exceptions/http_exceptions.py b/src/app/core/exceptions/http_exceptions.py index 5c937429..65506a4c 100644 --- a/src/app/core/exceptions/http_exceptions.py +++ b/src/app/core/exceptions/http_exceptions.py @@ -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) diff --git a/src/app/core/schemas.py b/src/app/core/schemas.py index d44e3b04..fd4d1036 100644 --- a/src/app/core/schemas.py +++ b/src/app/core/schemas.py @@ -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 -------------- diff --git a/src/app/core/utils/cache.py b/src/app/core/utils/cache.py index ef3f4653..dbbcdfe1 100644 --- a/src/app/core/utils/cache.py +++ b/src/app/core/utils/cache.py @@ -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 @@ -7,12 +7,12 @@ 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. @@ -20,8 +20,8 @@ def _infer_resource_id(kwargs: Dict[str, Any], resource_id_type: Union[type, str ---------- 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 ------- @@ -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(): @@ -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: @@ -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: @@ -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. @@ -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: diff --git a/src/app/crud/crud_base.py b/src/app/crud/crud_base.py index 197ab300..fab8fb33 100644 --- a/src/app/crud/crud_base.py +++ b/src/app/crud/crud_base.py @@ -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) diff --git a/src/app/schemas/post.py b/src/app/schemas/post.py index 0899f2d5..6b3e5379 100644 --- a/src/app/schemas/post.py +++ b/src/app/schemas/post.py @@ -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[ diff --git a/src/app/schemas/user.py b/src/app/schemas/user.py index 026c87b5..60cb6df3 100644 --- a/src/app/schemas/user.py +++ b/src/app/schemas/user.py @@ -32,7 +32,6 @@ class User(TimestampSchema, UserBase, UUIDSchema, PersistentDeletion): class UserRead(BaseModel): id: int - tier_id: int name: Annotated[ str,