Skip to content

Commit

Permalink
Add a way to export features information to a json file (#144)
Browse files Browse the repository at this point in the history
  • Loading branch information
AiroPi authored Mar 23, 2024
1 parent fee6ec6 commit a8cd213
Show file tree
Hide file tree
Showing 12 changed files with 95 additions and 61 deletions.
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ ENV PYTHONUNBUFFERED=0


FROM base as production
CMD ["/bin/sh", "-c", "alembic upgrade head && python ./main.py bot --sync -c ./config.toml"]
CMD ["/bin/sh", "-c", "alembic upgrade head && python ./main.py run --sync -c ./config.toml"]


FROM base as debug
ENV DEBUG=1
ENV LOG_LEVEL=DEBUG
RUN pip install debugpy
CMD ["/bin/sh", "-c", "alembic upgrade head && python -m debugpy --wait-for-client --listen 0.0.0.0:5678 ./main.py bot -c ./config.toml"]
CMD ["/bin/sh", "-c", "alembic upgrade head && python -m debugpy --wait-for-client --listen 0.0.0.0:5678 ./main.py run -c ./config.toml"]
4 changes: 4 additions & 0 deletions bin/export_commands.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
echo "Building the Docker image..."
docker compose --progress quiet build mybot
echo "Exporting the json file..."
docker compose --progress quiet run --rm -t -v "${PWD}:/app" mybot python3 ./src/main.py export-features
2 changes: 1 addition & 1 deletion requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ asyncpg
sqlalchemy[asyncio]
typing_extensions
discord.py
click
psutil
alembic
two048 # a personal library
topggpy==2.0.0a0
lingua-language-detector==1.3.4 # WAITFOR: https://github.com/pemistahl/lingua-py/issues/213
aiohttp
python-dateutil
typer[all]
17 changes: 16 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ asyncpg==0.29.0
attrs==23.2.0
# via aiohttp
click==8.1.7
# via -r requirements.in
# via typer
colorama==0.4.6
# via typer
discord-py==2.3.2
# via -r requirements.in
frozenlist==1.4.1
Expand All @@ -33,8 +35,12 @@ lingua-language-detector==1.3.4
# via -r requirements.in
mako==1.3.2
# via alembic
markdown-it-py==3.0.0
# via rich
markupsafe==2.1.5
# via mako
mdurl==0.1.2
# via markdown-it-py
multidict==6.0.5
# via
# aiohttp
Expand All @@ -43,10 +49,16 @@ numpy==1.26.4
# via lingua-language-detector
psutil==5.9.8
# via -r requirements.in
pygments==2.17.2
# via rich
python-dateutil==2.9.0.post0
# via -r requirements.in
regex==2023.12.25
# via lingua-language-detector
rich==13.7.1
# via typer
shellingham==1.5.4
# via typer
six==1.16.0
# via python-dateutil
sqlalchemy[asyncio]==2.0.28
Expand All @@ -57,11 +69,14 @@ topggpy==2.0.0a0
# via -r requirements.in
two048==1.0.2
# via -r requirements.in
typer[all]==0.9.0
# via -r requirements.in
typing-extensions==4.10.0
# via
# -r requirements.in
# alembic
# sqlalchemy
# typer
wheel==0.43.0
# via -r requirements.in
yarl==1.9.4
Expand Down
4 changes: 2 additions & 2 deletions src/cogs/help.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@
from discord.app_commands import Choice, locale_str as __
from discord.utils import get

from commands_exporter import ContextCommand, FeatureType, Misc, MiscCommandsType, SlashCommand
from core import ExtendedCog, ResponseType, response_constructor
from core.constants import Emojis
from core.i18n import _
from core.utils import splitter
from features_exporter import ContextCommand, FeatureType, Misc, MiscCommandsType, SlashCommand

if TYPE_CHECKING:
from discord import Embed, Interaction

from commands_exporter import Feature
from features_exporter import Feature
from mybot import MyBot


Expand Down
4 changes: 4 additions & 0 deletions src/cogs/translate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,10 @@ def __init__(self, bot: MyBot):
)
)

