Skip to content

Commit

Permalink
BREAKING CHANGE: 구조 리팩토링 (#49)
Browse files Browse the repository at this point in the history
* feat!: structure for clean architecture

* feat: make some use case for user

* feat: implement sqlalchemy infra

* docs: add diagram

Signed-off-by: Ryu Juheon <[email protected]>

* feat: meal and school entity

* feat: discord_id column type is string

* feat: neispy를 이용한 일부 기초적인 구조

* feat: 애플리케이션

* feat: 인수 변경

* test: 일부 테스트 코드 추가

* feat: 클라이언트 기초 구조

* feat: 에러 핸들러 구조

* feat: 개념증명

* feat: 타입힌트, 급식 명령어 일부 구현

* fix: fix string typos

* style: apply black

* fix: fix typo

* feat: register string

* style: apply isort

* refactor: use future annotations

* fix: fix docstrings

* feat(error): SchoolInfoNotFound handler

* feat:  학교 설정 추가

* style: change func name

* feat: preferences

* style: apply black and isort

* fix(test): fix test_user TypeError

* feat: timetable 구현

* style: apply isort and black

* test(schoolinfo): edit entity

* fix: change attribute sideeffect

* refactor: Strings

* feat: handle more exceptions

* style: apply black

* style: add type ignore

* feat(exceptions): use message

* test: fix user

* feat: 학과 계열 분리

* fix: typo

* docs: remove graph

* style: remove unused import

* fix(type): untyped function

* refactor: use Strings

* fix: address unhandled exceptions

* refactor: remove unused exception

* fix: address unhandled exceptions

* fix: wrong message in exception

* feat: preference command

* feat: 의견사항 적용

* feat: 데이터베이스 마이그레이션

* refactor: 페이지네이터

Co-authored-by: Starcea / 스타샤 <[email protected]>

* feat: profile command

* refactor(profile): refactor codes

* style: apply black and isort

* refactor: 프로필 커맨드 리팩토링

* style: apply isort

* style: apply isort

* fix(strings): organize and fix typo

* fix: typo

* fix: handle MealNotFound exception

* fix(timetable): show selected dates in embed

* fix(config): load config from json

* fix(neispy): start neispy with key

* feat: embed 분리

* feat: raise school info

* feat: Strings 나눔

* feat: 누락된것 수정

* refactor(timetable): use only get_timetable

* style: apply black and isort

* style: apply isort

* docs: reformat develop README

* fix: typo

* deps: update neispy

* refactor: 조식이 기본값으로 되게 변경

* chore: docker compose for deploy

* style: apply code style

---------

Signed-off-by: Ryu Juheon <[email protected]>
Co-authored-by: Starcea <[email protected]>
Co-authored-by: cuizzang <[email protected]>
  • Loading branch information
3 people authored Dec 16, 2023
1 parent 2a20654 commit cd9c13c
Show file tree
Hide file tree
Showing 200 changed files with 3,606 additions and 2,326 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@ jobs:
- name: Install dependencies
run: poetry install --no-interaction --with type
- name: Check type
run: poetry run pyright
- name: Check untyped function
run: poetry run mypy ./crenata

test:
runs-on: ubuntu-latest
services:
Expand Down
5 changes: 4 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"python.poetryPath": "poetry",
"python.analysis.typeCheckingMode": "strict",
"python.formatting.provider": "black",
"python.formatting.provider": "none",
"python.linting.mypyEnabled": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true
Expand Down Expand Up @@ -30,4 +30,7 @@
"sqlalchemy"
],
"python.linting.enabled": false,
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter"
},
}
21 changes: 5 additions & 16 deletions crenata/__main__.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,15 @@
from argparse import ArgumentParser
from sys import argv

from crenata.argparser import parse_args
from crenata.config import CrenataConfig
from crenata.discord.client import create_client
from crenata.discord.commands import load_commands
from crenata.discord.events.error import on_error
from discord import Intents, Object
from discord import Intents

