From 3930489d5f39a96fa5c00e96c75f8604a4dcaef1 Mon Sep 17 00:00:00 2001 From: Masum Billal Date: Sun, 16 Jun 2024 20:01:07 +0600 Subject: [PATCH 1/3] fixed structure --- pyproject.toml | 4 ++-- tests/{views => }/common.py | 0 tests/conftest.py | 6 +++--- tests/{views => }/test_bug.py | 2 +- tests/{views => }/test_story.py | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) rename tests/{views => }/common.py (100%) rename tests/{views => }/test_bug.py (97%) rename tests/{views => }/test_story.py (96%) diff --git a/pyproject.toml b/pyproject.toml index 2ecef0b..f60f918 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "tracker" -version = "2.1.16" +version = "2.1.17" description = "A simple bug tracking API" authors = ["Masum Billal "] license = "LICENSE" @@ -23,7 +23,7 @@ pytest = "^7.4.0" coverage = "^7.3.0" pytest-asyncio = "^0.21.1" -[tool.pytest] +[tool.pytest.ini_options] asyncio_mode = "auto" [tool.ruff] diff --git a/tests/views/common.py b/tests/common.py similarity index 100% rename from tests/views/common.py rename to tests/common.py diff --git a/tests/conftest.py b/tests/conftest.py index cab05cf..9b8dce0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,7 +20,7 @@ ) -@pytest_asyncio.fixture(scope="class") +@pytest_asyncio.fixture(scope="module") async def app(): session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)() app_test = FastAPI() @@ -33,7 +33,7 @@ async def app(): await session.close() -@pytest_asyncio.fixture(scope="class") +@pytest_asyncio.fixture(scope="module") async def client(app: FastAPI) -> AsyncGenerator[AsyncClient, None]: async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) @@ -47,7 +47,7 @@ async def client(app: FastAPI) -> AsyncGenerator[AsyncClient, None]: await engine.dispose() -@pytest.fixture(scope="class") +@pytest.fixture(scope="module") def event_loop(): policy = asyncio.get_event_loop_policy() loop = policy.new_event_loop() diff --git a/tests/views/test_bug.py b/tests/test_bug.py similarity index 97% rename from tests/views/test_bug.py rename to tests/test_bug.py index a33e093..835fa55 100644 --- a/tests/views/test_bug.py +++ b/tests/test_bug.py @@ -1,7 +1,7 @@ import pytest from httpx import AsyncClient -from tests.views.common import detail_not_found +from tests.common import detail_not_found class TestStory: diff --git a/tests/views/test_story.py b/tests/test_story.py similarity index 96% rename from tests/views/test_story.py rename to tests/test_story.py index 0ae948a..27fa017 100644 --- a/tests/views/test_story.py +++ b/tests/test_story.py @@ -1,7 +1,7 @@ import pytest from httpx import AsyncClient -from tests.views.common import detail_not_found +from tests.common import detail_not_found class TestStory: From ec42985e21fb6c9f2454ac034823d858c5618caf Mon Sep 17 00:00:00 2001 From: Masum Billal Date: Sun, 16 Jun 2024 21:16:46 +0600 Subject: [PATCH 2/3] refactored to remove unnecessary commands --- tests/conftest.py | 4 +- tests/test_bug.py | 21 +++++++--- tests/test_story.py | 9 +++-- tracker/main.py | 4 +- tracker/models/bug.py | 3 -- tracker/{serializers => responses}/base.py | 0 tracker/{serializers => responses}/bug.py | 4 +- tracker/{serializers => responses}/story.py | 2 +- tracker/{views => routers}/bug.py | 12 ++---- tracker/{views => routers}/story.py | 10 ++--- tracker/services/bug.py | 43 +++++++-------------- tracker/services/story.py | 11 +++--- 12 files changed, 53 insertions(+), 70 deletions(-) rename tracker/{serializers => responses}/base.py (100%) rename tracker/{serializers => responses}/bug.py (78%) rename tracker/{serializers => responses}/story.py (85%) rename tracker/{views => routers}/bug.py (71%) rename tracker/{views => routers}/story.py (81%) diff --git a/tests/conftest.py b/tests/conftest.py index 9b8dce0..24f7527 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,8 +10,8 @@ from tracker.db.session import get_db from tracker.models.base import Base -from tracker.views.bug import router as bug_router -from tracker.views.story import router as story_router +from tracker.routers.bug import router as bug_router +from tracker.routers.story import router as story_router SQLALCHEMY_DATABASE_URL = "sqlite+aiosqlite:///./test_db.db" diff --git a/tests/test_bug.py b/tests/test_bug.py index 835fa55..aa9359b 100644 --- a/tests/test_bug.py +++ b/tests/test_bug.py @@ -1,10 +1,11 @@ import pytest +from fastapi import status from httpx import AsyncClient from tests.common import detail_not_found -class TestStory: +class TestBug: async def __test_bug(self, bug: dict): assert isinstance(bug, dict) assert "id" in bug @@ -22,14 +23,24 @@ async def test_create(self, client: AsyncClient) -> None: "story_id": 1, } response = await client.post(url="/bugs/", json=json) - assert response.status_code == 200 + assert response.status_code == status.HTTP_200_OK bug = response.json() await self.__test_bug(bug=bug) + @pytest.mark.asyncio + async def test_create_exception(self, client: AsyncClient) -> None: + json = { + "title": "Test Bug", + "description": "Description of Test Bug", + "story_id": 100, + } + response = await client.post(url="/bugs/", json=json) + assert response.status_code == status.HTTP_404_NOT_FOUND + @pytest.mark.asyncio async def test_get(self, client: AsyncClient) -> None: response = await client.get(url="/bugs/") - assert response.status_code == 200 + assert response.status_code == status.HTTP_200_OK bugs = response.json() assert isinstance(bugs, list) @@ -39,13 +50,13 @@ async def test_get(self, client: AsyncClient) -> None: @pytest.mark.asyncio async def test_get_by_id(self, client: AsyncClient) -> None: response = await client.get(url="/bugs/1") - assert response.status_code == 200 + assert response.status_code == status.HTTP_200_OK bug = response.json() await self.__test_bug(bug=bug) @pytest.mark.asyncio async def test_get_not_found(self, client: AsyncClient) -> None: response = await client.get(url="/bugs/2") - assert response.status_code == 404 + assert response.status_code == status.HTTP_404_NOT_FOUND error = response.json() detail_not_found(error=error) diff --git a/tests/test_story.py b/tests/test_story.py index 27fa017..bea37b7 100644 --- a/tests/test_story.py +++ b/tests/test_story.py @@ -1,4 +1,5 @@ import pytest +from fastapi import status from httpx import AsyncClient from tests.common import detail_not_found @@ -15,14 +16,14 @@ async def __test_story(self, story: dict): async def test_create(self, client: AsyncClient) -> None: json = {"name": "Test Story"} response = await client.post(url="/stories/", json=json) - assert response.status_code == 200 + assert response.status_code == status.HTTP_200_OK story = response.json() await self.__test_story(story=story) @pytest.mark.asyncio async def test_get(self, client: AsyncClient) -> None: response = await client.get(url="/stories/") - assert response.status_code == 200 + assert response.status_code == status.HTTP_200_OK data = response.json() assert isinstance(data, list) @@ -32,13 +33,13 @@ async def test_get(self, client: AsyncClient) -> None: @pytest.mark.asyncio async def test_get_by_id(self, client: AsyncClient) -> None: response = await client.get(url="/stories/1") - assert response.status_code == 200 + assert response.status_code == status.HTTP_200_OK story = response.json() await self.__test_story(story=story) @pytest.mark.asyncio async def test_get_not_found(self, client: AsyncClient) -> None: response = await client.get(url="/stories/2") - assert response.status_code == 404 + assert response.status_code == status.HTTP_404_NOT_FOUND error = response.json() detail_not_found(error=error) diff --git a/tracker/main.py b/tracker/main.py index d103016..71f371e 100644 --- a/tracker/main.py +++ b/tracker/main.py @@ -4,8 +4,8 @@ from fastapi import FastAPI from tracker.db.session import engine -from tracker.views.bug import router as bug_router -from tracker.views.story import router as story_router +from tracker.routers.bug import router as bug_router +from tracker.routers.story import router as story_router async def configure_router(): diff --git a/tracker/models/bug.py b/tracker/models/bug.py index 1ee8838..63d46cb 100644 --- a/tracker/models/bug.py +++ b/tracker/models/bug.py @@ -14,6 +14,3 @@ class Bug(Base): class Config: orm_mode = True - - def __repr__(self) -> str: - return f"{self.title}, {self.story_id}" diff --git a/tracker/serializers/base.py b/tracker/responses/base.py similarity index 100% rename from tracker/serializers/base.py rename to tracker/responses/base.py diff --git a/tracker/serializers/bug.py b/tracker/responses/bug.py similarity index 78% rename from tracker/serializers/bug.py rename to tracker/responses/bug.py index 767228d..de28007 100644 --- a/tracker/serializers/bug.py +++ b/tracker/responses/bug.py @@ -2,8 +2,8 @@ from pydantic import ConfigDict -from tracker.serializers.base import Base -from tracker.serializers.story import StoryOutput +from tracker.responses.base import Base +from tracker.responses.story import StoryOutput class BugBase(Base): diff --git a/tracker/serializers/story.py b/tracker/responses/story.py similarity index 85% rename from tracker/serializers/story.py rename to tracker/responses/story.py index 8e6c8a9..921246c 100644 --- a/tracker/serializers/story.py +++ b/tracker/responses/story.py @@ -2,7 +2,7 @@ from pydantic import ConfigDict -from tracker.serializers.base import Base +from tracker.responses.base import Base class StoryInput(Base): diff --git a/tracker/views/bug.py b/tracker/routers/bug.py similarity index 71% rename from tracker/views/bug.py rename to tracker/routers/bug.py index 43fddf4..e4415f1 100644 --- a/tracker/views/bug.py +++ b/tracker/routers/bug.py @@ -4,7 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from tracker.db.session import get_db -from tracker.serializers.bug import BugInput, BugOutput +from tracker.responses.bug import BugInput, BugOutput from tracker.services.bug import bug_by_id, bugs from tracker.services.bug import create as create_bug @@ -13,9 +13,7 @@ @router.post("/bugs/", response_model=BugOutput) async def create(bug: BugInput, db: Annotated[AsyncSession, Depends(get_db)]): - obj = await create_bug(db=db, bug=bug) - - return obj + return BugOutput.model_validate(await create_bug(db=db, bug=bug)) @router.get("/bugs/", response_model=list[BugOutput]) @@ -26,7 +24,5 @@ async def get( @router.get("/bugs/{id}", response_model=BugOutput) -async def get_bug(id: int, db: Annotated[AsyncSession, Depends(get_db)]): - obj = await bug_by_id(db=db, id=id) - - return obj +async def get_by_id(id: int, db: Annotated[AsyncSession, Depends(get_db)]): + return BugOutput.model_validate(await bug_by_id(db=db, id=id)) diff --git a/tracker/views/story.py b/tracker/routers/story.py similarity index 81% rename from tracker/views/story.py rename to tracker/routers/story.py index b0c0f79..4d6d92b 100644 --- a/tracker/views/story.py +++ b/tracker/routers/story.py @@ -4,7 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from tracker.db.session import get_db -from tracker.serializers.story import StoryInput, StoryOutput +from tracker.responses.story import StoryInput, StoryOutput from tracker.services.story import create as create_story from tracker.services.story import stories, story_by_id @@ -13,9 +13,7 @@ @router.post("/stories/", response_model=StoryOutput) async def create(story: StoryInput, db: Annotated[AsyncSession, Depends(get_db)]): - obj = await create_story(db=db, story=story) - - return obj + return await create_story(db=db, story=story) @router.get("/stories/", response_model=list[StoryOutput]) @@ -27,6 +25,4 @@ async def get_stories( @router.get("/stories/{id}", response_model=StoryOutput) async def get_story(id: int, db: Annotated[AsyncSession, Depends(get_db)]): - obj = await story_by_id(db=db, id=id) - - return obj + return StoryOutput.model_validate(await story_by_id(db=db, id=id)) diff --git a/tracker/services/bug.py b/tracker/services/bug.py index 9e20635..a851d3d 100644 --- a/tracker/services/bug.py +++ b/tracker/services/bug.py @@ -1,54 +1,37 @@ -from http import HTTPStatus +from typing import Sequence -from fastapi import HTTPException -from fastapi.encoders import jsonable_encoder +from fastapi import HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.future import select from tracker.models.bug import Bug -from tracker.serializers.bug import BugInput, BugOutput +from tracker.responses.bug import BugInput, BugOutput from tracker.services.story import story_by_id -async def bug_by_id(db: AsyncSession, id: int) -> BugOutput | HTTPException: +async def bug_by_id(db: AsyncSession, id: int) -> Bug: query = await db.execute(select(Bug).filter(Bug.id == id)) res = query.scalars().all() if len(res) > 0: return res[0] - raise HTTPException(status_code=HTTPStatus.NOT_FOUND) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) -def bug_output(bug: Bug) -> BugOutput: - story = jsonable_encoder(obj=bug.story) - bug_output = BugOutput( - id=int(bug.id), - title=bug.title, - description=bug.description, - story=story, - story_id=bug.story_id, - created_at=bug.created_at, - updated_at=bug.updated_at, - ) +async def bugs(db: AsyncSession, skip: int = 0, limit: int = 10) -> Sequence[BugOutput]: + query = await db.execute(select(Bug).offset(skip).limit(limit=limit)) - return bug_output - - -async def bugs(db: AsyncSession, skip: int = 0, limit: int = 10) -> list[BugOutput]: - bugs = await db.scalars(select(Bug).offset(offset=skip).limit(limit=limit)) - output = [] - - for bug in bugs: - output.append(bug_output(bug=bug)) - - return output + return query.scalars().all() async def create(db: AsyncSession, bug: BugInput) -> BugOutput: - story = await story_by_id(db=db, id=bug.story_id) + response = await story_by_id(db=db, id=bug.story_id) + + if isinstance(response, HTTPException): + raise response - obj = Bug(**bug.model_dump(), story=story) + obj = Bug(**bug.model_dump(), story=response) db.add(obj) await db.commit() diff --git a/tracker/services/story.py b/tracker/services/story.py index cf59ad0..f276402 100644 --- a/tracker/services/story.py +++ b/tracker/services/story.py @@ -1,12 +1,11 @@ -from http import HTTPStatus from typing import Sequence -from fastapi import HTTPException +from fastapi import HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.future import select from tracker.models.story import Story -from tracker.serializers.story import StoryInput, StoryOutput +from tracker.responses.story import StoryInput, StoryOutput async def stories( @@ -17,14 +16,14 @@ async def stories( return query.scalars().all() -async def story_by_id(db: AsyncSession, id: int) -> StoryOutput | HTTPException: +async def story_by_id(db: AsyncSession, id: int) -> Story: query = await db.execute(select(Story).where(Story.id == id)) stories = query.scalars().all() if len(stories) > 0: return stories[0] - raise HTTPException(status_code=HTTPStatus.NOT_FOUND) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) async def create(db: AsyncSession, story: StoryInput) -> StoryOutput: @@ -34,4 +33,4 @@ async def create(db: AsyncSession, story: StoryInput) -> StoryOutput: await db.commit() await db.refresh(story_new) - return story_new + return StoryOutput.model_validate(story_new) From f2cd4651ee960d1459fcbf78593fb0320d9a1742 Mon Sep 17 00:00:00 2001 From: Masum Billal Date: Sun, 16 Jun 2024 21:19:53 +0600 Subject: [PATCH 3/3] fixed codecov --- codecov.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/codecov.yml b/codecov.yml index 6e213c3..808ece7 100644 --- a/codecov.yml +++ b/codecov.yml @@ -3,9 +3,6 @@ coverage: round: up range: 90...100 status: - patch: - default: - target: 90% project: default: target: 90%