async def cog_unload(self) -> None:
for translator in self.translators:
await translator.close()

async def public_translations(self, guild_id: int | None):
if guild_id is None: # we are in private channels, IG
return True
Expand Down
3 changes: 3 additions & 0 deletions src/cogs/translate/adapters/libretranslate.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ def __init__(self):
.build()
)

async def close(self):
await self.instance.close()

async def available_languages(self) -> Languages:
return Languages(x.value for x in language_to_libre)

Expand Down
3 changes: 2 additions & 1 deletion src/core/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import logging
import tomllib
from pathlib import Path
from typing import Any, ClassVar, Self

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -59,7 +60,7 @@ def __getattribute__(self, name: str) -> Any:
return None


def define_config(config_path: str | None = None, **kwargs: Any):
def define_config(config_path: Path | str | None = None, **kwargs: Any):
if config_path:
with open(config_path, encoding="utf-8") as f:
kwargs |= tomllib.load(f.buffer)
Expand Down
2 changes: 1 addition & 1 deletion src/core/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def _identity[T](x: T) -> T:


class Emoji(str):
__slots__ = ("id",)
__slots__ = ("_id",)

def __new__(cls, id: Snowflake) -> Self:
return super().__new__(cls, f"<:_:{id}>")
Expand Down
59 changes: 34 additions & 25 deletions src/commands_exporter.py → src/features_exporter.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
from __future__ import annotations

import asyncio
import gettext
import json
from dataclasses import asdict, dataclass, field, is_dataclass
from enum import Enum
from json import JSONEncoder
from pathlib import Path
from typing import TYPE_CHECKING, Any, NotRequired, TypedDict, cast, overload

import discord
from discord import app_commands

from core._config import define_config
from core.extended_commands import MiscCommand, MiscCommandsType
from core.i18n import translations

if TYPE_CHECKING:
from mybot import MyBot
Expand Down Expand Up @@ -79,15 +80,24 @@ def fill_features(
child: app_commands.Group | app_commands.Command[Any, ..., Any],
features: list[SlashCommand],
parent: SlashCommand,
translations: gettext.GNUTranslations | gettext.NullTranslations = ...,
) -> None: ...


@overload
def fill_features(child: FeatureCodebaseTypes, features: list[Feature], parent: SlashCommand | None = None) -> None: ...
def fill_features(
child: FeatureCodebaseTypes,
features: list[Feature],
parent: SlashCommand | None = None,
translations: gettext.GNUTranslations | gettext.NullTranslations = ...,
) -> None: ...


def fill_features(
child: FeatureCodebaseTypes, features: list[Feature] | list[SlashCommand], parent: SlashCommand | None = None
child: FeatureCodebaseTypes,
features: list[Feature] | list[SlashCommand],
parent: SlashCommand | None = None,
translations: gettext.GNUTranslations | gettext.NullTranslations = gettext.NullTranslations(),
) -> None:
extras = cast(Extras, child.extras)

Expand Down Expand Up @@ -159,20 +169,32 @@ def shared_kwargs(child: FeatureCodebaseTypes) -> dict[str, Any]:
features.append(feature) # type: ignore


def extract_features(mybot: MyBot) -> list[Feature]:
def extract_features(
mybot: MyBot,
translations: gettext.GNUTranslations | gettext.NullTranslations = gettext.NullTranslations(),
) -> list[Feature]:
features: list[Feature] = []

for app_command in mybot.tree.get_commands():
fill_features(app_command, features)
fill_features(app_command, features, translations=translations)

for misc_cmd in mybot.misc_commands():
fill_features(misc_cmd, features)
fill_features(misc_cmd, features, translations=translations)

return features


async def export(mybot: MyBot, filename: str = "features.json") -> None:
features: list[Feature] = extract_features(mybot)
async def features_exporter(filename: str | Path = Path("./features.json")):
from mybot import MyBot

mybot = MyBot()
await mybot.load_extensions()

# TODO: find out where the unclosed session comes from.
result: dict[str, list[Feature]] = {}