from crenata.application import create_app
from crenata.infrastructure.utils.argparser import parse_args

if __name__ == "__main__":
config = CrenataConfig()
parser = ArgumentParser("crenata")
args = parse_args(parser, argv[1:])
config.update_with_args(args)

client = create_client(config, intents=Intents.default())
client = create_app(args, intents=Intents.default())

commands = load_commands("crenata/discord/commands/**/*.py")
for command in commands:
if config.PRODUCTION:
client.tree.add_command(command)
else:
client.tree.add_command(command, guild=Object(config.TEST_GUILD_ID))
setattr(client.tree, "on_error", on_error)
client.run()
5 changes: 0 additions & 5 deletions crenata/abc/domain.py

This file was deleted.

27 changes: 27 additions & 0 deletions crenata/application/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from argparse import Namespace

from discord import Intents

from crenata.application.client import Crenata
from crenata.application.commands.exit import exit
from crenata.application.commands.preferences import preferences
from crenata.application.commands.profile import profile
from crenata.application.commands.register import register
from crenata.application.commands.school import school
from crenata.application.error.callback import error_handler


def create_app(args: Namespace, intents: Intents) -> Crenata:
crenata = Crenata(intents=intents)

crenata.config.update_with_args(args)

crenata.tree.set_error_handler(error_handler)

crenata.tree.add_command(register)
crenata.tree.add_command(profile)
crenata.tree.add_command(exit)
crenata.tree.add_command(school)
crenata.tree.add_command(preferences)

return crenata
File renamed without changes.
56 changes: 56 additions & 0 deletions crenata/application/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from __future__ import annotations

from typing import Any

from discord import Client, Intents, Object
from neispy import Neispy

from crenata.application.tree import CrenataCommandTree
from crenata.infrastructure.sqlalchemy import Database
from crenata.infrastructure.utils.config import CrenataConfig


class Crenata(Client):
def __init__(self, intents: Intents, *args: Any, **kwargs: Any) -> None:
super().__init__(intents=intents, *args, **kwargs)

self.tree = CrenataCommandTree(self)
self.config = CrenataConfig()

async def startup(self) -> None:
self.neispy = Neispy(self.config.NEIS_API_KEY)
self.database = await Database.setup(self.config.DB_URL)

async def closeup(self) -> None:
if self.neispy.session and not self.neispy.session.closed:
await self.neispy.session.close()

if getattr(self.database, "database", None):
await self.database.engine.dispose()

async def setup_hook(self) -> None:
if self.config.PRODUCTION:
await self.tree.sync()

else:
await self.tree.sync(guild=Object(self.config.TEST_GUILD_ID))

async def close(self) -> None:
await self.closeup()

return await super().close()

async def start(self, token: str, *, reconnect: bool = True) -> None:
await self.startup()

return await super().start(token, reconnect=reconnect)

def run(self, *args: Any, **kwargs: Any) -> None:
"""
Crenata를 실행합니다.
토큰은 Config에서 로드하기 때문에 인자로 줄 필요가 없습니다.
"""
kwargs.update({"token": self.config.TOKEN})

return super().run(*args, **kwargs)
File renamed without changes.
43 changes: 43 additions & 0 deletions crenata/application/commands/exit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from discord import Interaction, app_commands

from crenata.application.client import Crenata
from crenata.application.embeds.exit import exit_embed_builder
from crenata.application.strings import ApplicationStrings
from crenata.application.utils import InteractionLock
from crenata.application.view.confirm import Confirm
from crenata.core.user.usecases.delete import DeleteUserUseCase
from crenata.core.user.usecases.get import GetUserUseCase
from crenata.infrastructure.sqlalchemy.user.domain.repository import UserRepositoryImpl


@app_commands.command(name="탈퇴", description="탈퇴합니다.")
async def exit(interaction: Interaction[Crenata]) -> None:
async with InteractionLock(interaction):
user_repository = UserRepositoryImpl(interaction.client.database)
get_user_usecase = GetUserUseCase(user_repository)

