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
10 changes: 2 additions & 8 deletions .claude/commands/commit.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
Your goal is to write a commit message and open a pull request.
Your goal is to write a commit message.

1) Write a clear and concise commit message that follows conventional commit format (if applicable). It should explain what was changed and why.

2) Create a pull request with:
* A descriptive title
* A summary of changes
* Any relevant context or background
* Linked issues (if any)
* Checklist of tasks done (tests, docs, etc.)
2) Take a look to the branch name to understand context of the issue/feature
10 changes: 10 additions & 0 deletions .claude/commands/commit_and_pr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Your goal is to write a commit message and open a pull request.

1) Write a clear and concise commit message that follows conventional commit format (if applicable). It should explain what was changed and why.

2) Create a pull request with:
* A descriptive title
* A summary of changes
* Any relevant context or background
* Linked issues (if any)
* Checklist of tasks done (tests, docs, etc.)
8 changes: 8 additions & 0 deletions .claude/commands/create_pr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Your goal is to analyze this branch and open a pull request.

1Create a pull request with:
* A descriptive title
* A summary of changes
* Any relevant context or background
* Linked issues (if any)
* Checklist of tasks done (tests, docs, etc.)
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ dependencies = [
"pyyaml>=6.0.2",
"ruff>=0.12.5",
"sqlalchemy>=2.0.41",
"ty>=0.0.1a16",
"uvicorn>=0.35.0",
]
5 changes: 2 additions & 3 deletions src/application/auth/tg.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from src.application.common.interactor import Interactor
from src.application.common.transaction import TransactionManager
from src.application.interfaces.auth import AuthService, InitDataDTO
from src.application.interfaces.auth import AuthService
from src.domain.user import (
CreateUserDTO,
UserRepository,
Expand Down Expand Up @@ -30,11 +30,10 @@ def __init__(
self.transaction_manager = transaction_manager
self.auth_service = auth_service


async def __call__(self, data: AuthTgInputDTO) -> AuthTgOutputDTO:
parsed_data = self.auth_service.validate_init_data(data.init_data)

user = await self.user_repository.get_user(parsed_data.user_id, by='id')
user = await self.user_repository.get_user(parsed_data.user_id, by="id")
if user is None:
await self.user_repository.create_user(
CreateUserDTO(
Expand Down
2 changes: 1 addition & 1 deletion src/application/interfaces/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class InitDataDTO:
is_premium: bool
start_param: str | None
photo_url: str
ui_language_code: str
ui_language_code: str | None


class AuthService(Protocol):
Expand Down
9 changes: 8 additions & 1 deletion src/domain/user/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
from .entity import User
from .repository import UserRepository, CreateUserDTO
from .repository import UserRepository, CreateUserDTO


__all__ = [
"User",
"CreateUserDTO",
"UserRepository",
]
5 changes: 4 additions & 1 deletion src/domain/user/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class CreateUserDTO(TypedDict):
is_premium: bool
photo_url: str


class UpdateUserDTO(TypedDict):
username: str | None
first_name: str
Expand All @@ -28,7 +29,9 @@ async def get_user(self, identifier: str) -> User: ...
async def get_user(self, identifier: int, by: str = Literal["id"]) -> User: ...

@overload
async def get_user(self, identifier: str, by: str = Literal["username"]) -> User: ...
async def get_user(
self, identifier: str, by: str = Literal["username"]
) -> User: ...

@abstractmethod
async def get_user(
Expand Down
13 changes: 9 additions & 4 deletions src/infrastructure/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ def __init__(self, config: Config) -> None:
def validate_init_data(self, init_data: str) -> InitDataDTO:
# todo - cover with tests
try:
parsed_data = safe_parse_webapp_init_data(self.config.telegram.bot_token, init_data)
except ValueError as e:
parsed_data = safe_parse_webapp_init_data(
self.config.telegram.bot_token, init_data
)
except ValueError:
error_msg = f"Invalid init data '{init_data}'"
raise ValidationError(message=error_msg)

Expand All @@ -32,14 +34,17 @@ def validate_init_data(self, init_data: str) -> InitDataDTO:
is_premium=parsed_data.user.is_premium or False,
start_param=parsed_data.start_param,
photo_url=parsed_data.user.photo_url,
ui_language_code=parsed_data.user.language_code,
ui_language_code=parsed_data.user.language_code
if parsed_data.user.language_code
else None,
)

def create_access_token(self, user_id: int) -> str:
# todo - cover with tests
to_encode = {
"sub": str(user_id),
"exp": datetime.now(UTC) + timedelta(minutes=self.config.auth.access_token_expire_minutes),
"exp": datetime.now(UTC)
+ timedelta(minutes=self.config.auth.access_token_expire_minutes),
}
encoded_jwt = encode(
to_encode,
Expand Down
1 change: 1 addition & 0 deletions src/infrastructure/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class AuthConfig(BaseModel):
algorithm: str
access_token_expire_minutes: int


class TelegramConfig(BaseModel):
bot_token: str

Expand Down
3 changes: 1 addition & 2 deletions src/infrastructure/db/models/user.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from typing import Optional

from sqlalchemy import Column
from sqlalchemy.orm import Mapped, mapped_column

from src.domain.user.entity import User
Expand All @@ -19,7 +18,7 @@ class UserModel(BaseORMModel):
username: Mapped[Optional[Username]] = mapped_column(
UsernameType, nullable=True, unique=False
)
bio: Mapped[Optional[Bio]] = Column(BioType, nullable=True)
bio: Mapped[Bio | None] = mapped_column(BioType, nullable=True)

def to_domain(self) -> User:
return User(
Expand Down
2 changes: 1 addition & 1 deletion src/infrastructure/db/repos/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@


__all__ = [
'UserRepositoryImpl',
"UserRepositoryImpl",
]
32 changes: 20 additions & 12 deletions src/infrastructure/db/repos/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,32 @@ async def get_user(
return user_model.to_domain() if user_model else None

async def create_user(self, user: CreateUserDTO) -> User:
stmt = insert(UserModel).values(
id=user["id"],
username=user["username"],
first_name=user["first_name"],
last_name=user["last_name"],
).returning(UserModel)
stmt = (
insert(UserModel)
.values(
id=user["id"],
username=user["username"],
first_name=user["first_name"],
last_name=user["last_name"],
)
.returning(UserModel)
)

result = await self._session.execute(stmt)
orm_model = result.scalar_one()
return orm_model.to_domain()

async def update_user(self, user_id: int, **fields: Unpack[UpdateUserDTO]) -> User:
stmt = update(UserModel).values(
id=user_id,
username=fields["username"],
first_name=fields["first_name"],
last_name=fields["last_name"],
).returning(UserModel)
stmt = (
update(UserModel)
.values(
id=user_id,
username=fields["username"],
first_name=fields["first_name"],
last_name=fields["last_name"],
)
.returning(UserModel)
)
result = await self._session.execute(stmt)
orm_model = result.scalar_one()
return orm_model.to_domain()
2 changes: 1 addition & 1 deletion src/presentation/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def create_app() -> Litestar:
exception_handlers={
Exception: custom_exception_handler,
ValidationError: validation_error_handler,
}
},
)

container = make_async_container(
Expand Down
17 changes: 13 additions & 4 deletions src/presentation/api/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,20 @@
logger = logging.getLogger(__name__)


def custom_exception_handler(_: Request[Any, Any, Any], exc: Exception) -> Response[Any]:
def custom_exception_handler(
_: Request[Any, Any, Any], exc: Exception
) -> Response[Any]:
logger.exception(exc)
return Response({"detail": "Internal Server Error", "status_code": 500}, status_code=500)
return Response(
{"detail": "Internal Server Error", "status_code": 500}, status_code=500
)


def validation_error_handler(_: Request[Any, Any, Any], exc: ValidationError) -> Response[Any]:
def validation_error_handler(
_: Request[Any, Any, Any], exc: ValidationError
) -> Response[Any]:
logger.exception(exc)
return Response({"detail": exc.message, "status_code": exc.status_code}, status_code=exc.status_code)
return Response(
{"detail": exc.message, "status_code": exc.status_code},
status_code=exc.status_code,
)
4 changes: 2 additions & 2 deletions src/presentation/api/health/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from .router import health_router

__all__ = [
'health_router',
]
"health_router",
]
27 changes: 27 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.