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
7 changes: 6 additions & 1 deletion config/env_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,14 @@ def _get_env(self, name: str) -> str:
Raises:
RuntimeError: If the mandatory environment variable is missing.
"""
# Intentionally NOT in exceptions.py: startup-only crash path.
# Never caught by application code. Builtin RuntimeError sufficient.
# Do not move to exceptions.py.
value = os.getenv(name)
if not value:
raise RuntimeError(f"Infrastructure Error : Missing environment variable '{name}'")
raise RuntimeError(
f"Infrastructure Error : Missing environment variable '{name}'"
)
return value

@property
Expand Down
103 changes: 103 additions & 0 deletions exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
class BlogCommentError(Exception):
"""Base for all application and infrastructure exceptions."""
pass


class AccountAlreadyExistsError(BlogCommentError):
"""Raised when username or email already exists."""
pass


class FileTooLargeError(BlogCommentError):
"""Raised when uploaded file exceeds max size."""
pass


class FileTypeError(BlogCommentError):
"""Raised when uploaded file has unsupported type/extension."""
pass


class AuthenticationError(BlogCommentError):
"""Raised when credentials are invalid or user is not authenticated."""
pass


class AuthorizationError(BlogCommentError):
"""Raised when user lacks permission for the requested action."""
pass


class InsufficientPermissionsError(AuthorizationError):
"""Raised when account role does not have the required permission level."""
pass


class AccountNotFoundError(BlogCommentError):
"""Raised when an account is not found by id, username, or email."""
pass


class AccountBannedError(BlogCommentError):
"""Raised when an account is banned and cannot perform actions."""
pass


class UsernameAlreadyTakenError(BlogCommentError):
"""Raised when trying to register with an existing username."""
pass


class EmailAlreadyTakenError(BlogCommentError):
"""Raised when trying to register or update with an existing email."""
pass


class ArticleNotFoundError(BlogCommentError):
"""Raised when an article is not found."""
pass


class OwnershipError(BlogCommentError):
"""Raised when a user is not the owner of the resource."""
pass


class CommentNotFoundError(BlogCommentError):
"""Raised when a comment is not found."""
pass


class CommentAuthorizationError(AuthorizationError):
"""Raised when a user is not authorized for a comment action."""
pass


class CommentDeletedError(BlogCommentError):
"""Raised when attempting to modify a deleted comment."""
pass


class CommentValidationError(BlogCommentError):
"""Raised when comment content is invalid (empty, too long, etc.)."""
pass


class DatabaseError(BlogCommentError):
"""Raised when a database operation fails (connection, query, commit)."""
pass


class PasswordsDoNotMatchError(ValueError):
"""Raised when password and confirmation do not match."""
pass


class CommentEmptyError(ValueError):
"""Raised when comment content is empty."""
pass


class CommentTooLongError(ValueError):
"""Raised when comment content exceeds max length."""
pass
30 changes: 0 additions & 30 deletions src/application/application_exceptions.py

This file was deleted.

45 changes: 24 additions & 21 deletions src/application/input_ports/account_session_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,35 +66,30 @@ def update_avatar(self, avatar_file_id: str | None) -> None:
pass

@abstractmethod
def update_email(self, new_email: str) -> str | None:
def update_email(self, new_email: str) -> None:
"""
Updates the email address for the currently authenticated account.

Validates that the new email is not already in use by another account
before persisting the change.

Args:
new_email: The new email address to set.

Returns:
str | None: None on success, or an error message string if
the email is already taken or the user is not authenticated.
Raises:
AuthenticationError: If the user is not signed in.
EmailAlreadyTakenError: If the email is already in use by another account.
"""
pass

@abstractmethod
def update_password(self, new_password: str) -> str | None:
def update_password(self, new_password: str) -> None:
"""
Updates the password for the currently authenticated account.

Hashes the new password and persists it via the account repository.

Args:
new_password: The new plaintext password to set.

Returns:
str | None: None on success, or an error message string if
the user is not authenticated.
Raises:
AuthenticationError: If the user is not signed in.
"""
pass

Expand Down Expand Up @@ -164,7 +159,7 @@ def count_search_accounts(self, query: str) -> int:
pass

@abstractmethod
def ban_account(self, admin_id: int, target_account_id: int, ban_reason: str | None) -> str | None:
def ban_account(self, admin_id: int, target_account_id: int, ban_reason: str | None) -> None:
"""
Bans a user account. Only admins can ban non-admin accounts.