user = await get_user_usecase.execute(interaction.user.id)

embed = exit_embed_builder()

view = Confirm(interaction.user.id)

await interaction.response.send_message(embed=embed, view=view, ephemeral=True)

if not await view.wait():
if view.is_confirm:
delete_user_usecase = DeleteUserUseCase(user_repository)

await delete_user_usecase.execute(user)

await interaction.edit_original_response(
content=ApplicationStrings.UNREGISTER_COMPLETED,
embed=None,
view=None,
)

return

await interaction.edit_original_response(
content=ApplicationStrings.USER_CANCELLED, embed=None, view=None
)
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
from discord import app_commands

from crenata.application.commands.preferences.edit import edit

preferences = app_commands.Group(name="환경설정", description="환경설정 관련 명령어입니다.")

preferences.add_command(edit)
34 changes: 34 additions & 0 deletions crenata/application/commands/preferences/edit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from discord import app_commands
from discord.interactions import Interaction

from crenata.application.client import Crenata
from crenata.application.strings import ApplicationStrings
from crenata.application.utils import InteractionLock
from crenata.core.preferences.domain.entity import Preferences
from crenata.core.preferences.usecases.update import UpdatePreferencesUseCase
from crenata.infrastructure.sqlalchemy.preferences.domain.repository import (
PreferencesRepositoryImpl,
)


@app_commands.command(name="변경", description="환경설정을 변경합니다.")
@app_commands.describe(private="학교 이름을 비공개로 할지 여부입니다.")
@app_commands.describe(ephemeral="자기 자신에게만 보이게 할지 여부입니다.")
async def edit(
interaction: Interaction[Crenata], private: bool, ephemeral: bool
) -> None:
async with InteractionLock(interaction):
preferences_repository = PreferencesRepositoryImpl(interaction.client.database)
update_preferences_usecase = UpdatePreferencesUseCase(preferences_repository)

await update_preferences_usecase.execute(
interaction.user.id,
Preferences(
private,
ephemeral,
),
)

await interaction.response.send_message(
content=ApplicationStrings.PREFERENCE_EDITED, ephemeral=True
)
26 changes: 26 additions & 0 deletions crenata/application/commands/profile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from discord import Interaction, app_commands

from crenata.application.client import Crenata
from crenata.application.embeds.profile import profile_embed_builder
from crenata.core.user.usecases.get import GetUserUseCase
from crenata.infrastructure.sqlalchemy.user.domain.repository import UserRepositoryImpl


@app_commands.command(name="프로필", description="내 프로필을 확인합니다.")
async def profile(interaction: Interaction[Crenata]) -> None:
user_repository = UserRepositoryImpl(interaction.client.database)
get_user_usecase = GetUserUseCase(user_repository)

user = await get_user_usecase.execute(interaction.user.id)

is_private = user.preferences.private
is_empheral = user.preferences.ephemeral

embed = profile_embed_builder(
interaction.user,
user.school_info,
is_private,
is_empheral,
)

await interaction.response.send_message(embed=embed, ephemeral=is_empheral)
19 changes: 19 additions & 0 deletions crenata/application/commands/register.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from discord import Embed, Interaction, app_commands

from crenata.application.client import Crenata
from crenata.application.strings import ApplicationStrings
from crenata.core.user.domain.entity import User
from crenata.core.user.usecases.create import CreateUserUseCase
from crenata.infrastructure.sqlalchemy.user.domain.repository import UserRepositoryImpl


@app_commands.command(name="가입", description="가입합니다.")
async def register(interaction: Interaction[Crenata]) -> None:
user_repository = UserRepositoryImpl(interaction.client.database)
create_user_usecase = CreateUserUseCase(user_repository)

await create_user_usecase.execute(User.default(interaction.user.id))

embed = Embed(title=ApplicationStrings.REGISTER_COMPLETED)

