From a2ffd8aa3b8717571f93b501d5e8340f754dc3fe Mon Sep 17 00:00:00 2001 From: Thastertyn Date: Thu, 9 Jan 2025 14:14:27 +0100 Subject: [PATCH] [main] WIP major rework of current design, database management and other things --- src/app.py | 84 ++++++++++------ src/models/__init__.py | 2 +- src/models/book_overview.py | 31 ++++++ src/models/book_overview_view.py | 16 ---- src/ui/dashboard/book_card.py | 68 +++++++++++++ src/ui/dashboard/book_list_entry.py | 27 ------ src/ui/dashboard/dashboard.py | 111 +++++++++++----------- src/ui/settings.py | 52 +++++----- src/ui/window.py | 13 +-- src/utils/__init__.py | 0 src/utils/config.py | 76 +++++++++++++++ src/utils/database.py | 27 ++++++ src/utils/errors/__init__.py | 0 src/utils/errors/database_config_error.py | 7 ++ src/utils/setup_logger.py | 14 +++ 15 files changed, 360 insertions(+), 168 deletions(-) create mode 100644 src/models/book_overview.py delete mode 100644 src/models/book_overview_view.py create mode 100644 src/ui/dashboard/book_card.py delete mode 100644 src/ui/dashboard/book_list_entry.py create mode 100644 src/utils/__init__.py create mode 100644 src/utils/database.py create mode 100644 src/utils/errors/__init__.py create mode 100644 src/utils/errors/database_config_error.py create mode 100644 src/utils/setup_logger.py diff --git a/src/app.py b/src/app.py index 95066f4..53c9378 100644 --- a/src/app.py +++ b/src/app.py @@ -1,38 +1,60 @@ import sys -from PySide6 import QtWidgets, QtCore +import os +import logging +from PySide6.QtWidgets import QMessageBox, QApplication + +from utils.config import UserConfig +from utils.database import DatabaseManager +from utils.errors.database_config_error import DatabaseConfigError +from utils.setup_logger import setup_logger from ui.window import LibraryWindow +class LibraryApp(): + def __init__(self, user_config: UserConfig | None = None): + setup_logger() + self.logger = logging.getLogger(__name__) + self.logger.info("Starting") + + self.qt_app = QApplication([]) + self.user_config = user_config + + try: + self.database_manger = DatabaseManager() + except DatabaseConfigError as e: + detail_text = f"Invalid config: {e.config_name}" + self.show_error(e.message, detail_text=detail_text) + sys.exit(1) + except FileNotFoundError: + self.show_error("Configuration not found") + sys.exit(1) + + self.window = LibraryWindow() + + def run(self) -> int: + self.window.show() + status = self.qt_app.exec() + self.cleanup() + self.logger.info("Exitting") + return status + + def show_error(self, text: str, detail_text: str = ""): + error_dialog = QMessageBox() + error_dialog.setIcon(QMessageBox.Icon.Critical) + error_dialog.setWindowTitle("Error") + error_dialog.setText(text) + if detail_text: + error_dialog.setInformativeText(detail_text) + error_dialog.setStandardButtons(QMessageBox.StandardButton.Ok) + error_dialog.exec() + + def cleanup(self) -> None: + self.logger.info("Cleaning up") + self.database_manger.cleanup() + + if __name__ == "__main__": - app = QtWidgets.QApplication([]) - window = LibraryWindow() - window.show() - sys.exit(app.exec()) - -# from sqlalchemy import create_engine -# from sqlalchemy.orm import sessionmaker - -# from models.book_overview_view import BookOverviewView - -# # Replace with your MySQL database credentials -# DATABASE_URI = 'mysql+mysqlconnector://username:password@localhost:3306/library' - -# # Create the engine -# engine = create_engine(DATABASE_URI) - -# from models.base import Base -# Base.metadata.create_all(engine) - -# # Create a configured session class -# SessionLocal = sessionmaker(bind=engine) - -# # Create a session instance -# session = SessionLocal() - -# books = session.query(BookOverviewView).all() -# for book in books: -# print(book.title, book.author_name, book.categories) - -# session.close() \ No newline at end of file + library_app = LibraryApp() + sys.exit(library_app.run()) diff --git a/src/models/__init__.py b/src/models/__init__.py index c6d5a5d..49b7b9c 100644 --- a/src/models/__init__.py +++ b/src/models/__init__.py @@ -7,6 +7,6 @@ from .member import Member from .librarian import Librarian from .loan import Loan -from .book_overview_view import BookOverviewView +from .book_overview import BooksOverview __all__ = ["Author", "Book", "BookCategory", "BookCategoryLink", "Member", "Librarian", "Loan", "BookOverviewView"] diff --git a/src/models/book_overview.py b/src/models/book_overview.py new file mode 100644 index 0000000..59bade6 --- /dev/null +++ b/src/models/book_overview.py @@ -0,0 +1,31 @@ +from sqlalchemy import Column, String, TIMESTAMP, Integer, Text, Enum + +from .base import Base + +from models.book import BookStatusEnum + +class BooksOverview(Base): + __tablename__ = 'books_overview' + __table_args__ = {'extend_existing': True} + + + id = Column(Integer, primary_key=True) + title = Column(String, nullable=False) + author_name = Column(String, nullable=False) + author_id = Column(Integer, nullable=False) + categories = Column(Text, nullable=True) + year_published = Column(Integer, nullable=True) + isbn = Column(String, nullable=True) + status = Column(Enum(BookStatusEnum), nullable=False) + created_at = Column(TIMESTAMP, nullable=False) + borrower_name = Column(String, nullable=True) + member_id = Column(Integer, nullable=True) + librarian_name = Column(String, nullable=True) + librarian_id = Column(Integer, nullable=True) + + # This prevents accidental updates/deletes as it's a view + def __repr__(self): + return (f"") \ No newline at end of file diff --git a/src/models/book_overview_view.py b/src/models/book_overview_view.py deleted file mode 100644 index 477e3ed..0000000 --- a/src/models/book_overview_view.py +++ /dev/null @@ -1,16 +0,0 @@ -from sqlalchemy import Column, String, TIMESTAMP, Integer - -from .base import Base - -class BookOverviewView(Base): - __tablename__ = 'books_overview' - - id = Column(Integer, primary_key=True, autoincrement=True) - title = Column(String()) - author_name = Column(String()) - categories = Column(String()) - year_published = Column(String()) - isbn = Column(String()) - created_at = Column(TIMESTAMP()) - borrower_name = Column(String()) - librarian_name = Column(String()) \ No newline at end of file diff --git a/src/ui/dashboard/book_card.py b/src/ui/dashboard/book_card.py new file mode 100644 index 0000000..2d166ae --- /dev/null +++ b/src/ui/dashboard/book_card.py @@ -0,0 +1,68 @@ +from PySide6.QtGui import QGuiApplication, QAction +from PySide6.QtQml import QQmlApplicationEngine +from PySide6.QtWidgets import QHBoxLayout, QVBoxLayout, QLabel, QWidget, QMenu, QSizePolicy, QLayout + +from models.book import BookStatusEnum + +from models.book_overview import BooksOverview + +STATUS_TO_COLOR_MAP = { + BookStatusEnum.available: "#3c702e", + BookStatusEnum.borrowed: "#702525", + BookStatusEnum.reserved: "#bc7613" +} +class BookCard(QWidget): + def __init__(self, book_overview: BooksOverview): + super().__init__() + + # Create the layout for the card + layout = QHBoxLayout(self) + layout.setSizeConstraint(QLayout.SizeConstraint.SetMinimumSize) # Ensure minimum size is respected + self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) # Avoid vertical stretching + + layout.setContentsMargins(10, 10, 10, 10) # Optional: Add margins for spacing + layout.setSpacing(10) + + # Left-side content + left_side = QVBoxLayout() + layout.addLayout(left_side) + title_label = QLabel(book_overview.title) + author_label = QLabel("By: " + book_overview.author_name) + isbn_label = QLabel("ISBN: " + (book_overview.isbn or "Not Available")) + left_side.addWidget(title_label) + left_side.addWidget(author_label) + left_side.addWidget(isbn_label) + + # Right-side content + right_side = QVBoxLayout() + layout.addLayout(right_side) + status_label = QLabel(str(book_overview.status)) + status_label.setStyleSheet(f"color: {STATUS_TO_COLOR_MAP[book_overview.status]}") + right_side.addWidget(status_label) + + if book_overview.librarian_name and book_overview.borrower_name: + borrower_label = QLabel("Borrowed: " + book_overview.borrower_name) + librarian_label = QLabel("By: " + book_overview.librarian_name) + right_side.addWidget(borrower_label) + right_side.addWidget(librarian_label) + + self.setLayout(layout) + + + def contextMenuEvent(self, event): + """Override to create a custom right-click menu.""" + context_menu = QMenu(self) + + # Add actions to the menu + action_edit_book = context_menu.addAction("Edit Book") + action_edit_author = context_menu.addAction("Edit Author") + action_mark_returned = context_menu.addAction("Mark as Returned") + + # Execute the menu and get the selected action + action = context_menu.exec_(self.mapToGlobal(event.pos())) + if action == action_edit_book: + self.label.setText("Edit Book selected") + elif action == action_edit_author: + self.label.setText("Edit Author selected") + elif action == action_mark_returned: + self.label.setText("Mark as Returned selected") \ No newline at end of file diff --git a/src/ui/dashboard/book_list_entry.py b/src/ui/dashboard/book_list_entry.py deleted file mode 100644 index fd7d097..0000000 --- a/src/ui/dashboard/book_list_entry.py +++ /dev/null @@ -1,27 +0,0 @@ -from PySide6.QtGui import QGuiApplication, QAction -from PySide6.QtQml import QQmlApplicationEngine -from PySide6 import QtWidgets, QtCore - -from models.book import BookStatusEnum - -STATUS_TO_COLOR_MAP = { - BookStatusEnum.available: "highlight", - BookStatusEnum.borrowed: "highlighted-text", - BookStatusEnum.reserved: "base" -} - -class BookListEntry(QtWidgets.QWidget): - def __init__(self, title, status): - super().__init__() - - layout = QtWidgets.QHBoxLayout(self) - - book_label = QtWidgets.QLabel(title) - layout.addWidget(book_label) - - status_label = QtWidgets.QLabel(status) - status_label.setStyleSheet(f"color: palette({STATUS_TO_COLOR_MAP[status]});") - layout.addWidget(status_label) - - - self.setLayout(layout) diff --git a/src/ui/dashboard/dashboard.py b/src/ui/dashboard/dashboard.py index 304e2bf..990a4be 100644 --- a/src/ui/dashboard/dashboard.py +++ b/src/ui/dashboard/dashboard.py @@ -1,92 +1,87 @@ -from PySide6.QtGui import QGuiApplication, QAction -from PySide6.QtQml import QQmlApplicationEngine -from PySide6 import QtWidgets, QtCore +from PySide6.QtGui import QAction +from PySide6.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QScrollArea, + QFrame, QPushButton, QMessageBox, QVBoxLayout, QScrollArea +) +from PySide6.QtCore import Qt -from ui.settings import SettingsDialog -class LibraryDashboard(QtWidgets.QWidget): +from .book_card import BookCard +from models.book_overview import BooksOverview + +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +class LibraryDashboard(QWidget): def __init__(self): super().__init__() # Central widget and layout - main_layout = QtWidgets.QVBoxLayout(self) + main_layout = QVBoxLayout(self) # Title label - title_label = QtWidgets.QLabel("Dashboard", self) - title_label.setAlignment(QtCore.Qt.AlignCenter) + title_label = QLabel("Dashboard", self) + title_label.setAlignment(Qt.AlignCenter) title_label.setStyleSheet("font-size: 20px; font-weight: bold; color: #0078D4;") main_layout.addWidget(title_label) - - # Available books list - available_label = QtWidgets.QLabel("Available Books") - available_label.setStyleSheet("font-size: 16px;") - main_layout.addWidget(available_label) - # Search bar - self.search_input = QtWidgets.QLineEdit() + self.search_input = QLineEdit() self.search_input.setPlaceholderText("Type to search...") self.search_input.textChanged.connect(self.filter_books) main_layout.addWidget(self.search_input) + # Scrollable area for cards + self.scroll_area = QScrollArea() + self.scroll_area.setWidgetResizable(True) - self.available_books_list = QtWidgets.QListWidget() - self.books = ["Book One", "Book Two", "Book Three", "Book Four", - "Book Five", "Book Six", "Book Seven", "Book Eight"] - - self.available_books_list.addItems(self.books) - self.available_books_list.itemClicked.connect(self.edit_book) - self.available_books_list.setStyleSheet(""" - QListWidget { - font-size: 14px; - } - QListWidget::item { - padding: 5px; - } - QListWidget::item:hover { - background-color: palette(highlight); - color: palette(highlighted-text); - } - """) - main_layout.addWidget(self.available_books_list) + # Container widget for the scroll area + self.scroll_widget = QWidget() + self.scroll_layout = QVBoxLayout(self.scroll_widget) + self.scroll_layout.setSpacing(5) # Optional: Adjust spacing between cards + self.scroll_layout.setContentsMargins(0, 0, 0, 0) - # Borrowed books list - borrowed_label = QtWidgets.QLabel("Currently Borrowed Books") - borrowed_label.setStyleSheet("font-size: 16px;") - main_layout.addWidget(borrowed_label) + # Example cards + # self.books = self.fetch_books_from_db() + # self.book_cards = [] - self.borrowed_books_list = QtWidgets.QListWidget() - self.borrowed_books_list.addItems(["Book Two", "Book Four"]) - self.borrowed_books_list.itemClicked.connect(self.return_book) - main_layout.addWidget(self.borrowed_books_list) + # for book in self.books: + # card = BookCard(book) + # self.scroll_layout.addWidget(card) + # self.book_cards.append(card) + + self.scroll_widget.setLayout(self.scroll_layout) + self.scroll_area.setWidget(self.scroll_widget) + main_layout.addWidget(self.scroll_area) # Buttons for actions - button_layout = QtWidgets.QHBoxLayout() - register_member_button = QtWidgets.QPushButton("Add New Member") + button_layout = QHBoxLayout() + register_member_button = QPushButton("Add New Member") register_member_button.clicked.connect(self.register_member) button_layout.addWidget(register_member_button) - add_borrow_record_button = QtWidgets.QPushButton("Add Borrow Record") + add_borrow_record_button = QPushButton("Add Borrow Record") add_borrow_record_button.clicked.connect(self.add_borrow_record) button_layout.addWidget(add_borrow_record_button) main_layout.addLayout(button_layout) def filter_books(self, text): - """Filter the available books list based on the search input.""" - self.available_books_list.clear() - filtered_books = [book for book in self.books if text.lower() in book.lower()] - self.available_books_list.addItems(filtered_books) - - # Slots for button actions - def edit_book(self, item): - QtWidgets.QMessageBox.information(self, "Edit Book", f"Edit details for '{item.text()}'.") - - def return_book(self, item): - QtWidgets.QMessageBox.information(self, "Return Book", f"Mark '{item.text()}' as returned.") + """Filter the cards based on the search input.""" + for card, book in zip(self.book_cards, self.books): + card.setVisible(text.lower() in book["title"].lower() or text.lower() in book["description"].lower()) def register_member(self): - QtWidgets.QMessageBox.information(self, "Add Member", "Open dialog to register a new member.") + QMessageBox.information(self, "Add Member", "Open dialog to register a new member.") def add_borrow_record(self): - QtWidgets.QMessageBox.information(self, "Add Borrow Record", "Open dialog to add a borrow record.") + QMessageBox.information(self, "Add Borrow Record", "Open dialog to add a borrow record.") + def fetch_books_from_db(self): + + """Fetch all books from the database.""" + try: + session = SessionLocal() + books = session.query(BooksOverview).all() + return books + except Exception as e: + QMessageBox.critical(self, "Database Error", f"Failed to fetch books: {e}") + return [] \ No newline at end of file diff --git a/src/ui/settings.py b/src/ui/settings.py index 5d03060..0ab0554 100644 --- a/src/ui/settings.py +++ b/src/ui/settings.py @@ -2,12 +2,14 @@ import sys from PySide6 import QtWidgets +from utils.config import UserConfig, TransactionLevel + class SettingsDialog(QtWidgets.QDialog): def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("Settings") - self.setMinimumSize(400, 300) + self.setMinimumSize(400, 100) # Position the dialog relative to the parent (main window) if parent: @@ -18,29 +20,24 @@ class SettingsDialog(QtWidgets.QDialog): # Create layout layout = QtWidgets.QVBoxLayout(self) - # Checkbox: Enable Notifications - self.notifications_checkbox = QtWidgets.QCheckBox("Enable Notifications") - layout.addWidget(self.notifications_checkbox) + data_mode_layout = QtWidgets.QHBoxLayout() - # Checkbox: Dark Mode - self.dark_mode_checkbox = QtWidgets.QCheckBox("Enable Dark Mode") - layout.addWidget(self.dark_mode_checkbox) + self.data_mode_label = QtWidgets.QLabel("Data mode:") + data_mode_layout.addWidget(self.data_mode_label) - # Dropdown: Language Selection - self.language_label = QtWidgets.QLabel("Language:") - layout.addWidget(self.language_label) + self.data_mode_dropdown = QtWidgets.QComboBox() + for tl in TransactionLevel: + self.data_mode_dropdown.addItem(tl.name.capitalize(), tl.value) - self.language_dropdown = QtWidgets.QComboBox() - self.language_dropdown.addItems(["English", "Spanish", "French", "German"]) - layout.addWidget(self.language_dropdown) + data_mode_layout.addWidget(self.data_mode_dropdown) + layout.addLayout(data_mode_layout) - # Dropdown: Theme Selection - self.theme_label = QtWidgets.QLabel("Theme:") - layout.addWidget(self.theme_label) - - self.theme_dropdown = QtWidgets.QComboBox() - self.theme_dropdown.addItems(["Light", "Dark", "System Default"]) - layout.addWidget(self.theme_dropdown) + # Set the currently selected mode to the mode in UserConfig + config = UserConfig() + current_level = config.transaction_level + index = self.data_mode_dropdown.findData(current_level) + if index != -1: + self.data_mode_dropdown.setCurrentIndex(index) # Buttons button_layout = QtWidgets.QHBoxLayout() @@ -56,16 +53,13 @@ class SettingsDialog(QtWidgets.QDialog): layout.addLayout(button_layout) def save_settings(self): - # Example of how to fetch settings - notifications = self.notifications_checkbox.isChecked() - dark_mode = self.dark_mode_checkbox.isChecked() - language = self.language_dropdown.currentText() - theme = self.theme_dropdown.currentText() + data_mode = self.data_mode_dropdown.currentData() + + config = UserConfig() + config.transaction_level = data_mode print("Settings Saved:") - print(f"Notifications: {notifications}") - print(f"Dark Mode: {dark_mode}") - print(f"Language: {language}") - print(f"Theme: {theme}") + print(f"Data Mode: {config.transaction_level}") + self.accept() \ No newline at end of file diff --git a/src/ui/window.py b/src/ui/window.py index 6a3d30d..3028367 100644 --- a/src/ui/window.py +++ b/src/ui/window.py @@ -56,11 +56,11 @@ class LibraryWindow(QtWidgets.QMainWindow): # File menu file_menu = menu_bar.addMenu("File") - import_action = QAction("Import", self) + import_action = QAction("Import books", self) import_action.triggered.connect(self.import_data) file_menu.addAction(import_action) - export_action = QAction("Export", self) + export_action = QAction("Export overview", self) export_action.triggered.connect(self.export_data) file_menu.addAction(export_action) @@ -74,9 +74,10 @@ class LibraryWindow(QtWidgets.QMainWindow): # Edit menu edit_menu = menu_bar.addMenu("Edit") - preferences = QAction("Preferences", self) - preferences.triggered.connect(self.edit_preferences) - edit_menu.addAction(preferences) + preferences_action = QAction("Preferences", self) + preferences_action.setShortcut("Ctrl+,") + preferences_action.triggered.connect(self.edit_preferences) + edit_menu.addAction(preferences_action) # Help menu help_menu = menu_bar.addMenu("Help") @@ -100,4 +101,4 @@ class LibraryWindow(QtWidgets.QMainWindow): pass def about(self): - QtWidgets.QMessageBox.information(self, "About", "Library Dashboard v1.0\nDeveloped by You.") \ No newline at end of file + QtWidgets.QMessageBox.information(self, "About", "Library app demonstrating the phantom read problem") \ No newline at end of file diff --git a/src/utils/__init__.py b/src/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/config.py b/src/utils/config.py index e69de29..8ee6540 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -0,0 +1,76 @@ +import enum +import os + +from utils.errors.database_config_error import DatabaseConfigError +from dotenv import load_dotenv +import logging + + +class DatabaseConfig(): + def __init__(self): + self.logger = logging.getLogger(__name__) + self.logger.debug("Parsing .env") + + anything_set = load_dotenv() + + if anything_set: + self.logger.debug(".env found") + else: + self.logger.error(".env not found") + raise FileNotFoundError(".env not found.") + + host = os.getenv("DATABASE_HOST") + port = os.getenv("DATABASE_PORT", "3306") + name = os.getenv("DATABASE_NAME") + user = os.getenv("DATABASE_USER") + password = os.getenv("DATABASE_PASSWORD") + + self.logger.debug("Validating fetched values from .env") + + if host is None or host == "": + self.logger.error("DATABASE_HOST is empty") + raise DatabaseConfigError( + "Config is invalid.", "DATABASE_HOST", "(Empty)") + if name is None or name == "": + self.logger.error("DATABASE_NAME is empty") + raise DatabaseConfigError( + "Config is invalid.", "DATABASE_NAME", "(Empty)") + if user is None or user == "": + self.logger.error("DATABASE_USER is empty") + raise DatabaseConfigError( + "Config is invalid.", "DATABASE_USER", "(Empty)") + if password is None or password == "": + self.logger.error("DATABASE_PASSWORD is empty") + raise DatabaseConfigError( + "Config is invalid.", "DATABASE_PASSWORD", "(Empty)") + + if not port.isdigit(): + self.logger.error("DATABASE_PORT is invalid. Not a number") + raise DatabaseConfigError( + "Config is invalid", "DATABASE_PORT", port) + + self.logger.debug("All config validated") + + self.host = host + self.port = port + self.name = name + self.user = user + self.password = password + + +class TransactionLevel(enum.Enum): + insecure = 'READ UNCOMMITED' + secure = 'SERIALIZABLE' + + +class UserConfig(): + _instance = None + + def __new__(cls, *args, **kwargs): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self): + if not hasattr(self, "transaction_level"): + self.transaction_level = TransactionLevel.insecure diff --git a/src/utils/database.py b/src/utils/database.py new file mode 100644 index 0000000..859b6f0 --- /dev/null +++ b/src/utils/database.py @@ -0,0 +1,27 @@ +import logging + +from mysql.connector import connect, cursor + +from utils.config import DatabaseConfig +from utils.errors.database_config_error import DatabaseConfigError + + +class DatabaseManager(): + def __init__(self) -> None: + self.logger = logging.getLogger(__name__) + self.logger.info("Reading database config") + self.database_config = DatabaseConfig() + self.connection = connect( + host=self.database_config.host, + port=self.database_config.port, + database=self.database_config.name, + user=self.database_config.user, + password=self.database_config.password + ) + + def get_cursor(self) -> cursor.MySQLCursorAbstract: + return self.connection.cursor(dictionary=True) + + def cleanup(self) -> None: + self.logger.debug("Closing connection") + self.connection.close() diff --git a/src/utils/errors/__init__.py b/src/utils/errors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/errors/database_config_error.py b/src/utils/errors/database_config_error.py new file mode 100644 index 0000000..6a9ea99 --- /dev/null +++ b/src/utils/errors/database_config_error.py @@ -0,0 +1,7 @@ +class DatabaseConfigError(Exception): + def __init__(self, message: str, config_name: str, config_value: str): + super().__init__(message) + + self.message = message + self.config_name = config_name + self.config_value = config_value diff --git a/src/utils/setup_logger.py b/src/utils/setup_logger.py new file mode 100644 index 0000000..827400a --- /dev/null +++ b/src/utils/setup_logger.py @@ -0,0 +1,14 @@ +import sys +import logging + +def setup_logger(): + logger = logging.getLogger() + logger.setLevel(logging.DEBUG) + + handler = logging.StreamHandler(sys.stdout) + handler.setLevel(logging.DEBUG) + + formatter = logging.Formatter("[%(levelname)s] - %(name)s - %(message)s") + handler.setFormatter(formatter) + + logger.addHandler(handler)