Skip to content

chore: improved DDD layering#432

Merged
TeKrop merged 2 commits into
mainfrom
fix/ddd-layering-improvements
Jul 1, 2026
Merged

chore: improved DDD layering#432
TeKrop merged 2 commits into
mainfrom
fix/ddd-layering-improvements

Conversation

@TeKrop

@TeKrop TeKrop commented Jul 1, 2026

Copy link
Copy Markdown
Owner

Summary by Sourcery

Enforce framework-agnostic domain layering by having domain services and parsers raise and propagate domain-specific exceptions instead of FastAPI HTTPException, and update tests, linting, and documentation to reflect the new error-handling and layering rules.

Bug Fixes:

  • Ensure unknown-player marking logic only applies to recognized parser errors and does not run for arbitrary HTTPException instances raised by adapters.

Enhancements:

  • Refine player service exception handling to propagate ParserBlizzardError/ParserInternalError directly and centralize HTTP response translation in the API layer.
  • Allow OverfastError/ParserBlizzardError message payloads to be structured data for richer client-facing error details.
  • Simplify hero service by letting parser-level ParserBlizzardError propagate instead of being wrapped into HTTPException.
  • Update parsers to depend on standard http.HTTPStatus instead of FastAPI status codes, improving domain independence.

Build:

  • Tighten Ruff configuration to ban direct FastAPI usage in the domain layer and to scope TID251 ignores to API- and adapter-facing packages.

Documentation:

  • Document the DDD layering and dependency flow in the README, including a mermaid diagram of the architecture.

Tests:

  • Adjust player and hero parser/service tests to assert on ParserBlizzardError propagation, updated message typing, and the refined unknown-player marking behavior.

@TeKrop TeKrop self-assigned this Jul 1, 2026
@TeKrop TeKrop added the chore Code cleanup, refactoring, etc. label Jul 1, 2026
@sourcery-ai

sourcery-ai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Refactors domain services and parsers to be FastAPI-agnostic by operating purely on domain exceptions, adjusts parser and service behavior to propagate ParserBlizzardError/ParserParsingError through to the API layer, tightens lint rules to enforce DDD boundaries, and updates tests and documentation to reflect the new layering model and structured error payloads.

File-Level Changes

Change Details Files
Player service now operates solely on domain exceptions and handles unknown-player caching only for ParserBlizzardError/ParserParsingError, no longer wrapping or modifying FastAPI HTTPException instances.
  • Remove fastapi.HTTPException usage/imports from the player service and rely on ParserBlizzardError/ParserParsingError/ParserInternalError instead.
  • Change _mark_player_unknown to accept ParserBlizzardError, mutating its message to a structured payload with retry metadata instead of altering HTTPException.detail.
  • Update _handle_player_exceptions to mark unknown players and re-raise ParserBlizzardError/ParserInternalError directly, convert specific ParserParsingError cases into ParserBlizzardError(404), and fall back to raising unrecognized errors unchanged.
  • Ensure non-domain HTTPException instances (e.g. Blizzard adapter transport errors) are passed through without unknown-player marking.
app/domain/services/player_service.py
tests/players/services/test_player_service.py
Hero service no longer translates parser errors into HTTPException, allowing ParserBlizzardError/ParserParsingError to propagate to the API layer.
  • Remove FastAPI imports and HTTPException wrapping in hero_service, returning raw parser exceptions.
  • Ensure hero HTML fetch/parse flow validates existence via parse_hero_html and lets ParserBlizzardError(404) bubble out.
  • Preserve ParserInternalError wrapping for ParserParsingError during hero stats retrieval while dropping special handling for ParserBlizzardError.
app/domain/services/hero_service.py
Domain exceptions now support structured message payloads and are used consistently in parsers with stdlib HTTPStatus instead of fastapi.status.
  • Allow OverfastError.message and ParserBlizzardError.message to be either strings or structured dict payloads and adjust str accordingly.
  • Update ParserBlizzardError.init signature to accept str
