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
File renamed without changes.
4 changes: 2 additions & 2 deletions config/env_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ 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.
# Intentionally NOT in blog_exceptions.py: startup-only crash path.
# Never caught by application code. Builtin RuntimeError sufficient.
# Do not move to exceptions.py.
# Do not move to blog_exceptions.py.
value = os.getenv(name)
if not value:
raise RuntimeError(
Expand Down
4 changes: 2 additions & 2 deletions src/application/services/article_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import re
from datetime import UTC, datetime

from exceptions import (
from blog_exceptions import (
AccountBannedError,
AccountNotFoundError,
ArticleNotFoundError,
Expand Down Expand Up @@ -40,7 +40,7 @@ def _extract_image_uuids(content: str) -> set[str]:
try:
data = json.loads(content)
# Python builtin — safety net for json.loads on non-string input.
# Not in exceptions.py. Do not move it there.
# Not in blog_exceptions.py. Do not move it there.
except (json.JSONDecodeError, TypeError):
return set()
uuids: set[str] = set()
Expand Down
2 changes: 1 addition & 1 deletion src/application/services/comment_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import nh3

from exceptions import (
from blog_exceptions import (
AccountBannedError,
AccountNotFoundError,
ArticleNotFoundError,
Expand Down
2 changes: 1 addition & 1 deletion src/application/services/file_service.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from datetime import datetime
from uuid import uuid4

from exceptions import FileTooLargeError, FileTypeError
from blog_exceptions import FileTooLargeError, FileTypeError
from src.application.domain.file_record import FileRecord
from src.application.input_ports.file_management import FileManagementPort
from src.application.output_ports.file_storage_repository import FileStorageRepository
Expand Down
2 changes: 1 addition & 1 deletion src/application/services/login_service.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from exceptions import (
from blog_exceptions import (
AccountBannedError,
AccountNotFoundError,
AuthenticationError,
Expand Down
2 changes: 1 addition & 1 deletion src/application/services/registration_service.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from exceptions import AccountAlreadyExistsError, EmailAlreadyTakenError, UsernameAlreadyTakenError
from blog_exceptions import AccountAlreadyExistsError, EmailAlreadyTakenError, UsernameAlreadyTakenError
from src.application.domain.account import Account, AccountRole
from src.application.input_ports.registration_management import RegistrationManagementPort
from src.application.output_ports.account_repository import AccountRepository
Expand Down
2 changes: 1 addition & 1 deletion src/infrastructure/input_adapters/dto/comment_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from pydantic import BaseModel, Field, field_validator

from exceptions import CommentEmptyError, CommentTooLongError
from blog_exceptions import CommentEmptyError, CommentTooLongError


class CommentRequest(BaseModel):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from pydantic import BaseModel, EmailStr, Field, model_validator

from exceptions import PasswordsDoNotMatchError
from blog_exceptions import PasswordsDoNotMatchError


class RegistrationRequest(BaseModel):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from flask_babel import gettext as _

from exceptions import BlogCommentError, FileTooLargeError, FileTypeError
from blog_exceptions import BlogCommentError, FileTooLargeError, FileTypeError
from flask import abort, flash, jsonify, redirect, render_template, request, session, url_for
from flask import g as global_request_context
from flask.views import MethodView
Expand Down Expand Up @@ -185,7 +185,7 @@ def upload_profile_photo(self):
# Intentionally broad: non-critical cleanup (delete old avatar
# file from DB). Broad catch ensures request never fails due to
# cleanup failure, even from unexpected bugs.
# Not in exceptions.py. Do not move it there.
# Not in blog_exceptions.py. Do not move it there.
except Exception:
logging.getLogger(__name__).warning(
"Failed to delete old avatar %s for account %s",
Expand Down Expand Up @@ -226,7 +226,7 @@ def remove_profile_photo(self):
self.session_service.update_avatar(None)
# Intentionally broad: non-critical cleanup. Broad catch ensures
# request never fails; if cleanup fails, user gets a flash error.
# Not in exceptions.py. Do not move it there.
# Not in blog_exceptions.py. Do not move it there.
except Exception:
flash(_("Failed to remove profile photo."), "error")
return redirect(url_for("auth.profile"))
Expand Down Expand Up @@ -392,7 +392,7 @@ def delete_account(self):
# Intentionally broad: non-critical cleanup (delete avatar
# file from DB before account deletion). Broad catch ensures
# account deletion proceeds even if avatar cleanup fails.
# Not in exceptions.py. Do not move it there.
# Not in blog_exceptions.py. Do not move it there.
except Exception:
logging.getLogger(__name__).warning(
"Failed to delete avatar %s for account %s",
Expand Down
10 changes: 5 additions & 5 deletions src/infrastructure/input_adapters/flask/flask_article_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from pydantic import ValidationError
from werkzeug.wrappers.response import Response

from exceptions import BlogCommentError
from blog_exceptions import BlogCommentError
from flask import flash, jsonify, redirect, render_template, request, url_for
from flask import g as global_request_context
from src.application.domain.comment import CommentNode
Expand Down Expand Up @@ -125,7 +125,7 @@ def read_article(self, article_id: int) -> str | Response:
try:
json.loads(content)
# Python builtin — safety net for json.loads on non-string input.
# Not in exceptions.py. Do not move it there.
# Not in blog_exceptions.py. Do not move it there.
except (json.JSONDecodeError, TypeError):
content = json.dumps([{
"type": "paragraph",
Expand Down Expand Up @@ -186,7 +186,7 @@ def api_get_article(self, article_id: int) -> Response | tuple[Response, int]:
try:
json.loads(content)
# Python builtin — safety net for json.loads on non-string input.
# Not in exceptions.py. Do not move it there.
# Not in blog_exceptions.py. Do not move it there.
except (json.JSONDecodeError, TypeError):
content = json.dumps([{
"type": "paragraph",
Expand Down Expand Up @@ -233,7 +233,7 @@ def api_create_article(self) -> Response | tuple[Response, int]:
description=data.get("description", ""),
)
# Pydantic library exception — caught at web boundary for 400 response.
# Not in exceptions.py. Do not move it there.
# Not in blog_exceptions.py. Do not move it there.
except ValidationError as e:
for error in e.errors():
return jsonify({"error": f"({error['loc'][0]}): {error['msg']}"}), 400
Expand Down Expand Up @@ -281,7 +281,7 @@ def api_update_article(self, article_id: int) -> Response | tuple[Response, int]
description=data.get("description", ""),
)
# Pydantic library exception — caught at web boundary for 400 response.
# Not in exceptions.py. Do not move it there.
# Not in blog_exceptions.py. Do not move it there.
except ValidationError as e:
for error in e.errors():
return jsonify({"error": f"({error['loc'][0]}): {error['msg']}"}), 400
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from pydantic import ValidationError
from werkzeug.wrappers.response import Response

from exceptions import BlogCommentError
from blog_exceptions import BlogCommentError
from flask import flash, redirect, request, url_for
from flask import g as global_request_context
from src.application.input_ports.comment_management import CommentManagementPort
Expand Down Expand Up @@ -71,7 +71,7 @@ def create_comment(self, article_id: int) -> Response:
try:
req_data = CommentRequest(content=request.form.get("content", ""))
# Pydantic library exception — caught at web boundary for flash + redirect.
# Not in exceptions.py. Do not move it there.
# Not in blog_exceptions.py. Do not move it there.
except ValidationError as e:
for error in e.errors():
msg = error["msg"].removeprefix("Value error, ")
Expand Down Expand Up @@ -118,7 +118,7 @@ def reply_to_comment(self, article_id: int, parent_comment_id: int) -> Response:
try:
req_data = CommentRequest(content=request.form.get("content", ""))
# Pydantic library exception — caught at web boundary for flash + redirect.
# Not in exceptions.py. Do not move it there.
# Not in blog_exceptions.py. Do not move it there.
except ValidationError as e:
for error in e.errors():
msg = error["msg"].removeprefix("Value error, ")
Expand Down
4 changes: 2 additions & 2 deletions src/infrastructure/input_adapters/flask/flask_file_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from flask_babel import gettext as _

from exceptions import FileTooLargeError, FileTypeError
from blog_exceptions import FileTooLargeError, FileTypeError
from flask import jsonify, request, send_file
from src.application.input_ports.file_management import FileManagementPort
from src.infrastructure.input_adapters.dto.file_upload_request import FileUploadRequest
Expand Down Expand Up @@ -41,7 +41,7 @@ def upload_image(self):
mime_type=uploaded_file.content_type or "application/octet-stream",
)
# Intentionally broad: catches Pydantic ValidationError or unexpected errors
# from file upload request parsing. Not in exceptions.py. Do not move it there.
# from file upload request parsing. Not in blog_exceptions.py. Do not move it there.
except Exception as e:
return jsonify({"error": str(e)}), 400

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from flask_babel import gettext as _
from pydantic import ValidationError

from exceptions import AccountBannedError, AuthenticationError
from blog_exceptions import AccountBannedError, AuthenticationError
from flask import flash, redirect, render_template, request, url_for
from flask import g as global_request_context
from flask.views import MethodView
Expand Down Expand Up @@ -51,7 +51,7 @@ def authenticate(self):
password=request.form.get("password", "")
)
# Pydantic library exception — caught at web boundary for flash + redirect.
# Not in exceptions.py. Do not move it there.
# Not in blog_exceptions.py. Do not move it there.
except ValidationError as e:
for error in e.errors():
location = str(error["loc"][0]) if error["loc"] else "Request"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from flask_babel import gettext as _
from pydantic import ValidationError

from exceptions import BlogCommentError
from blog_exceptions import BlogCommentError
from flask import flash, redirect, render_template, request, url_for
from flask import g as global_request_context
from flask.views import MethodView
Expand Down Expand Up @@ -54,7 +54,7 @@ def register(self):
confirm_password=request.form.get("confirm_password", "")
)
# Pydantic library exception — caught at web boundary for flash + redirect.
# Not in exceptions.py. Do not move it there.
# Not in blog_exceptions.py. Do not move it there.
except ValidationError as e:
for error in e.errors():
location = str(error["loc"][0]) if error["loc"] else "Request"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from datetime import datetime

from exceptions import AccountNotFoundError
from blog_exceptions import AccountNotFoundError
from src.application.domain.account import Account, AccountRole
from src.application.output_ports.account_repository import AccountRepository

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,11 @@ def verify(self, password: str, hashed_password: str) -> bool:
try:
return self._hasher.verify(hashed_password, password)
# Argon2 library exception — caught, returns False.
# Not in exceptions.py. Do not move it there.
# Not in blog_exceptions.py. Do not move it there.
except VerifyMismatchError:
return False
# Argon2 library exception — caught for legacy hash fallback.
# Not in exceptions.py. Do not move it there.
# Not in blog_exceptions.py. Do not move it there.
except InvalidHashError:
is_correct = secrets.compare_digest(password, hashed_password)
self._hasher.hash("dummy_password_for_timing_consistency")
Expand All @@ -76,6 +76,6 @@ def check_needs_rehash(self, hashed_password: str) -> bool:
try:
return self._hasher.check_needs_rehash(hashed_password)
# Argon2 library exception — caught for legacy hash fallback.
# Not in exceptions.py. Do not move it there.
# Not in blog_exceptions.py. Do not move it there.
except InvalidHashError:
return True
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session

from exceptions import AccountAlreadyExistsError, AccountNotFoundError, DatabaseError
from blog_exceptions import AccountAlreadyExistsError, AccountNotFoundError, DatabaseError
from src.application.domain.account import Account
from src.application.output_ports.account_repository import AccountRepository
from src.infrastructure.output_adapters.dto.account_record import AccountRecord
Expand Down Expand Up @@ -143,7 +143,7 @@ def save(self, account: Account) -> None:
try:
self._db_commit()
# SQLAlchemy library exception — caught to translate to domain exception.
# Not in exceptions.py. Do not move it there.
# Not in blog_exceptions.py. Do not move it there.
except IntegrityError as e:
constraint_name = cast(UniqueViolation, e.orig).diag.constraint_name if e.orig else None

Expand Down Expand Up @@ -202,7 +202,7 @@ def update_email(self, account_id: int, new_email: str) -> None:
try:
self._db_commit()
# SQLAlchemy library exception — caught to translate to domain exception.
# Not in exceptions.py. Do not move it there.
# Not in blog_exceptions.py. Do not move it there.
except IntegrityError as e:
constraint_name = cast(UniqueViolation, e.orig).diag.constraint_name if e.orig else None
if constraint_name == "accounts_account_email_key":
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
from sqlalchemy.orm import Session

from exceptions import DatabaseError
from blog_exceptions import DatabaseError


class SqlAlchemyBaseAdapter:
Expand Down Expand Up @@ -32,7 +32,7 @@ def _db_get(self, model_class, pk):
try:
return self._session.get(model_class, pk)
# SQLAlchemy library exception — caught and translated to DatabaseError.
# Not in exceptions.py. Do not move it there.
# Not in blog_exceptions.py. Do not move it there.
except SQLAlchemyError as e:
raise DatabaseError("Database read failed.") from e

Expand All @@ -48,7 +48,7 @@ def _db_add(self, model):
try:
self._session.add(model)
# SQLAlchemy library exception — caught and translated to DatabaseError.
# Not in exceptions.py. Do not move it there.
# Not in blog_exceptions.py. Do not move it there.
except SQLAlchemyError as e:
raise DatabaseError("Database insert failed.") from e

Expand All @@ -64,7 +64,7 @@ def _db_delete(self, model):
try:
self._session.delete(model)
# SQLAlchemy library exception — caught and translated to DatabaseError.
# Not in exceptions.py. Do not move it there.
# Not in blog_exceptions.py. Do not move it there.
except SQLAlchemyError as e:
raise DatabaseError("Database delete failed.") from e

Expand All @@ -81,12 +81,12 @@ def _db_commit(self):
try:
self._session.commit()
# SQLAlchemy library exception — re-raised for constraint handling by callers.
# Not in exceptions.py. Do not move it there.
# Not in blog_exceptions.py. Do not move it there.
except IntegrityError:
self._session.rollback()
raise
# SQLAlchemy library exception — caught and translated to DatabaseError.
# Not in exceptions.py. Do not move it there.
# Not in blog_exceptions.py. Do not move it there.
except SQLAlchemyError as e:
self._session.rollback()
raise DatabaseError("Database commit failed.") from e
Expand Down Expand Up @@ -131,6 +131,6 @@ def _db_query_raw(self, query_fn):
try:
return query_fn()
# SQLAlchemy library exception — caught and translated to DatabaseError.
# Not in exceptions.py. Do not move it there.
# Not in blog_exceptions.py. Do not move it there.
except SQLAlchemyError as e:
raise DatabaseError("Database query failed.") from e
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ def test_update_email_unauthenticated(self):
def test_update_email_error(self):
fake_user = create_test_account(account_id=1, account_email="old@test.com")
self.mock_session_service.get_current_account.return_value = fake_user
from exceptions import EmailAlreadyTakenError
from blog_exceptions import EmailAlreadyTakenError
self.mock_session_service.update_email.side_effect = EmailAlreadyTakenError("This email is already taken.")
response = self.client.post(
"/profile/email",
Expand Down Expand Up @@ -535,7 +535,7 @@ def test_admin_change_role_nonexistent_target(self):
admin = create_test_account(account_id=1, account_role=AccountRole.ADMIN)
self.set_current_user(admin)
self.mock_session_service.get_current_account.return_value = admin
from exceptions import AccountNotFoundError
from blog_exceptions import AccountNotFoundError
self.mock_session_service.update_account_role.side_effect = AccountNotFoundError("Account not found.")
self.mock_session_service.get_account_by_id.return_value = None
response = self.client.post(
Expand Down Expand Up @@ -614,7 +614,7 @@ def test_admin_ban_another_admin_returns_302_with_flash(self):
admin = create_test_account(account_id=1, account_role=AccountRole.ADMIN)
self.set_current_user(admin)
self.mock_session_service.get_current_account.return_value = admin
from exceptions import AuthorizationError
from blog_exceptions import AuthorizationError
self.mock_session_service.ban_account.side_effect = AuthorizationError("Cannot ban another admin.")
response = self.client.post("/admin/users/2/ban", data={"ban_reason": "Spam"}, follow_redirects=True)
assert response.status_code == 200
Expand All @@ -625,7 +625,7 @@ def test_admin_ban_nonexistent_user_redirects_with_flash(self):
admin = create_test_account(account_id=1, account_role=AccountRole.ADMIN)
self.set_current_user(admin)
self.mock_session_service.get_current_account.return_value = admin
from exceptions import AccountNotFoundError
from blog_exceptions import AccountNotFoundError
self.mock_session_service.ban_account.side_effect = AccountNotFoundError("Account not found.")
response = self.client.post("/admin/users/999/ban", data={"ban_reason": "Spam"}, follow_redirects=True)
assert response.status_code == 200
Expand Down
Loading
Loading