Skip to content

Commit

Permalink
Dqb 28 choose meal (#41)
Browse files Browse the repository at this point in the history
* DQB-28 Created command to choose from menu

Command can choose first item on the menu or a random item

* DQB-28 Introduce more of select command

Replace command args with flag converter

* DQB-28 Clean up select command code and create tests for controller

Add pytest-mock to pyproject.toml to support rare cases of needing to patch code.

* DQB-28 Upgrade python packages

Run poetry upgrade to upgrade existing installed packages

* DQB-28 Update tests to use factory boy and introduce factories

Install factoryboy into pyproject.toml, and update ruff rules accordingly.
Installed and implemented factory boy through out tests

* DQB-28 Delete broken factory

Maybe clause in factory was not working. will come back to this
  • Loading branch information
jplhanna authored Apr 23, 2024
1 parent 5662842 commit 76c7912
Show file tree
Hide file tree
Showing 16 changed files with 693 additions and 374 deletions.
753 changes: 411 additions & 342 deletions poetry.lock

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ module = "sqlalchemy.*"
ignore_missing_imports = true
module = "furl"

[[tool.mypy.overrides]]
ignore_missing_imports = true
module = "factory.*"

[tool.poetry]
authors = ["jplhanna <[email protected]>"]
description = "Discord bot for tracking and rewarding tasks"
Expand All @@ -73,6 +77,7 @@ typing-extensions = "^4.9.0"
bandit = {extras = ["toml"], version = "^1.7.4"}
black = "*"
coverage = {extras = ["toml"], version = "^6.4.4"}
factory-boy = "^3.3.0"
faker = "*"
flake8-pytest-style = "^1.6.0"
mypy = "*"
Expand All @@ -81,6 +86,8 @@ pytest = "<=7.4.4"
pytest-async-sqlalchemy = "*"
pytest-asyncio = "*"
pytest-cov = "*"
pytest-factoryboy = "^2.7.0"
pytest-mock = "^3.12.0"
pytest-randomly = "*"
ruff = "^0.2.0"

Expand Down Expand Up @@ -132,4 +139,5 @@ lines-between-types = 1
max-complexity = 10

[tool.ruff.lint.per-file-ignores]
"src/helpers/factories/*" = ["FBT001"]
"src/model_hub.py" = ["F401"]
15 changes: 15 additions & 0 deletions src/bot/commands.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
from datetime import date
from typing import cast

from discord import Guild
from discord import Intents
from discord.ext.commands import Bot
from discord.ext.commands import Context
Expand All @@ -12,7 +16,9 @@
from src.bot.controllers import get_quest_list_text
from src.bot.controllers import get_tavern_menu
from src.bot.controllers import remove_from_tavern_menu
from src.bot.controllers import select_from_tavern_menu
from src.bot.controllers import upsert_tavern_menu
from src.bot.typeshed import RandomChoiceFlag
from src.config import DISCORD_OWNER_ID
from src.constants import DayOfWeek

Expand Down Expand Up @@ -91,3 +97,12 @@ async def tavern_menu_add(ctx: Context, *, day_of_week: DayOfWeek, menu_item: st
async def tavern_menu_remove(ctx: Context, *, menu_item: str, day_of_week: DayOfWeek | None) -> None:
res = await remove_from_tavern_menu(ctx, menu_item, day_of_week)
await ctx.send(res)


@tavern_menu.command(name="choose")
@guild_only()
async def tavern_menu_choose(ctx: Context, *, flags: RandomChoiceFlag) -> None:
if not flags.day_of_week:
flags.day_of_week = DayOfWeek(date.today().weekday())
res = await select_from_tavern_menu(cast(Guild, ctx.guild), flags.style, flags.day_of_week)
await ctx.send(res)
1 change: 1 addition & 0 deletions src/bot/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
REGISTER_FIRST_MESSAGE = "Please register first"
NO_MENU_THIS_WEEK_MESSAGE = "No menu has been created for this week."
SERVER_ONLY_BAD_REQUEST_MESSAGE = "Bad request, this feature is only available in servers."
NO_MENU_ITEMS_FOR_CHOSEN_DAY_MESSAGE = "No items to select for the day you asked."
23 changes: 23 additions & 0 deletions src/bot/controllers.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import random

from dependency_injector.wiring import Provide
from dependency_injector.wiring import inject
from discord import Guild
from discord.ext.commands import Context

from src.bot.constants import ALREADY_REGISTERED_MESSAGE
from src.bot.constants import NEW_USER_MESSAGE
from src.bot.constants import NO_MENU_ITEMS_FOR_CHOSEN_DAY_MESSAGE
from src.bot.constants import NO_MENU_THIS_WEEK_MESSAGE
from src.bot.constants import REGISTER_FIRST_MESSAGE
from src.bot.constants import SERVER_ONLY_BAD_REQUEST_MESSAGE
from src.constants import ChooseStyle
from src.constants import DayOfWeek
from src.containers import Container
from src.exceptions import NoIDProvided
Expand Down Expand Up @@ -129,3 +134,21 @@ async def remove_from_tavern_menu(
return f"{item_name_str.capitalize()} could not be found{day_of_week_error_text} in this week's menu."

return "Item successfully removed"


@inject
async def select_from_tavern_menu(
guild: Guild,
style: ChooseStyle,
day_of_week: DayOfWeek,
tavern_service: TavernService = Provide[Container.tavern_service],
) -> str:
menu = await tavern_service.get_this_weeks_menu(guild.id)
if not menu:
return NO_MENU_THIS_WEEK_MESSAGE
if not (food_items := menu.grouped_items.get(day_of_week)):
return NO_MENU_ITEMS_FOR_CHOSEN_DAY_MESSAGE
food_text = food_items[0].food.title()
if style == ChooseStyle.RANDOM:
food_text = random.choice(food_items).food.title() # nosec
return f"Order Up!\n{food_text}"
12 changes: 12 additions & 0 deletions src/bot/typeshed.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
from collections.abc import Callable
from collections.abc import Coroutine
from datetime import date
from typing import TYPE_CHECKING
from typing import Concatenate
from typing import ParamSpec

from discord.ext.commands import FlagConverter
from discord.ext.commands import flag

from src.constants import ChooseStyle
from src.constants import DayOfWeek

if TYPE_CHECKING:
from discord.ext.commands import Context
from discord.ext.commands import HybridCommand
Expand All @@ -22,3 +29,8 @@
]
else:
CommandRegisterType = Callable


class RandomChoiceFlag(FlagConverter):
style: ChooseStyle = ChooseStyle.RANDOM
day_of_week: DayOfWeek = flag(default=lambda _: DayOfWeek(date.today().weekday()))
26 changes: 20 additions & 6 deletions src/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@
from copy import copy
from unittest.mock import AsyncMock
from unittest.mock import MagicMock
from unittest.mock import Mock
from unittest.mock import sentinel

import pytest

from pytest_factoryboy import register
from sqlalchemy import create_engine
from sqlalchemy import inspect
from sqlalchemy.ext.asyncio import async_sessionmaker
from sqlmodel.ext.asyncio.session import AsyncSession

from src.config import Settings
from src.containers import Container
from src.helpers.factories import factory_classes
from src.helpers.factories.base_factories import test_session
from src.helpers.sqlalchemy_helpers import BaseModel
from src.models import User
from src.repositories import BaseRepository
Expand All @@ -28,6 +31,13 @@ def test_config_obj():
return Settings()


@pytest.fixture(scope="session", autouse=True)
def setup_factory_session(test_config_obj):
engine = create_engine(test_config_obj.db.database_uri)
test_session.configure(bind=engine)
return test_session


@pytest.fixture()
def mock_container() -> Generator[Container, None, None]:
mocked_container = copy(base_mock_container)
Expand All @@ -37,14 +47,14 @@ def mock_container() -> Generator[Container, None, None]:
mocked_container.unwire()


@pytest.fixture(scope="session")
def mocked_user() -> User:
return Mock(spec=User, discord_id=sentinel.discord_id, id=sentinel.user_id, _sa_instance_state=MagicMock())
@pytest.fixture()
def mocked_guild() -> MagicMock:
return MagicMock(id=sentinel.guild_id)


@pytest.fixture()
def mocked_ctx() -> MagicMock:
return MagicMock(author=MagicMock(id=sentinel.discord_id), guild=MagicMock(id=sentinel.guild_id))
def mocked_ctx(mocked_guild) -> MagicMock:
return MagicMock(author=MagicMock(id=sentinel.discord_id), guild=mocked_guild)


@pytest.fixture()
Expand Down Expand Up @@ -114,3 +124,7 @@ async def db_user(db_session):
if not inspect(user).was_deleted:
await db_session.delete(user)
await db_session.flush()


for cls in factory_classes:
register(cls)
7 changes: 7 additions & 0 deletions src/constants.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Message Strings
from enum import Enum
from enum import IntEnum
from enum import auto

GOOD_LUCK_ADVENTURER = "You have accepted {}! Good luck adventurer"
QUEST_ALREADY_ACCEPTED = "You have already accepted this request"
Expand All @@ -15,3 +17,8 @@ class DayOfWeek(IntEnum):
THURSDAY = 5
FRIDAY = 6
SATURDAY = 7


class ChooseStyle(Enum):
RANDOM = auto()
FIRST = auto()
4 changes: 2 additions & 2 deletions src/containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ def __init__(self, db_url: str) -> None:
)

async def create_database(self) -> None:
current_session = self.get_session()
await current_session.run_sync(BaseModel.metadata.create_all)
async with self._async_engine.begin() as conn:
await conn.run_sync(BaseModel.metadata.create_all)

def get_session(self) -> AsyncSession:
return self._session_factory()
Expand Down
10 changes: 10 additions & 0 deletions src/helpers/factories/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import inspect
import sys

from .base_factories import BaseFactory
from .quest_factories import * # noqa: F403
from .tavern_factories import * # noqa: F403
from .user_factories import * # noqa: F403

fact_classes = inspect.getmembers(sys.modules[__name__], lambda x: inspect.isclass(x) and issubclass(x, BaseFactory))
factory_classes = [cls for cls_name, cls in fact_classes if cls_name != "BaseFactory"]
15 changes: 15 additions & 0 deletions src/helpers/factories/base_factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from factory.alchemy import SQLAlchemyModelFactory
from sqlalchemy.orm import scoped_session
from sqlalchemy.orm import sessionmaker

from src.config import DBSettings

db_settings = DBSettings()

test_session = scoped_session(sessionmaker())


class BaseFactory(SQLAlchemyModelFactory):
class Meta:
abstract = True
sqlalchemy_session = test_session
47 changes: 47 additions & 0 deletions src/helpers/factories/quest_factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from typing import Any

import factory

from pytest_factoryboy import register

from src.helpers.factories.base_factories import BaseFactory
from src.helpers.factories.user_factories import UserFactory
from src.quests import ExperienceTransaction
from src.quests import Quest
from src.quests import UserQuest


@register
class QuestFactory(BaseFactory):
id = factory.Sequence(lambda n: n + 1)

class Meta:
model = Quest

name = "Test Quest"
experience = 100


@register
class UserQuestFactory(BaseFactory):
class Meta:
model = UserQuest

date_completed = factory.Maybe("is_completed", yes_declaration=factory.Faker("date_between", start_date="-10d"))

user = factory.SubFactory(UserFactory)
quest = factory.SubFactory(QuestFactory)

@factory.post_generation
def append_to_quests(self, _create: bool, _extracted: Any, **_kwargs: Any) -> None:
self.user.quests.append(self.quest)


@register
class ExperienceTransactionFactory(BaseFactory):
class Meta:
model = ExperienceTransaction

quest = factory.SubFactory(QuestFactory)
experience = factory.SelfAttribute("quest.experience")
user = factory.SubFactory(UserFactory)
30 changes: 30 additions & 0 deletions src/helpers/factories/tavern_factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from unittest.mock import sentinel

import factory

from pytest_factoryboy import register

from src.constants import DayOfWeek
from src.helpers.factories.base_factories import BaseFactory
from src.tavern import Menu
from src.tavern.models import MenuItem


@register
class MenuItemFactory(BaseFactory):
class Meta:
model = MenuItem

food = factory.Sequence(lambda n: f"Test Food {n}")
day_of_the_week = factory.Iterator(DayOfWeek)
menu = None


@register
class MenuFactory(BaseFactory):
class Meta:
model = Menu

server_id = sentinel.guild_id
start_date = factory.Faker("date_object")
items = factory.RelatedFactoryList(MenuItemFactory, factory_related_name="menu")
16 changes: 16 additions & 0 deletions src/helpers/factories/user_factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import factory

from pytest_factoryboy import register

from src.helpers.factories.base_factories import BaseFactory
from src.models import User


@register
class UserFactory(BaseFactory):
id = factory.Sequence(lambda n: n + 1)

class Meta:
model = User

discord_id = factory.Faker("random_number", digits=10)
Loading

0 comments on commit 76c7912

Please sign in to comment.