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
on:
workflow_run: # ← event name must be workflow_run
workflows: ["Review"] # ← exact name of your triggering workflow
types: [completed]
branches: [Development]
push:
branches:
- Development
jobs:
on-success:
check:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
steps:
@@ -30,9 +27,3 @@ jobs:
- name: Run pylint
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
on:
workflow_run:
workflows: ["linters"]
types: [completed] #requested
push:
branches:
- main
jobs:
on-success:
Unit-Test:
runs-on: ubuntu-latest
steps:
@@ -29,10 +28,4 @@ jobs:
# 6. Run unit tests
- name: Run pytest
run: pytest
on-failure:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'failure' }}
steps:
- run: echo 'The triggering workflow failed'
run: pytest

Binary file not shown.

View File

@@ -1,4 +1,5 @@
import os
from pathlib import Path
from typing import Any, Dict
@@ -10,7 +11,15 @@ class DatabaseConfig:
def _load_config(self) -> Dict[str, Any]:
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":
return {
"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
from flask import current_app, g
from flask import g
from .interface import DatabaseInterface

View File

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

View File

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

Binary file not shown.

View File

@@ -1,8 +1,12 @@
import logging
import os
from dotenv import load_dotenv
from frontend.app import app
load_dotenv()
# Logging konfigurieren
logging.basicConfig(
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"
host = os.getenv("FLASK_HOST", "0.0.0.0")
port = int(os.getenv("FLASK_PORT", "8080"))
print(port)
logger.info(f"Starting ChatBot application on {host}:{port}")
logger.info(f"Debug mode: {debug}")

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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