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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
python-version: "3.13"

- name: Install uv
uses: astral-sh/setup-uv@v6
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@ natural.

## Technology Stack

- **Python**: `3.12`
- **Python**: `3.13`
- **Core**: `alembic`, `alembic-postgresql-enum`, `bcrypt`, `dishka`, `fastapi-error-map`, `fastapi`, `orjson`,
`psycopg3[binary]`, `pydantic[email]`, `pyjwt[crypto]`, `rtoml`, `sqlalchemy[mypy]`, `uuid6`, `uvicorn`, `uvloop`
- **Development**: `mypy`, `pre-commit`, `ruff`, `slotscheck`
Expand Down Expand Up @@ -596,7 +596,7 @@ export APP_ENV=local
3. Check it and generate `.env`

```shell
# Probably you'll need Python 3.12 installed on your system to run these commands.
# Probably you'll need Python 3.13 installed on your system to run these commands.
# The next code section provides commands for its fast installation.
make env # should print APP_ENV=local
make dotenv # should tell you where .env.local was generated
Expand All @@ -609,7 +609,7 @@ make dotenv # should tell you where .env.local was generated
# sudo apt install pipx
# pipx ensurepath
# pipx install uv
# uv python install 3.12
# uv python install 3.13
uv v
source .venv/bin/activate
# .venv\Scripts\activate # Windows
Expand Down
4 changes: 2 additions & 2 deletions config/local/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS builder
WORKDIR /app
ENV UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy
Expand All @@ -8,7 +8,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
uv pip install --system --target /app/dependencies .
COPY . ./

FROM python:3.12-slim-bookworm AS final
FROM python:3.13-slim-bookworm AS final
ARG APP_UID=10001
ARG APP_GID=10001
RUN groupadd -g ${APP_GID} appgroup && \
Expand Down
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ license = "MIT"
authors = [
{ name = "Ivan Borovets", email = "[email protected]" },
]
requires-python = "==3.12.*"
requires-python = "==3.13.*"
dependencies = [
"alembic==1.16.4",
"alembic-postgresql-enum==1.8.0",
Expand Down Expand Up @@ -174,7 +174,6 @@ split-on-trailing-comma = true
"S107", # hardcoded-password-default
]
#
"src/app/domain/value_objects/base.py" = ["B024", ] # abstract-base-class-without-abstract-method
"src/app/infrastructure/adapters/password_hasher_bcrypt.py" = ["E501", ] # line-too-long
"src/app/infrastructure/auth/session/constants.py" = ["S105", ] # hardcoded-password-string
"src/app/presentation/http/auth/constants.py" = ["S105", ] # hardcoded-password-string
Expand Down
2 changes: 1 addition & 1 deletion src/app/application/commands/activate_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from app.domain.enums.user_role import UserRole
from app.domain.exceptions.user import UserNotFoundByUsernameError
from app.domain.services.user import UserService
from app.domain.value_objects.username.username import Username
from app.domain.value_objects.username import Username

log = logging.getLogger(__name__)

Expand Down
4 changes: 2 additions & 2 deletions src/app/application/commands/change_password.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
from app.domain.entities.user import User
from app.domain.exceptions.user import UserNotFoundByUsernameError
from app.domain.services.user import UserService
from app.domain.value_objects.raw_password.raw_password import RawPassword
from app.domain.value_objects.username.username import Username
from app.domain.value_objects.raw_password import RawPassword
from app.domain.value_objects.username import Username

log = logging.getLogger(__name__)

Expand Down
4 changes: 2 additions & 2 deletions src/app/application/commands/create_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
from app.domain.enums.user_role import UserRole
from app.domain.exceptions.user import UsernameAlreadyExistsError
from app.domain.services.user import UserService
from app.domain.value_objects.raw_password.raw_password import RawPassword
from app.domain.value_objects.username.username import Username
from app.domain.value_objects.raw_password import RawPassword
from app.domain.value_objects.username import Username

log = logging.getLogger(__name__)

Expand Down
2 changes: 1 addition & 1 deletion src/app/application/commands/deactivate_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from app.domain.enums.user_role import UserRole
from app.domain.exceptions.user import UserNotFoundByUsernameError
from app.domain.services.user import UserService
from app.domain.value_objects.username.username import Username
from app.domain.value_objects.username import Username

log = logging.getLogger(__name__)

Expand Down
2 changes: 1 addition & 1 deletion src/app/application/commands/grant_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from app.domain.enums.user_role import UserRole
from app.domain.exceptions.user import UserNotFoundByUsernameError
from app.domain.services.user import UserService
from app.domain.value_objects.username.username import Username
from app.domain.value_objects.username import Username

log = logging.getLogger(__name__)

Expand Down
2 changes: 1 addition & 1 deletion src/app/application/commands/revoke_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from app.domain.enums.user_role import UserRole
from app.domain.exceptions.user import UserNotFoundByUsernameError
from app.domain.services.user import UserService
from app.domain.value_objects.username.username import Username
from app.domain.value_objects.username import Username

log = logging.getLogger(__name__)

Expand Down
4 changes: 1 addition & 3 deletions src/app/application/common/ports/access_revoker.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,4 @@
class AccessRevoker(Protocol):
@abstractmethod
async def remove_all_user_access(self, user_id: UserId) -> None:
"""
:raises DataMapperError:
"""
""":raises DataMapperError:"""
4 changes: 2 additions & 2 deletions src/app/application/common/ports/flusher.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ class Flusher(Protocol):
@abstractmethod
async def flush(self) -> None:
"""
Flush pending changes to validate constraints or trigger side effects.

:raises DataMapperError:
:raises UsernameAlreadyExists:

Flush pending changes to validate constraints or trigger side effects.
"""
4 changes: 1 addition & 3 deletions src/app/application/common/ports/identity_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,4 @@
class IdentityProvider(Protocol):
@abstractmethod
async def get_current_user_id(self) -> UserId:
"""
:raises AuthenticationError:
"""
""":raises AuthenticationError:"""
4 changes: 2 additions & 2 deletions src/app/application/common/ports/transaction_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class TransactionManager(Protocol):
@abstractmethod
async def commit(self) -> None:
"""
Commit the successful outcome of a business transaction.

:raises DataMapperError:

Commit the successful outcome of a business transaction.
"""
14 changes: 4 additions & 10 deletions src/app/application/common/ports/user_command_gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,22 @@

from app.domain.entities.user import User
from app.domain.value_objects.user_id import UserId
from app.domain.value_objects.username.username import Username
from app.domain.value_objects.username import Username


class UserCommandGateway(Protocol):
@abstractmethod
def add(self, user: User) -> None:
"""
:raises DataMapperError:
"""
""":raises DataMapperError:"""

@abstractmethod
async def read_by_id(self, user_id: UserId) -> User | None:
"""
:raises DataMapperError:
"""
""":raises DataMapperError:"""

@abstractmethod
async def read_by_username(
self,
username: Username,
for_update: bool = False,
) -> User | None:
"""
:raises DataMapperError:
"""
""":raises DataMapperError:"""
4 changes: 1 addition & 3 deletions src/app/application/common/ports/user_query_gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,4 @@ async def read_all(
self,
user_read_all_params: UserListParams,
) -> list[UserQueryModel] | None:
"""
:raises ReaderError:
"""
""":raises ReaderError:"""
1 change: 1 addition & 0 deletions src/app/application/common/query_params/pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class Pagination:
offset: int

def __post_init__(self):
""":raises PaginationError:"""
if self.limit <= 0:
raise PaginationError(f"Limit must be greater than 0, got {self.limit}")
if self.offset < 0:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ def authorize[PC: PermissionContext](
*,
context: PC,
) -> None:
"""
:raises AuthorizationError:
"""
""":raises AuthorizationError:"""
if not permission.is_satisfied_by(context):
raise AuthorizationError(AUTHZ_NOT_AUTHORIZED)
22 changes: 20 additions & 2 deletions src/app/domain/entities/base.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from abc import ABC
from dataclasses import dataclass
from typing import Any, TypeVar

Expand All @@ -9,8 +8,10 @@


@dataclass(eq=False)
class Entity[T: ValueObject](ABC):
class Entity[T: ValueObject]:
"""
raises DomainError

Base class for domain entities, defined by a unique identity (`id`).
- `id`: Identity that remains constant throughout the entity's lifecycle.
- Entities are mutable, but are compared solely by their `id`.
Expand All @@ -20,8 +21,25 @@ class Entity[T: ValueObject](ABC):

id_: T

def __post_init__(self) -> None:
"""
:raises DomainError:

Hook for additional initialization and ensuring invariants.
Subclasses can override this method to implement custom logic, while
still calling `super().__post_init__()` to preserve base checks.
"""
self.__forbid_base_class_instantiation()

def __forbid_base_class_instantiation(self) -> None:
""":raises DomainError:"""
if type(self) is Entity:
raise DomainError("Base Entity cannot be instantiated directly.")

def __setattr__(self, name: str, value: Any) -> None:
"""
:raises DomainError:

Prevents modifying the `id` after it's set.
Other attributes can be changed as usual.
"""
Expand Down
2 changes: 1 addition & 1 deletion src/app/domain/entities/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from app.domain.enums.user_role import UserRole
from app.domain.value_objects.user_id import UserId
from app.domain.value_objects.user_password_hash import UserPasswordHash
from app.domain.value_objects.username.username import Username
from app.domain.value_objects.username import Username


@dataclass(eq=False, kw_only=True)
Expand Down
2 changes: 1 addition & 1 deletion src/app/domain/exceptions/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from app.domain.enums.user_role import UserRole
from app.domain.exceptions.base import DomainError
from app.domain.value_objects.username.username import Username
from app.domain.value_objects.username import Username


class UsernameAlreadyExistsError(DomainError):
Expand Down
2 changes: 1 addition & 1 deletion src/app/domain/ports/password_hasher.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from abc import abstractmethod
from typing import Protocol

from app.domain.value_objects.raw_password.raw_password import RawPassword
from app.domain.value_objects.raw_password import RawPassword


class PasswordHasher(Protocol):
Expand Down
12 changes: 4 additions & 8 deletions src/app/domain/services/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
)
from app.domain.ports.password_hasher import PasswordHasher
from app.domain.ports.user_id_generator import UserIdGenerator
from app.domain.value_objects.raw_password.raw_password import RawPassword
from app.domain.value_objects.raw_password import RawPassword
from app.domain.value_objects.user_id import UserId
from app.domain.value_objects.user_password_hash import UserPasswordHash
from app.domain.value_objects.username.username import Username
from app.domain.value_objects.username import Username


class UserService:
Expand Down Expand Up @@ -57,17 +57,13 @@ def change_password(self, user: User, raw_password: RawPassword) -> None:
user.password_hash = hashed_password

def toggle_user_activation(self, user: User, *, is_active: bool) -> None:
"""
:raises ActivationChangeNotPermittedError:
"""
""":raises ActivationChangeNotPermittedError:"""
if not user.role.is_changeable:
raise ActivationChangeNotPermittedError(user.username, user.role)
user.is_active = is_active

def toggle_user_admin_role(self, user: User, *, is_admin: bool) -> None:
"""
:raises RoleChangeNotPermittedError:
"""
""":raises RoleChangeNotPermittedError:"""
if not user.role.is_changeable:
raise RoleChangeNotPermittedError(user.username, user.role)
user.role = UserRole.ADMIN if is_admin else UserRole.USER
Loading