Merge pull request #6 from florianuhlig/Development
Development merge to Main
This commit is contained in:
4
.flake8
Normal file
4
.flake8
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[flake8]
|
||||||
|
#ignore = E226,E302,E41
|
||||||
|
max-line-length = 130
|
||||||
|
max-complexity = 10
|
||||||
17
.github/workflows/linters.yml
vendored
17
.github/workflows/linters.yml
vendored
@@ -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'
|
|
||||||
|
|||||||
17
.github/workflows/unit-test.yml
vendored
17
.github/workflows/unit-test.yml
vendored
@@ -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:
|
||||||
|
|
||||||
@@ -29,10 +28,4 @@ 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'
|
|
||||||
BIN
config/__pycache__/database.cpython-312.pyc
Normal file
BIN
config/__pycache__/database.cpython-312.pyc
Normal file
Binary file not shown.
@@ -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"),
|
||||||
|
|||||||
BIN
database/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
database/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
database/__pycache__/flask_integration.cpython-312.pyc
Normal file
BIN
database/__pycache__/flask_integration.cpython-312.pyc
Normal file
Binary file not shown.
BIN
database/__pycache__/interface.cpython-312.pyc
Normal file
BIN
database/__pycache__/interface.cpython-312.pyc
Normal file
Binary file not shown.
BIN
database/__pycache__/sqlite_db.cpython-312.pyc
Normal file
BIN
database/__pycache__/sqlite_db.cpython-312.pyc
Normal file
Binary file not shown.
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
BIN
frontend/__pycache__/app.cpython-312.pyc
Normal file
BIN
frontend/__pycache__/app.cpython-312.pyc
Normal file
Binary file not shown.
5
main.py
5
main.py
@@ -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}")
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
BIN
services/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
services/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
services/__pycache__/auth_service.cpython-312.pyc
Normal file
BIN
services/__pycache__/auth_service.cpython-312.pyc
Normal file
Binary file not shown.
BIN
services/__pycache__/user_service.cpython-312.pyc
Normal file
BIN
services/__pycache__/user_service.cpython-312.pyc
Normal file
Binary file not shown.
@@ -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:
|
||||||
|
|||||||
BIN
utils/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
utils/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/auth_decorators.cpython-312.pyc
Normal file
BIN
utils/__pycache__/auth_decorators.cpython-312.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/password_utils.cpython-312.pyc
Normal file
BIN
utils/__pycache__/password_utils.cpython-312.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/validation.cpython-312.pyc
Normal file
BIN
utils/__pycache__/validation.cpython-312.pyc
Normal file
Binary file not shown.
Reference in New Issue
Block a user