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
4 changes: 4 additions & 0 deletions .github/release-drafter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ categories:
- title: '✅ Tests'
labels:
- 'tests'
- title: '📚 Documentation'
labels:
- 'documentation'

change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
no-changes-template: '- No changes'
template: |
Expand Down
40 changes: 40 additions & 0 deletions ModuBotCore/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,29 @@


class ModuBotCore(BaseConfig):
"""
Core class for the ModuBot framework.

Handles logging, module loading, and lifecycle management (run/stop).
Modules are expected to reside in the "modules" directory and inherit from the defined MODULE_BASE_CLASS.

:cvar NAME: The name of the bot core.
:cvar VERSION: Current version of the bot core.
:cvar LOGGER_CONFIG: Logger configuration class.
:cvar MODULE_BASE_CLASS: Base class for all modules to be loaded.
"""

NAME: ClassVar[str] = "ModuBotCore"
VERSION: ClassVar[str] = "0.0.1"
LOGGER_CONFIG: ClassVar[Type[LoggerConfig]] = LoggerConfig
MODULE_BASE_CLASS: ClassVar[Type[BaseModule]] = BaseModule

def __init__(self):
"""
Initializes logging and prepares the module list.

Registers the stop() method to be called automatically on application exit.
"""
logging.basicConfig(
level=self.LOGGER_CONFIG.LEVEL,
format=self.LOGGER_CONFIG.FORMAT,
Expand All @@ -26,19 +43,36 @@ def __init__(self):
atexit.register(self.stop)

def run(self):
"""
Starts the bot and enables all loaded modules.

Calls the on_enable() method of each module after loading.
"""
self.logger.info(f"Starting {self.NAME}")
self._load_modules()
for module in self.modules:
self.logger.info(f'Enabling module "{module.NAME}"')
module.on_enable()

def stop(self):
"""
Disables all loaded modules and logs shutdown.

Calls the on_disable() method of each module.
"""
for module in self.modules:
self.logger.info(f'Disabling module "{module.NAME}"')
module.on_disable()
self.logger.info(f"Stopping {self.NAME}")

def _load_modules(self):
"""
Dynamically loads all modules from the "modules" directory.

Each module must be in its own directory with an `__init__.py` file.
The module must contain a class that inherits from MODULE_BASE_CLASS.
Only modules with ENABLING = True will be instantiated and added.
"""
root = Path().resolve()
module_dir = root / "modules"
self.logger.debug(f'Loading modules from "{module_dir}"')
Expand Down Expand Up @@ -78,4 +112,10 @@ def _load_modules(self):

@property
def logger(self) -> logging.Logger:
"""
Returns the logger instance for the bot.

:return: Logger bound to the bot's NAME.
:rtype: logging.Logger
"""
return logging.getLogger(self.NAME)
13 changes: 13 additions & 0 deletions ModuBotCore/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@


class BaseConfig:
"""
Base configuration class with runtime type checking for class variables.

This class automatically validates ClassVar attributes on subclass creation.
It ensures that the declared types match the actual values provided.

Supported inner types for ClassVar are:
- Primitive types: str, int, float, bool
- Type wrappers, e.g., Type[SomeClass]

:raises TypeError: If a ClassVar has an invalid or unsupported value/type.
"""

def __init_subclass__(cls) -> None:
hints = get_type_hints(cls)

Expand Down
10 changes: 10 additions & 0 deletions ModuBotCore/config/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@

@dataclass(frozen=True)
class LoggerConfig(BaseConfig):
"""
Configuration class for the logging system.

Defines the log level, formatting, and timestamp format used by the logger.

:cvar LEVEL: Logging level (e.g., DEBUG, INFO). Defaults to the LOG_LEVEL environment variable or 'INFO'.
:cvar FORMAT: Format string for log messages.
:cvar DATEFMT: Format string for timestamps in log messages.
"""

LEVEL: ClassVar[str] = os.getenv("LOG_LEVEL", "INFO")
FORMAT: ClassVar[str] = "[%(asctime)s - %(levelname)s - %(name)s]: %(message)s"
DATEFMT: ClassVar[str] = "%m/%d/%Y %H:%M:%S"
11 changes: 10 additions & 1 deletion ModuBotCore/helpers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
def str2bool(value: str) -> bool:
"""Convert string value to boolean."""
"""
Convert a string representation to a boolean value.

Common truthy values include: "yes", "true", "t", "y", "1" (case-insensitive).

:param value: Input string to convert.
:type value: str
:return: True if the value is considered truthy, False otherwise.
:rtype: bool
"""
return value.lower() in ("yes", "true", "t", "y", "1")
23 changes: 23 additions & 0 deletions ModuBotCore/modules/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,40 @@


class BaseModule(ABC, BaseConfig):
"""
Abstract base class for all ModuBot modules.

All modules must inherit from this class and implement the `on_enable` and `on_disable` lifecycle methods.

:cvar NAME: The name of the module.
:cvar ENABLING: Whether this module should be loaded and enabled.
"""

NAME: ClassVar[str] = "BaseModule"
ENABLING: ClassVar[bool] = True

@property
def logger(self) -> logging.Logger:
"""
Returns a logger instance specific to the module.

:return: Logger named after the module.
:rtype: logging.Logger
"""
return logging.getLogger(self.NAME)

@abstractmethod
def on_enable(self):
"""
Hook that is called when the module is enabled.
Must be implemented by subclasses.
"""
pass

@abstractmethod
def on_disable(self):
"""
Hook that is called when the module is disabled.
Must be implemented by subclasses.
"""
pass
Loading