-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
11b5692
commit 9fb7b5d
Showing
25 changed files
with
257 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -161,3 +161,7 @@ cython_debug/ | |
|
||
# Visual Studio Code | ||
.vscode/ | ||
|
||
# Storage | ||
files/* | ||
!files/.gitkeep |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,3 +21,6 @@ pydantic==2.5.3 | |
|
||
# Dependecy Injector | ||
kink==0.7.0 | ||
|
||
# Content-Types | ||
python-magic~=0.4.27 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
from src.planner.shared.domain.value_objects.base import ValueObject | ||
from typing import Any, Self | ||
|
||
from uuid import uuid4 | ||
from src.planner.users.domain.storage import UserFileStorage | ||
from src.planner.users.domain.mime_guesser import MimeGuesser | ||
from kink import inject | ||
|
||
@inject | ||
class FileValueObject(ValueObject): | ||
|
||
BASE_TYPE = bytes | ||
SUBPATH = str | ||
|
||
def __init__(self, value: Any, filename: str, storage: UserFileStorage) -> None: | ||
""" | ||
Args: | ||
value (str): Filename | ||
content (bytes, optional): FileContent | ||
""" | ||
self._filename = filename | ||
self._storage = storage | ||
super().__init__(value) | ||
|
||
@property | ||
def filename(self)-> str: | ||
return self._filename | ||
|
||
@classmethod | ||
@inject | ||
def make(cls, file, mime_guesser: MimeGuesser) -> Self: | ||
filename = f"{uuid4()}{mime_guesser.extension(file)}" | ||
return cls(file, filename) | ||
|
||
async def push(self) -> None: | ||
await self._storage.push(self.value, self.SUBPATH + self.filename) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
30 changes: 30 additions & 0 deletions
30
src/planner/users/application/update_avatar/avatar_updater.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
from kink import inject | ||
|
||
from src.planner.shared.domain.users import UserId | ||
from src.planner.users.domain.repository import UserRepository | ||
from src.planner.users.domain.value_objects import UserAvatar | ||
from src.shared.domain.bus.event.event_bus import EventBus | ||
from src.planner.users.application.find.finder import UserFinder | ||
|
||
|
||
@inject(use_factory=True) | ||
class UserAvatarUpdater: | ||
def __init__(self, | ||
repository: UserRepository, | ||
event_bus: EventBus, | ||
user_finder: UserFinder | ||
): | ||
self._repository = repository | ||
self._event_bus = event_bus | ||
self._user_finder = user_finder | ||
|
||
async def __call__( | ||
self, | ||
id: UserId, | ||
current_user_id: UserId, | ||
avatar: UserAvatar, | ||
) -> None: | ||
user = await self._user_finder(id, current_user_id) | ||
await user.update_avatar(avatar) | ||
await self._repository.save(user) | ||
await self._event_bus.publish(*user.pull_domain_events()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
from dataclasses import dataclass | ||
|
||
from src.planner.shared.domain.bus.command import Command | ||
|
||
|
||
@dataclass(frozen=True) | ||
class UpdateUserAvatarCommand(Command): | ||
id: str | ||
avatar: bytes | ||
user_id: str |
26 changes: 26 additions & 0 deletions
26
src/planner/users/application/update_avatar/command_handler.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
from kink import inject | ||
|
||
from src.planner.shared.domain.users import UserId | ||
from src.planner.users.domain.value_objects import ( | ||
UserAvatar, | ||
UserLastName, | ||
UserName, | ||
UserPassword, | ||
UserPronoun, | ||
) | ||
|
||
from .command import UpdateUserAvatarCommand | ||
from .avatar_updater import UserAvatarUpdater | ||
|
||
|
||
@inject | ||
class UpdateUserAvatarCommandHandler: | ||
def __init__(self, use_case: UserAvatarUpdater) -> None: | ||
self.use_case = use_case | ||
|
||
async def __call__(self, command: UpdateUserAvatarCommand) -> None: | ||
await self.use_case( | ||
id=UserId(command.id), | ||
avatar=UserAvatar.make(command.avatar), | ||
current_user_id=UserId(command.user_id) | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
from typing import Protocol, runtime_checkable | ||
|
||
|
||
@runtime_checkable | ||
class MimeGuesser(Protocol): | ||
def extension(self, file: bytes) -> str: | ||
... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
from typing import Protocol, runtime_checkable | ||
|
||
|
||
@runtime_checkable | ||
class UserFileStorage(Protocol): | ||
async def push(self, file: bytes, path: str) -> None: | ||
... | ||
|
||
async def pull(self, path: str) -> None: | ||
... | ||
|
||
def url(self, path: str) -> str: | ||
... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
from src.planner.shared.domain.value_objects.file import FileValueObject | ||
|
||
|
||
class UserAvatar(FileValueObject): | ||
NAME = "avatar" | ||
SUBPATH = "users/avatars/" |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
from magic import from_buffer | ||
from mimetypes import guess_extension | ||
from src.planner.users.domain.mime_guesser import MimeGuesser | ||
from kink import inject | ||
|
||
@inject(use_factory=True, alias=MimeGuesser) | ||
class MagicMimeGuesser: | ||
def extension(self, file: bytes) -> str: | ||
content_type = from_buffer(file, mime=True) | ||
return guess_extension(content_type) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
from src.planner.shared.domain.value_objects.file import FileValueObject | ||
from kink import inject | ||
from src.planner.users.domain.storage import UserFileStorage | ||
from pathlib import Path | ||
|
||
@inject(use_factory=True, alias=UserFileStorage) | ||
class LocalUserFileStorage: | ||
|
||
def __init__(self, basedir: str = 'files') -> None: | ||
self._basedir = basedir | ||
|
||
|
||
async def push(self, file: bytes, path: str) -> None: | ||
Path(f"{self._basedir}/{path}").parent.mkdir(parents=True, exist_ok=True) | ||
with open(f"{self._basedir}/{path}", "wb") as f: | ||
f.write(file) | ||
|
||
async def pull(self, path: str) -> bytes: | ||
with open(f"{self._basedir}/{path}", "rb") as f: | ||
response = f.read() | ||
return response |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
48 changes: 48 additions & 0 deletions
48
tests/apps/planner/users/test_controllers/test_update_avatar.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import pytest | ||
from faker import Faker | ||
from fastapi import status | ||
from httpx import AsyncClient | ||
from kink import di | ||
from sqlalchemy.ext.asyncio import AsyncSession | ||
|
||
from apps.planner.backend.config import settings | ||
from src.planner.users.domain.repository import UserRepository | ||
from tests.apps.planner.shared.auth import AuthAsUser | ||
from tests.src.planner.users.factories import UserFactory | ||
|
||
fake = Faker() | ||
|
||
pytestmark = pytest.mark.anyio | ||
|
||
|
||
class TestUpdateUserAvatarController: | ||
def setup_method(self): | ||
self._user = UserFactory.build() | ||
self._url = f"{settings.API_PREFIX}/v1/users/{self._user.id}/avatar" | ||
|
||
async def test_success( | ||
self, client: AsyncClient, sqlalchemy_sessionmaker: type[AsyncSession] | ||
) -> None: | ||
await di[UserRepository].create(self._user) # type: ignore[type-abstract] | ||
|
||
response = await client.put( | ||
self._url, | ||
auth=AuthAsUser(self._user.id), | ||
files={"avatar": ("sample.jpg", open('tests/fixtures/files/sample.jpg', "rb"), "image/jpeg")} | ||
) | ||
|
||
assert response.status_code == status.HTTP_200_OK, response.text | ||
assert response.json() == None | ||
|
||
async def test_should_return_unauthorized_missing_token( | ||
self, client: AsyncClient, sqlalchemy_sessionmaker: type[AsyncSession] | ||
) -> None: | ||
response = await client.put(self._url) | ||
|
||
assert response.status_code == status.HTTP_401_UNAUTHORIZED, response.text | ||
|
||
json_response = response.json() | ||
assert len(json_response["detail"]) == 1 | ||
error_response = json_response["detail"][0] | ||
assert error_response["msg"] == "Is required" | ||
assert error_response["source"] == "access_token" |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.