Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .flake8

This file was deleted.

56 changes: 30 additions & 26 deletions .github/workflows/grace_framework.yml
Original file line number Diff line number Diff line change
@@ -1,38 +1,42 @@
# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python

name: Grace Framework Tests
name: Grace Framework CI

on:
push:
branches: [ "main" ]
branches: ["main"]
pull_request:
branches: [ "main" ]
branches: ["main"]

permissions:
contents: read

jobs:
build:

test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Set up Python 3.10
uses: actions/setup-python@v3
with:
python-version: "3.10"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install .
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 grace --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 grace --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
pytest -v
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install .[dev]
pip install mypy

- name: Run code format check
run: |
black --check .
isort --check-only .

- name: Run type checks
run: |
mypy .

- name: Run tests
run: |
pytest -v
4 changes: 3 additions & 1 deletion grace/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
__version__ = "0.10.10-alpha"
__version__ = "1.0.0-alpha"

from discord.ext.commands import *
71 changes: 37 additions & 34 deletions grace/application.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,22 @@
from os import environ
from configparser import SectionProxy

from coloredlogs import install
from logging import basicConfig, critical
from logging.handlers import RotatingFileHandler

from os import environ
from pathlib import Path
from types import ModuleType
from typing import Generator, Any, Union, Dict, Optional, no_type_check
from typing import Any, Dict, Generator, Optional, Union, no_type_check

from sqlalchemy import create_engine
from coloredlogs import install
from sqlalchemy.engine import Engine
from sqlalchemy.exc import OperationalError
from sqlalchemy.orm import (
declarative_base,
sessionmaker,
Session,
DeclarativeMeta
)
from sqlalchemy_utils import (
database_exists,
create_database,
drop_database
)
from pathlib import Path
from sqlalchemy.orm import DeclarativeMeta, declarative_base
from sqlalchemy_utils import create_database, database_exists, drop_database
from sqlmodel import Session, create_engine

from grace.config import Config
from grace.exceptions import ConfigError
from grace.importer import find_all_importables, import_module

from grace.model import Model

ConfigReturn = Union[str, int, float, None]

Expand All @@ -43,13 +33,14 @@ class Application:

def __init__(self) -> None:
database_config_path: Path = Path("config/database.cfg")

if not database_config_path.exists():
raise ConfigError("Unable to find the 'database.cfg' file.")

self.__token: str = str(self.config.get("discord", "token"))
self.__engine: Union[Engine, None] = None

self.environment: str = "development"
self.command_sync: bool = True
self.watch: bool = False

Expand All @@ -67,8 +58,9 @@ def session(self) -> Session:
"""Instantiate the session for querying the database."""

if not self.__session:
session: sessionmaker = sessionmaker(bind=self.__engine)
self.__session = session()
# session_factory: sessionmaker = sessionmaker(bind=self.__engine)
# scoped_session_ = scoped_session(session_factory)
self.__session = Session(self.__engine)

return self.__session

Expand Down Expand Up @@ -99,11 +91,11 @@ def extension_modules(self) -> Generator[str, Any, None]:
def database_infos(self) -> Dict[str, str]:
return {
"dialect": self.session.bind.dialect.name,
"database": self.session.bind.url.database
"database": self.session.bind.url.database,
}

@property
def database_exists(self):
def database_exists(self) -> bool:
return database_exists(self.config.database_uri)

def get_extension_module(self, extension_name) -> Union[str, None]:
Expand Down Expand Up @@ -134,9 +126,7 @@ def load_models(self):

def load_logs(self) -> None:
file_handler: RotatingFileHandler = RotatingFileHandler(
f"logs/{self.config.current_environment}.log",
maxBytes=10000,
backupCount=5
f"logs/{self.config.current_environment}.log", maxBytes=10000, backupCount=5
)