for locale, translator in translations.items():
result[locale.value] = extract_features(mybot, translator)

def default(o: Any):
if is_dataclass(o):
Expand All @@ -183,20 +205,7 @@ def default(o: Any):
return JSONEncoder().default(o)

with open(filename, "w", encoding="utf-8") as file: # noqa: ASYNC101
json.dump(features, file, indent=4, default=default)


async def main():
mybot = MyBot(False)
await mybot.load_extensions()

# TODO: find out where the unclosed session comes from.
await export(mybot)


if __name__ == "__main__":
from mybot import MyBot

define_config(EXPORT_MODE=True)
json.dump(result, file, indent=4, default=default)

asyncio.run(main())
for ext in tuple(mybot.extensions):
await mybot.unload_extension(ext)
49 changes: 24 additions & 25 deletions src/main.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import asyncio
import logging
import os
from os import environ
from typing import Any
from pathlib import Path
from typing import Annotated, Any

import click
import typer

from core._config import define_config
from core._logger import create_logger
from features_exporter import features_exporter

try:
from dotenv import load_dotenv # pyright: ignore [reportMissingImports, reportUnknownVariableType]
Expand All @@ -18,28 +21,19 @@
logger = create_logger(level=getattr(logging, environ.get("LOG_LEVEL", "INFO")))
logging.getLogger("discord").setLevel(logging.INFO)


@click.group()
def cli():
# The base command group. Will not be called directly.
pass
cli = typer.Typer()


@cli.command()
@click.option(
"-c",
"--config",
"config_path",
default=None,
type=click.Path(exists=True),
help="Bind a configuration file.",
)
@click.option("--sync", is_flag=True, default=False, help="Sync slash command with Discord.")
@click.option("--sync-only", is_flag=True, default=False, help="Don't start the bot: just sync commands.")
def bot(
config_path: str | None,
sync: bool,
sync_only: bool,
def run(
config_path: Annotated[
Path,
typer.Option("--config", "-c", help="Bind a configuration file."),
] = Path("./config.toml"),
sync: Annotated[
bool,
typer.Option("--sync", help="Sync slash command with Discord."),
] = False,
):
required_env_var = {"POSTGRES_USER", "POSTGRES_PASSWORD", "POSTGRES_DB", "MYBOT_TOKEN"}
optional_env_var = {
Expand All @@ -66,15 +60,20 @@ def bot(

define_config(config_path, **kwargs)

if sync_only:
raise NotImplementedError("The option is not implemented yet. Will probably never be.")

from mybot import MyBot # MyBot is imported after the config is defined.

mybot: MyBot = MyBot(True, sync)
mybot: MyBot = MyBot(sync)

mybot.run(os.environ["MYBOT_TOKEN"], reconnect=True, log_handler=None)


@cli.command()
def export_features(
filename: Annotated[Path, typer.Argument(help="The json filename for the output.")] = Path("./features.json"),
):
define_config(EXPORT_MODE=True)
asyncio.run(features_exporter(filename=filename))


if __name__ == "__main__":
cli()
5 changes: 2 additions & 3 deletions src/mybot.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@
from discord.utils import get
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine

from commands_exporter import Feature, extract_features
from core import ExtendedCog, ResponseType, TemporaryCache, config, response_constructor
from core.custom_command_tree import CustomCommandTree
from core.error_handler import ErrorHandler
from core.extended_commands import MiscCommandContext
from core.i18n import Translator
from features_exporter import Feature, extract_features

if TYPE_CHECKING:
from discord import Guild, Thread, User
Expand Down Expand Up @@ -45,9 +45,8 @@ class MyBot(AutoShardedBot):
db_engine: AsyncEngine
async_session: async_sessionmaker[AsyncSession]

def __init__(self, running: bool = True, startup_sync: bool = False) -> None:
def __init__(self, startup_sync: bool = False) -> None:
self.startup_sync: bool = startup_sync
self.running = running
self._invite: discord.Invite | None = None

self.error_handler = ErrorHandler(self)
Expand Down

0 comments on commit a8cd213

Please sign in to comment.