Merge pull request #6 from florianuhlig/Development

Development merge to Main
This commit is contained in:
Florian Uhlig
2025-10-07 08:53:18 +02:00
committed by GitHub
24 changed files with 121 additions and 103 deletions

4
.flake8 Normal file
View File

@@ -0,0 +1,4 @@
[flake8]
#ignore = E226,E302,E41
max-line-length = 130
max-complexity = 10

View File

@@ -1,14 +1,11 @@
name: linters name: linters
on: on:
workflow_run: # ← event name must be workflow_run push:
workflows: ["Review"] # ← exact name of your triggering workflow branches:
types: [completed] - Development
branches: [Development]
jobs: jobs:
on-success: check:
if: ${{ github.event.workflow_run.conclusion == 'success' }} if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@@ -30,9 +27,3 @@ jobs:
- name: Run pylint - name: Run pylint
run: pylint frontend services utils run: pylint frontend services utils
on-failure:
if: ${{ github.event.workflow_run.conclusion == 'failure' }}
runs-on: ubuntu-latest
steps:
- run: echo 'The Review workflow failed'

View File

@@ -1,13 +1,12 @@
name: Unit Test name: Unit Test
on: on:
workflow_run: push:
workflows: ["linters"] branches:
types: [completed] #requested - main
jobs: jobs:
on-success: Unit-Test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@@ -30,9 +29,3 @@ jobs:
# 6. Run unit tests # 6. Run unit tests
- name: Run pytest - name: Run pytest
run: pytest run: pytest
on-failure:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'failure' }}
steps:
- run: echo 'The triggering workflow failed'

Binary file not shown.

View File