Expand All @@ -173,22 +168,25 @@ def ban_account(self, admin_id: int, target_account_id: int, ban_reason: str | N
target_account_id: The unique identifier of the account to ban.
ban_reason: Optional reason for the ban.

Returns:
str | None: None on success, or an error message string if the operation fails.
Raises:
AuthorizationError: If the requester is not an admin.
AccountNotFoundError: If the target account is not found.
AuthorizationError: If the target is another admin.
"""
pass

@abstractmethod
def unban_account(self, admin_id: int, target_account_id: int) -> str | None:
def unban_account(self, admin_id: int, target_account_id: int) -> None:
"""
Unbans a user account. Only admins can unban accounts.

Args:
admin_id: The unique identifier of the admin performing the action.
target_account_id: The unique identifier of the account to unban.

Returns:
str | None: None on success, or an error message string if the operation fails.
Raises:
AuthorizationError: If the requester is not an admin.
AccountNotFoundError: If the target account is not found.
"""
pass

Expand All @@ -203,11 +201,14 @@ def delete_account(self, account_id: int) -> None:

Args:
account_id: The unique identifier of the account to delete.

Raises:
AccountNotFoundError: If no account exists with the given id.
"""
pass

@abstractmethod
def update_account_role(self, admin_id: int, target_id: int, new_role: str) -> str | None:
def update_account_role(self, admin_id: int, target_id: int, new_role: str) -> None:
"""
Allows an admin user to update the role of another user account.

Expand All @@ -216,7 +217,9 @@ def update_account_role(self, admin_id: int, target_id: int, new_role: str) -> s
target_id: The unique identifier of the account whose role is to be updated.
new_role: The new role string ("user" or "author").

Returns:
str | None: None on success, or an error message string if the operation fails.
Raises:
AuthorizationError: If the requester is not an admin.
AccountNotFoundError: If the target account is not found.
AuthorizationError: If the target is another admin.
"""
pass
43 changes: 30 additions & 13 deletions src/application/input_ports/article_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class ArticleManagementPort(ABC):
"""

@abstractmethod
def create_article(self, title: str, content: str, author_id: int, author_role: str, description: str = "") -> Article | str:
def create_article(self, title: str, content: str, author_id: int, author_role: str, description: str = "") -> Article:
"""
Creates a new article if the user has sufficient permissions.

Expand All @@ -22,8 +22,12 @@ def create_article(self, title: str, content: str, author_id: int, author_role:
description (str): Short description displayed in article list. Optional.

Returns:
Article | str: The newly created Article domain entity,
or an error message string if unauthorized or account not found.
Article: The newly created Article domain entity.

Raises:
AccountNotFoundError: If the author account is not found.
InsufficientPermissionsError: If the user is not an author or admin.
AccountBannedError: If the account is banned.
"""
pass

Expand Down Expand Up @@ -51,7 +55,7 @@ def get_by_id(self, article_id: int) -> Article | None:
pass

@abstractmethod
def update_article(self, article_id: int, user_id: int, title: str, content: str, description: str = "") -> Article | str:
def update_article(self, article_id: int, user_id: int, title: str, content: str, description: str = "") -> Article:
"""
Updates an existing article ensuring the requester is the original author.

Expand All @@ -63,13 +67,19 @@ def update_article(self, article_id: int, user_id: int, title: str, content: str
description (str): Short description displayed in article list. Optional.

Returns:
Article | str: The updated Article domain entity,
or an error message string if not found or unauthorized.
Article: The updated Article domain entity.

Raises:
AccountNotFoundError: If the user account is not found.
InsufficientPermissionsError: If the user is not an author or admin.
AccountBannedError: If the account is banned.
ArticleNotFoundError: If the article does not exist.
OwnershipError: If the user is not the author (and not admin).
"""
pass

@abstractmethod
def delete_article(self, article_id: int, user_id: int) -> bool | str:
def delete_article(self, article_id: int, user_id: int) -> bool:
"""
Deletes an article. Only the original author or an admin can delete it.

Expand All @@ -78,7 +88,14 @@ def delete_article(self, article_id: int, user_id: int) -> bool | str:
user_id (int): ID of the user requesting the deletion.

Returns:
bool | str: True if deletion was successful, or an error message string.
bool: True if deletion was successful.

Raises:
AccountNotFoundError: If the user account is not found.
InsufficientPermissionsError: If the user is not an author or admin.
AccountBannedError: If the account is banned.
ArticleNotFoundError: If the article does not exist.
OwnershipError: If the user is not the author (and not admin).
"""
pass

Expand Down Expand Up @@ -122,17 +139,18 @@ def get_author_name(self, author_id: int | None) -> str:
pass

@abstractmethod
def get_article_with_comments(self, article_id: int) -> ArticleDetailView | str:
def get_article_with_comments(self, article_id: int) -> ArticleDetailView:
"""
Orchestrates the retrieval of an article, its associated comments, and author information.
Respects the separation of concerns by using the comment management port.

Args:
article_id (int): ID of the article to retrieve.

Returns:
ArticleDetailView | str: A Read Model for the complete article detail page,
or an error message string if the article is missing.
ArticleDetailView: A Read Model for the complete article detail page.

Raises:
ArticleNotFoundError: If the article does not exist.
"""
pass

Expand Down Expand Up @@ -166,4 +184,3 @@ def count_search(self, query: str) -> int:
The total number of matching articles.
"""
pass

Loading
Loading