await interaction.response.send_message(embed=embed, ephemeral=True)
15 changes: 15 additions & 0 deletions crenata/application/commands/school/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from discord import app_commands

from crenata.application.commands.school.meal import meal
from crenata.application.commands.school.search import search
from crenata.application.commands.school.setup import setup
from crenata.application.commands.school.timetable import timetable
from crenata.application.commands.school.users import users

school = app_commands.Group(name="학교", description="학교 관련 명령어입니다.")

school.add_command(meal)
school.add_command(search)
school.add_command(setup)
school.add_command(users)
school.add_command(timetable)
87 changes: 87 additions & 0 deletions crenata/application/commands/school/meal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from datetime import datetime
from typing import Literal, Optional

from discord import app_commands, ui
from discord.interactions import Interaction
from neispy.utils import KST

from crenata.application.client import Crenata
from crenata.application.embeds.meal import meal_embed_builder
from crenata.application.interaction import school_page
from crenata.application.utils import ToDatetime, respond
from crenata.core.meal.exceptions import MealNameNotFound
from crenata.core.meal.usecases.get import GetMealUseCase
from crenata.core.school.usecases.get import GetSchoolUseCase
from crenata.core.schoolinfo.exceptions import SchoolInfoNotFound
from crenata.core.user.usecases.get import GetUserUseCase
from crenata.infrastructure.neispy.meal.domain.repository import MealRepositoryImpl
from crenata.infrastructure.neispy.school.domain.repository import SchoolRepositoryImpl
from crenata.infrastructure.sqlalchemy.user.domain.repository import UserRepositoryImpl


class AllergyUI(ui.Select[ui.View]):
def __init__(self, executor_id: int) -> None:
super().__init__(placeholder="알러지 정보")
self.executor_id = executor_id
self.add_option(label="1.난류, 2.우유, 3.메밀")
self.add_option(label="4.땅콩, 5.대두, 6.밀")
self.add_option(label="7.고등어, 8.게, 9.새우")
self.add_option(label="10.돼지고기, 11.복숭아, 12.토마토")
self.add_option(label="13.아황산염, 14.호두, 15.닭고기")
self.add_option(label="16.쇠고기, 17.오징어, 18.조개류")

async def callback(self, interaction: Interaction) -> None:
if self.executor_id == interaction.user.id:
self.placeholder = self.values[0]
await interaction.response.edit_message(view=self.view)


@app_commands.command(name="급식", description="급식 식단표를 가져옵니다.")
async def meal(
interaction: Interaction[Crenata],
school_name: Optional[str] = None,
meal_time: Literal["조식", "중식", "석식"] = "중식",
date: Optional[app_commands.Transform[datetime, ToDatetime]] = None,
) -> None:
if date is None:
date = datetime.now(tz=KST)

if school_name is None:
user_repository = UserRepositoryImpl(interaction.client.database)
get_user_usecase = GetUserUseCase(user_repository)

user = await get_user_usecase.execute(interaction.user.id)

if user.school_info is None:
raise SchoolInfoNotFound

school_info = user.school_info
is_private = user.preferences.private

else:
school_repository = SchoolRepositoryImpl(interaction.client.neispy)
get_school_usecase = GetSchoolUseCase(school_repository)

school_infos = await get_school_usecase.execute(school_name)

school_info = await school_page(interaction, school_infos)

is_private = True

meal_repository = MealRepositoryImpl(interaction.client.neispy)
get_meal_usecase = GetMealUseCase(meal_repository)

meal = await get_meal_usecase.execute(
school_info.edu_office_code, school_info.standard_school_code, date, meal_time
)

if not meal:
raise MealNameNotFound

embed = meal_embed_builder(meal, is_private)

view = ui.View()
select_allergy_ui = AllergyUI(interaction.user.id)
view.add_item(select_allergy_ui)

await respond(interaction, content=None, embed=embed, view=view)
Loading

0 comments on commit cd9c13c

Please sign in to comment.