@@ -1,4 +1,5 @@
import os import os
from pathlib import Path
from typing import Any, Dict from typing import Any, Dict
@@ -10,7 +11,15 @@ class DatabaseConfig:
def _load_config(self) -> Dict[str, Any]: def _load_config(self) -> Dict[str, Any]:
if self.db_type.lower() == "sqlite": if self.db_type.lower() == "sqlite":
return {"path": os.getenv("SQLITE_PATH", "databases/chatbot.db")} # Standardpfad relativ zum Projektroot
default_path = Path(__file__).parent.parent / "databases" / "chatbot.db"
sqlite_path = os.getenv("SQLITE_PATH", str(default_path))
# Stelle sicher, dass wir einen absoluten Pfad haben
if not Path(sqlite_path).is_absolute():
sqlite_path = Path.cwd() / sqlite_path
return {"path": str(sqlite_path)}
elif self.db_type.lower() == "mysql": elif self.db_type.lower() == "mysql":
return { return {
"host": os.getenv("MYSQL_HOST", "localhost"), "host": os.getenv("MYSQL_HOST", "localhost"),

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,6 +1,6 @@
import logging import logging
from flask import current_app, g from flask import g
from .interface import DatabaseInterface from .interface import DatabaseInterface

View File

@@ -1,5 +1,5 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional from typing import Any, Dict, Optional
class DatabaseInterface(ABC): class DatabaseInterface(ABC):

View File

@@ -1,5 +1,6 @@
import logging import logging
import sqlite3 import sqlite3
from pathlib import Path
from threading import local from threading import local
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
@@ -13,32 +14,50 @@ class SQLiteDatabase(DatabaseInterface):
def __init__(self, db_path: str): def __init__(self, db_path: str):
self.db_path = db_path self.db_path = db_path
# Erstelle Verzeichnis falls nicht vorhanden
self._ensure_database_directory()
# Thread-local storage für Verbindungen # Thread-local storage für Verbindungen
self._local = local() self._local = local()
def _ensure_database_directory(self) -> None:
"""Stellt sicher, dass das Database-Verzeichnis existiert"""
db_dir = Path(self.db_path).parent
if not db_dir.exists():
try:
db_dir.mkdir(parents=True, exist_ok=True)
logger.info(f"Created database directory: {db_dir}")
except OSError as e:
logger.error(f"Failed to create database directory {db_dir}: {e}")
raise
def _get_connection(self) -> sqlite3.Connection: def _get_connection(self) -> sqlite3.Connection:
"""Holt oder erstellt eine thread-lokale Verbindung""" """Holt oder erstellt eine thread-lokale Verbindung"""
if not hasattr(self._local, "connection") or self._local.connection is None: if not hasattr(self._local, "connection") or self._local.connection is None:
self._local.connection = sqlite3.connect( try:
self.db_path, # Absoluten Pfad verwenden für bessere Kompatibilität
check_same_thread=False, # Erlaubt thread-übergreifende Nutzung abs_path = Path(self.db_path).resolve()
timeout=30.0, self._local.connection = sqlite3.connect(
detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES, str(abs_path),
) check_same_thread=False, # Erlaubt thread-übergreifende Nutzung
self._local.connection.row_factory = sqlite3.Row timeout=30.0,
# Optimierungen für bessere Performance detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES,
self._local.connection.execute("PRAGMA journal_mode=WAL") )
self._local.connection.execute("PRAGMA synchronous=NORMAL") self._local.connection.row_factory = sqlite3.Row
self._local.connection.execute("PRAGMA cache_size=1000") # Optimierungen für bessere Performance
self._local.connection.execute("PRAGMA temp_store=MEMORY") self._local.connection.execute("PRAGMA journal_mode=WAL")
logger.debug(f"New SQLite connection created for thread") self._local.connection.execute("PRAGMA synchronous=NORMAL")
self._local.connection.execute("PRAGMA cache_size=1000")
self._local.connection.execute("PRAGMA temp_store=MEMORY")
logger.debug(f"New SQLite connection created for thread: {abs_path}")
except sqlite3.Error as e:
logger.error(f"Failed to connect to SQLite database at {abs_path}: {e}")
raise
return self._local.connection return self._local.connection
def connect(self) -> None: def connect(self) -> None:
"""Initialisiert die thread-lokale Verbindung""" """Initialisiert die thread-lokale Verbindung"""
try: try:
conn = self._get_connection() self._get_connection() # Entfernt das unbenutzte 'conn'
logger.info(f"Connected to SQLite database: {self.db_path}") logger.info(f"Connected to SQLite database: {self.db_path}")
except Exception as e: except Exception as e:
logger.error(f"Failed to connect to SQLite database: {e}") logger.error(f"Failed to connect to SQLite database: {e}")
@@ -61,14 +80,14 @@ class SQLiteDatabase(DatabaseInterface):
try: try:
cursor.execute( cursor.execute(
""" """
CREATE TABLE IF NOT EXISTS users CREATE TABLE IF NOT EXISTS users
( (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE, username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL UNIQUE, email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL, password_hash TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )
""" """
) )
conn.commit() conn.commit()
logger.info("User table created/verified") logger.info("User table created/verified")

Binary file not shown.

View File

@@ -1,8 +1,12 @@
import logging import logging
import os import os
from dotenv import load_dotenv
from frontend.app import app from frontend.app import app
load_dotenv()
# Logging konfigurieren # Logging konfigurieren
logging.basicConfig( logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
@@ -15,6 +19,7 @@ if __name__ == "__main__":
debug = os.getenv("FLASK_DEBUG", "False").lower() == "true" debug = os.getenv("FLASK_DEBUG", "False").lower() == "true"
host = os.getenv("FLASK_HOST", "0.0.0.0") host = os.getenv("FLASK_HOST", "0.0.0.0")
port = int(os.getenv("FLASK_PORT", "8080")) port = int(os.getenv("FLASK_PORT", "8080"))
print(port)
logger.info(f"Starting ChatBot application on {host}:{port}") logger.info(f"Starting ChatBot application on {host}:{port}")
logger.info(f"Debug mode: {debug}") logger.info(f"Debug mode: {debug}")

View File

@@ -1,36 +1,28 @@
# Requirements for PY_ChatBot astroid==3.3.11
# Core Framework black==25.9.0
Flask>=2.3.0,<3.0.0 blinker==1.9.0
Werkzeug>=2.3.0,<3.0.0 click==8.3.0
dill==0.4.0
# Database & ORM (Optional for future expansion) dotenv==0.9.9
SQLAlchemy>=2.0.0,<3.0.0 flake8==7.3.0
Flask==3.1.2
# Environment & Configuration iniconfig==2.1.0
python-dotenv>=1.0.0 isort==6.1.0
itsdangerous==2.2.0
# Security & Authentication Jinja2==3.1.6
bcrypt>=4.0.0 MarkupSafe==3.0.3
mccabe==0.7.0
# HTTP Client (for health checks and external APIs) mypy_extensions==1.1.0
requests>=2.31.0 packaging==25.0
pathspec==0.12.1
# Date/Time utilities platformdirs==4.4.0
python-dateutil>=2.8.0 pluggy==1.6.0
pycodestyle==2.14.0
# Development Dependencies (commented out for production) pyflakes==3.4.0
# pytest>=7.4.0 Pygments==2.19.2
# pytest-flask>=1.2.0 pylint==3.3.8
# pytest-cov>=4.1.0 pytest==8.4.2
# black>=23.7.0 python-dotenv==1.1.1
# flake8>=6.0.0 pytokens==0.1.10
# mypy>=1.5.0 tomlkit==0.13.3
Werkzeug==3.1.3
# Production WSGI Server
gunicorn>=21.2.0
# Logging
colorlog>=6.7.0
# Utility
click>=8.1.0

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -15,36 +15,41 @@ class UserService:
def create_user( def create_user(
self, username: str, email: str, password: str self, username: str, email: str, password: str
) -> tuple[bool, list[str]]: ) -> tuple[bool, list[str]]:
errors = [] errors = self._validate_user_inputs(username, email, password)
if errors:
return False, errors
# Validierung errors = self._check_user_existence(username, email)
if errors:
return False, errors
password_hash = PasswordUtils.hash_password_simple(password)
return self._attempt_user_creation(username, email, password_hash)
def _validate_user_inputs(
self, username: str, email: str, password: str
) -> list[str]:
errors = []
if not ValidationUtils.validate_username(username): if not ValidationUtils.validate_username(username):
errors.append("Invalid username format") errors.append("Invalid username format")
if not ValidationUtils.validate_email(email): if not ValidationUtils.validate_email(email):
errors.append("Invalid email format") errors.append("Invalid email format")
password_valid, password_errors = ValidationUtils.validate_password(password) password_valid, password_errors = ValidationUtils.validate_password(password)
if not password_valid: if not password_valid:
errors.extend(password_errors) errors.extend(password_errors)
return errors
if errors: def _check_user_existence(self, username: str, email: str) -> list[str]:
return False, errors errors = []
# Prüfe auf existierende User
if self.get_user_by_email(email): if self.get_user_by_email(email):
errors.append("Email already registered") errors.append("Email already registered")
if self.get_user_by_username(username): if self.get_user_by_username(username):
errors.append("Username already taken") errors.append("Username already taken")
return errors
if errors: def _attempt_user_creation(
return False, errors self, username: str, email: str, password_hash: str
) -> tuple[bool, list[str]]:
# Passwort hashen
password_hash = PasswordUtils.hash_password_simple(password)
# User erstellen
try: try:
success = self.db.create_user(username, email.lower(), password_hash) success = self.db.create_user(username, email.lower(), password_hash)
if success: if success:

0
test.txt Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.