dict messages.
  • Switch hero, hero stats, and player profile parsers to use http.HTTPStatus.*.value codes instead of fastapi.status constants for ParserBlizzardError raising.
  • Adjust tests that asserted on ParserBlizzardError.message to treat it as possibly non-string by coercing to str before inspection.
  • DDD layering and FastAPI framework boundaries are now enforced via documentation and lint configuration.
    • Extend README with an explicit description and mermaid diagram of the DDD layers (api, adapters, domain, infrastructure) and their dependency directions.
    • Strengthen Ruff configuration to ban direct fastapi imports globally via flake8-tidy-imports, with per-directory TID251 ignores only for non-domain modules where FastAPI usage is allowed.
    • Allow tests to ignore TID251 so they can use fastapi.TestClient while still keeping domain code framework-agnostic.
    README.md
    pyproject.toml

    Tips and commands

    Interacting with Sourcery

    • Trigger a new review: Comment @sourcery-ai review on the pull request.
    • Continue discussions: Reply directly to Sourcery's review comments.
    • Generate a GitHub issue from a review comment: Ask Sourcery to create an
      issue from a review comment by replying to it. You can also reply to a
      review comment with @sourcery-ai issue to create an issue from it.
    • Generate a pull request title: Write @sourcery-ai anywhere in the pull
      request title to generate a title at any time. You can also comment
      @sourcery-ai title on the pull request to (re-)generate the title at any time.
    • Generate a pull request summary: Write @sourcery-ai summary anywhere in
      the pull request body to generate a PR summary at any time exactly where you
      want it. You can also comment @sourcery-ai summary on the pull request to
      (re-)generate the summary at any time.
    • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
      request to (re-)generate the reviewer's guide at any time.
    • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
      pull request to resolve all Sourcery comments. Useful if you've already
      addressed all the comments and don't want to see them anymore.
    • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
      request to dismiss all existing Sourcery reviews. Especially useful if you
      want to start fresh with a new review - don't forget to comment
      @sourcery-ai review to trigger a new review!

    Customizing Your Experience

    Access your dashboard to:

    • Enable or disable review features such as the Sourcery-generated pull request
      summary, the reviewer's guide, and others.
    • Change the review language.
    • Add, remove or edit custom review instructions.
    • Adjust other review settings.

    Getting Help

    @TeKrop TeKrop changed the title fix: improved DDD layering chore: improved DDD layering Jul 1, 2026

    @sourcery-ai sourcery-ai Bot left a comment

    Copy link
    Copy Markdown
    Contributor

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Hey - I've found 2 issues

    Prompt for AI Agents
    Please address the comments from this code review:
    
    ## Individual Comments
    
    ### Comment 1
    <location path="tests/players/services/test_player_service.py" line_range="378-387" />
    <code_context>
    +    async def test_unrecognized_exception_type_reraises_unmarked(self):
    </code_context>
    <issue_to_address>
    **issue (testing):** Mock the cache instead of casting to Any to reliably assert `set_player_status` is not awaited.
    
    Here the test relies on `cast("Any", svc.cache).set_player_status.assert_not_awaited()`, which only works if `svc.cache` happens to be a mock exposing that method. With a different implementation this could either raise a type error or, worse, silently stop testing what you intend.
    
    To make the behavior explicit and reliable, please inject a mocked cache as in other tests:
    
    ```python
    cache = AsyncMock()
    svc = _make_service(cache=cache)
    ...
    with pytest.raises(HTTPException) as exc_info:
        await svc._handle_player_exceptions(error, "TeKrop-2217", identity)
    
    assert exc_info.value is error
    cache.set_player_status.assert_not_awaited()
    ```
    
    This way the test definitively verifies that unknown-player marking is not triggered for non-domain `HTTPException` instances, independent of the concrete `svc.cache` implementation.
    </issue_to_address>
    
    ### Comment 2
    <location path="tests/players/services/test_player_service.py" line_range="272-273" />
    <code_context>
         async def test_disabled_feature_is_noop(self):
             svc = _make_service()
    -        exc = HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="not found")
    +        exc = ParserBlizzardError(
    +            status_code=status.HTTP_404_NOT_FOUND, message="not found"
    +        )
             with patch("app.domain.services.player_service.settings") as s:
    </code_context>
    <issue_to_address>
    **suggestion (testing):** Add assertions that `_mark_player_unknown` updates `ParserBlizzardError.message` with the structured payload.
    
    Since `_mark_player_unknown` now mutates a `ParserBlizzardError` by replacing `message` with a dict containing `error`, `retry_after`, and `next_check_at`, add assertions in `test_404_marks_player` (and/or `test_404_increments_check_count`) to verify this, e.g.:
    
    ```python
    assert isinstance(exc.message, dict)
    assert exc.message["error"] == "Player not found"
    assert "retry_after" in exc.message
    assert "next_check_at" in exc.message
    ```
    
    This ensures the exception is enriched with the structured payload expected by the API layer.
    
    Suggested implementation:
    
    ```python
        @pytest.mark.asyncio
        async def test_404_marks_player(self):
            svc = _make_service()
            exc = ParserBlizzardError(
                status_code=status.HTTP_404_NOT_FOUND, message="not found"
            )
            # existing setup / cache assertions above or below
            await svc._mark_player_unknown("abc123", exc)
    
            # ensure the exception was enriched with structured payload
            assert isinstance(exc.message, dict)
            assert exc.message["error"] == "Player not found"
            assert "retry_after" in exc.message
            assert "next_check_at" in exc.message
    
    ```
    
    ```python
        @pytest.mark.asyncio
        async def test_404_increments_check_count(self):
            svc = _make_service()
            exc = ParserBlizzardError(
                status_code=status.HTTP_404_NOT_FOUND, message="not found"
            )
            # existing setup / cache assertions above or below
            await svc._mark_player_unknown("abc123", exc)
    
            # ensure the exception was enriched with structured payload
            assert isinstance(exc.message, dict)
            assert exc.message["error"] == "Player not found"
            assert "retry_after" in exc.message
            assert "next_check_at" in exc.message
    
    ```
    
    I only see the `test_disabled_feature_is_noop` and `test_non_404_is_noop` tests in your snippet, so I’ve assumed the presence and approximate shape of `test_404_marks_player` and `test_404_increments_check_count` based on your comment.
    
    To apply these edits, you may need to:
    1. Adjust the `SEARCH` blocks to match the exact bodies of `test_404_marks_player` and `test_404_increments_check_count` in your file (especially comments or extra setup/teardown code).
    2. If the error message used by `_mark_player_unknown` is different from `"Player not found"`, update the `exc.message["error"]` assertion accordingly.
    3. If only one of the tests exists (e.g., only `test_404_marks_player`), include the assertion block only there.
    </issue_to_address>

    Sourcery is free for open source - if you like our reviews please consider sharing them ✨
    Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

    Comment thread tests/players/services/test_player_service.py
    Comment thread tests/players/services/test_player_service.py
    @sonarqubecloud

    sonarqubecloud Bot commented Jul 1, 2026

    Copy link
    Copy Markdown

    @TeKrop TeKrop merged commit 5818d6c into main Jul 1, 2026
    9 checks passed
    @TeKrop TeKrop deleted the fix/ddd-layering-improvements branch July 1, 2026 20:32
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Labels

    chore Code cleanup, refactoring, etc.

    Projects

    None yet

    Development

    Successfully merging this pull request may close these issues.

    1 participant