basicConfig(
Expand All @@ -147,19 +137,24 @@ def load_logs(self) -> None:

install(
self.config.environment.get("log_level"),
fmt="".join([
"[%(asctime)s] %(programname)s %(funcName)s ",
"%(module)s %(levelname)s %(message)s"
]),
fmt="".join(
[
"[%(asctime)s] %(programname)s %(funcName)s ",
"%(module)s %(levelname)s %(message)s",
]
),
programname=self.config.current_environment,
)

def load_database(self):
def load_database(self) -> None:
"""Loads and connects to the database using the loaded config"""

if not self.config.database_uri:
raise ValueError("No database uri.")

self.__engine = create_engine(
self.config.database_uri,
echo=self.config.environment.getboolean("sqlalchemy_echo")
echo=self.config.environment.getboolean("sqlalchemy_echo"),
)

if self.database_exists:
Expand All @@ -168,6 +163,8 @@ def load_database(self):
except OperationalError as e:
critical(f"Unable to load the 'database': {e}")

Model.set_engine(self.__engine)

def unload_database(self):
"""Unloads the current database"""

Expand All @@ -176,7 +173,7 @@ def unload_database(self):

def reload_database(self):
"""
Reload the database. This function can be use in case
Reload the database. This function can be used in case
there's a dynamic environment change.
"""

Expand All @@ -198,11 +195,17 @@ def drop_database(self):
def create_tables(self):
"""Creates all the tables for the current loaded database"""

if not self.__engine:
raise RuntimeError("Database engine is not initialized.")

self.load_database()
self.base.metadata.create_all(self.__engine)

def drop_tables(self):
"""Drops all the tables for the current loaded database"""

if not self.__engine:
raise RuntimeError("Database engine is not initialized.")

self.load_database()
self.base.metadata.drop_all(self.__engine)
57 changes: 26 additions & 31 deletions grace/bot.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
from logging import info, warning, critical
from logging import critical, info, warning

from apscheduler.schedulers.asyncio import AsyncIOScheduler
from discord import Intents, LoginFailure, Object as DiscordObject
from discord.ext.commands import Bot as DiscordBot, when_mentioned_or
from discord.ext.commands.errors import (
ExtensionNotLoaded,
ExtensionAlreadyLoaded
)
from discord import Intents, LoginFailure
from discord import Object as DiscordObject
from discord.ext.commands import Bot as DiscordBot
from discord.ext.commands import when_mentioned_or
from discord.ext.commands.errors import ExtensionAlreadyLoaded, ExtensionNotLoaded

from grace.application import Application, SectionProxy
from grace.watcher import Watcher

# make discord.ext.commands importable from this module
from discord.ext.commands import *


class Bot(DiscordBot):
"""This class is the core of the bot

This class is a subclass of `discord.ext.commands.Bot` and is the core
of the bot. It is responsible for loading the extensions and
of the bot. It is responsible for loading the extensions and
syncing the commands.

The bot is instantiated with the application object and the intents.
Expand All @@ -30,23 +28,16 @@ def __init__(self, app: Application, **kwargs) -> None:
self.watcher: Watcher = Watcher(self.on_reload)

command_prefix = kwargs.pop(
'command_prefix',
when_mentioned_or(self.config.get("prefix", "!"))
)
description: str = kwargs.pop(
'description',
self.config.get("description")
)
intents: Intents = kwargs.pop(
'intents',
Intents.default()
"command_prefix", when_mentioned_or(self.config.get("prefix", "!"))
)
description: str = kwargs.pop("description", self.config.get("description"))
intents: Intents = kwargs.pop("intents", Intents.default())

super().__init__(
command_prefix=command_prefix,
description=description,
intents=intents,
**kwargs
**kwargs,
)

async def load_extensions(self) -> None:
Expand All @@ -63,8 +54,10 @@ async def sync_commands(self) -> None:

async def invoke(self, ctx):
if ctx.command:
info(f"'{ctx.command}' has been invoked by {ctx.author} "
f"({ctx.author.display_name})")
info(
f"'{ctx.command}' has been invoked by {ctx.author} "
f"({ctx.author.display_name})"
)
await super().invoke(ctx)

async def setup_hook(self) -> None:
Expand All @@ -78,13 +71,13 @@ async def setup_hook(self) -> None:

self.scheduler.start()

async def load_extension(self, name: str) -> None:
async def load_extension(self, name: str) -> None: # type: ignore[override]
try:
await super().load_extension(name)
except ExtensionAlreadyLoaded:
warning(f"Extension '{name}' already loaded, skipping.")

async def unload_extension(self, name: str) -> None:
async def unload_extension(self, name: str) -> None: # type: ignore[override]
try:
await super().unload_extension(name)
except ExtensionNotLoaded:
Expand All @@ -97,14 +90,16 @@ async def on_reload(self):
await self.unload_extension(module)
await self.load_extension(module)

def run(self) -> None: # type: ignore[override]
def run(self) -> None: # type: ignore[override]
"""Override the `run` method to handle the token retrieval"""
try:
if self.app.token:
super().run(self.app.token)
else:
critical("Unable to find the token. Make sure your current"
"directory contains an '.env' and that "
"'DISCORD_TOKEN' is defined")
critical(
"Unable to find the token. Make sure your current"
"directory contains an '.env' and that "
"'DISCORD_TOKEN' is defined"
)
except LoginFailure as e:
critical(f"Authentication failed : {e}")
critical(f"Authentication failed : {e}")
Loading