generated from MinBZK/python-project-template
-
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
1393a5e
commit c247982
Showing
13 changed files
with
284 additions
and
25 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
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,38 @@ | ||
"""drop users table | ||
Revision ID: 22298f3aac77 | ||
Revises: 7f20f8562007 | ||
Create Date: 2024-11-12 09:33:50.853310 | ||
""" | ||
|
||
from collections.abc import Sequence | ||
|
||
import sqlalchemy as sa | ||
from alembic import op | ||
|
||
# revision identifiers, used by Alembic. | ||
revision: str = "22298f3aac77" | ||
down_revision: str | None = "6581a03aabec" | ||
branch_labels: str | Sequence[str] | None = None | ||
depends_on: str | Sequence[str] | None = None | ||
|
||
|
||
def upgrade() -> None: | ||
with op.batch_alter_table("task", schema=None) as batch_op: | ||
batch_op.drop_constraint("fk_task_user_id_user", type_="foreignkey") | ||
op.drop_column("task", "user_id") | ||
op.drop_table("user") | ||
|
||
|
||
def downgrade() -> None: | ||
op.create_table( | ||
"user", | ||
sa.Column("id", sa.INTEGER(), nullable=False), | ||
sa.Column("name", sa.VARCHAR(length=255), nullable=False), | ||
sa.Column("avatar", sa.VARCHAR(length=255), nullable=True), | ||
sa.PrimaryKeyConstraint("id", name="pk_user"), | ||
) | ||
op.add_column("task", sa.Column("user_id", sa.INTEGER(), nullable=True)) | ||
with op.batch_alter_table("task", schema=None) as batch_op: | ||
batch_op.create_foreign_key("fk_task_user_id_user", "user", ["user_id"], ["id"]) |
37 changes: 37 additions & 0 deletions
37
amt/migrations/versions/69243fd24222_create_user_table_with_uuid_as_pk.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,37 @@ | ||
"""create user table with uuid as pk | ||
Revision ID: 69243fd24222 | ||
Revises: 22298f3aac77 | ||
Create Date: 2024-11-12 09:49:53.558089 | ||
""" | ||
|
||
from collections.abc import Sequence | ||
|
||
import sqlalchemy as sa | ||
from alembic import op | ||
|
||
# revision identifiers, used by Alembic. | ||
revision: str = "69243fd24222" | ||
down_revision: str | None = "22298f3aac77" | ||
branch_labels: str | Sequence[str] | None = None | ||
depends_on: str | Sequence[str] | None = None | ||
|
||
|
||
def upgrade() -> None: | ||
op.create_table( | ||
"user", | ||
sa.Column("id", sa.UUID(), nullable=False), | ||
sa.Column("name", sa.String(), nullable=False), | ||
sa.PrimaryKeyConstraint("id", name=op.f("pk_user")), | ||
) | ||
op.add_column("task", sa.Column("user_id", sa.UUID(), nullable=True)) | ||
with op.batch_alter_table("task", schema=None) as batch_op: | ||
batch_op.create_foreign_key(op.f("fk_task_user_id_user"), "user", ["user_id"], ["id"]) | ||
|
||
|
||
def downgrade() -> None: | ||
with op.batch_alter_table("task", schema=None) as batch_op: | ||
batch_op.drop_constraint(op.f("fk_task_user_id_user"), type_="foreignkey") | ||
op.drop_column("task", "user_id") | ||
op.drop_table("user") |
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
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,55 @@ | ||
import logging | ||
from typing import Annotated | ||
from uuid import UUID | ||
|
||
from fastapi import Depends | ||
from sqlalchemy import select | ||
from sqlalchemy.exc import NoResultFound, SQLAlchemyError | ||
from sqlalchemy.ext.asyncio import AsyncSession | ||
|
||
from amt.core.exceptions import AMTRepositoryError | ||
from amt.models.user import User | ||
from amt.repositories.deps import get_session | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class UsersRepository: | ||
""" | ||
The UsersRepository provides access to the repository layer. | ||
""" | ||
|
||
def __init__(self, session: Annotated[AsyncSession, Depends(get_session)]) -> None: | ||
self.session = session | ||
|
||
async def find_by_id(self, id: UUID) -> User | None: | ||
""" | ||
Returns the user with the given id. | ||
:param id: the id of the user to find | ||
:return: the user with the given id or an exception if no user was found | ||
""" | ||
statement = select(User).where(User.id == id) | ||
try: | ||
return (await self.session.execute(statement)).scalars().one() | ||
except NoResultFound: | ||
return None | ||
|
||
async def upsert(self, user: User) -> User: | ||
""" | ||
Upserts (create or update) a user. | ||
:param user: the user to upsert. | ||
:return: the upserted user. | ||
""" | ||
try: | ||
existing_user = await self.find_by_id(user.id) | ||
if existing_user: | ||
existing_user.name = user.name | ||
else: | ||
self.session.add(user) | ||
await self.session.commit() | ||
except SQLAlchemyError as e: # pragma: no cover | ||
logger.exception("Error saving user") | ||
await self.session.rollback() | ||
raise AMTRepositoryError from e | ||
|
||
return user |
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,25 @@ | ||
import logging | ||
from typing import Annotated | ||
from uuid import UUID | ||
|
||
from fastapi import Depends | ||
|
||
from amt.models.user import User | ||
from amt.repositories.users import UsersRepository | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class UsersService: | ||
def __init__( | ||
self, | ||
repository: Annotated[UsersRepository, Depends(UsersRepository)], | ||
) -> None: | ||
self.repository = repository | ||
|
||
async def get(self, id: str | UUID) -> User | None: | ||
id = UUID(id) if isinstance(id, str) else id | ||
return await self.repository.find_by_id(id) | ||
|
||
async def create_or_update(self, user: User) -> User: | ||
return await self.repository.upsert(user) |
This file was deleted.
Oops, something went wrong.
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
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,50 @@ | ||
from unittest.mock import AsyncMock | ||
|
||
import pytest | ||
from amt.core.exceptions import AMTRepositoryError | ||
from amt.repositories.users import UsersRepository | ||
from sqlalchemy.exc import SQLAlchemyError | ||
from tests.constants import default_user | ||
from tests.database_test_utils import DatabaseTestUtils | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_find_by_id(db: DatabaseTestUtils): | ||
await db.given([default_user()]) | ||
users_repository = UsersRepository(db.get_session()) | ||
result = await users_repository.find_by_id(default_user().id) | ||
assert result is not None | ||
assert result.id == default_user().id | ||
assert result.name == default_user().name | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_upsert_new(db: DatabaseTestUtils): | ||
new_user = default_user() | ||
users_repository = UsersRepository(db.get_session()) | ||
await users_repository.upsert(new_user) | ||
result = await users_repository.find_by_id(new_user.id) | ||
assert result is not None | ||
assert result.id == new_user.id | ||
assert result.name == new_user.name | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_upsert_existing(db: DatabaseTestUtils): | ||
await db.given([default_user()]) | ||
new_user = default_user(name="John Smith New") | ||
users_repository = UsersRepository(db.get_session()) | ||
await users_repository.upsert(new_user) | ||
result = await users_repository.find_by_id(new_user.id) | ||
assert result is not None | ||
assert result.id == new_user.id | ||
assert result.name == new_user.name | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_upsert_error(db: DatabaseTestUtils): | ||
new_user = default_user(name="John Smith New") | ||
users_repository = UsersRepository(db.get_session()) | ||
users_repository.find_by_id = AsyncMock(side_effect=SQLAlchemyError("Database error")) | ||
with pytest.raises(AMTRepositoryError): | ||
await users_repository.upsert(new_user) |
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,46 @@ | ||
from uuid import UUID | ||
|
||
import pytest | ||
from amt.repositories.users import UsersRepository | ||
from amt.services.users import UsersService | ||
from pytest_mock import MockFixture | ||
from tests.constants import default_user | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_get_user(mocker: MockFixture): | ||
# Given | ||
id = UUID("3d284d80-fc47-41ab-9696-fab562bacbd5") | ||
name = "John Smith" | ||
users_service = UsersService( | ||
repository=mocker.AsyncMock(spec=UsersRepository), | ||
) | ||
users_service.repository.find_by_id.return_value = default_user(id=id, name=name) # type: ignore | ||
|
||
# When | ||
user = await users_service.get(id) | ||
|
||
# Then | ||
assert user is not None | ||
assert user.id == id | ||
assert user.name == name | ||
users_service.repository.find_by_id.assert_awaited_once_with(id) # type: ignore | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_create_or_update(mocker: MockFixture): | ||
# Given | ||
user = default_user() | ||
users_service = UsersService( | ||
repository=mocker.AsyncMock(spec=UsersRepository), | ||
) | ||
users_service.repository.upsert.return_value = user # type: ignore | ||
|
||
# When | ||
retreived_user = await users_service.create_or_update(user) | ||
|
||
# Then | ||
assert retreived_user is not None | ||
assert retreived_user.id == user.id | ||
assert retreived_user.name == user.name | ||
users_service.repository.upsert.assert_awaited_once_with(user) # type: ignore |