diff --git a/docs/de/changelog.md b/docs/de/changelog.md new file mode 100644 index 0000000..f4d16fd --- /dev/null +++ b/docs/de/changelog.md @@ -0,0 +1 @@ +{!CHANGELOG.md!} diff --git a/docs/de/contributing/code-guidelines.md b/docs/de/contributing/code-guidelines.md new file mode 100644 index 0000000..3473bf1 --- /dev/null +++ b/docs/de/contributing/code-guidelines.md @@ -0,0 +1,748 @@ +# Code-Richtlinien + +Umfassende Coding-Standards und Best Practices für Beiträge zu FastAPI-fastkit. + +## Überblick + +Diese Richtlinien sorgen für Code-Qualität, Konsistenz und Wartbarkeit im gesamten FastAPI-fastkit-Projekt. Wenn Sie ihnen folgen, entsteht eine Codebasis, die leicht zu lesen, zu pflegen und zu erweitern ist. + +## Python-Codestil + +### PEP-8-Konformität + +Folgen Sie [PEP 8](https://www.python.org/dev/peps/pep-0008/) mit diesen spezifischen Einstellungen: + +- **Zeilenlänge**: 88 Zeichen (Black-Standard) +- **Einrückung**: 4 Leerzeichen (keine Tabs) +- **Nachgestellte Kommas**: in mehrzeiligen Strukturen erforderlich +- **Anführungszeichen**: doppelte Anführungszeichen bevorzugt + +### Code-Formatierung + +Wir verwenden **Black** für automatische Code-Formatierung: + +```python +# Gut ✅ +def create_project( + name: str, + template: str, + options: Dict[str, Any], +) -> ProjectResult: + """Create a new FastAPI project.""" + return ProjectResult(name=name, template=template) + +# Nicht gut ❌ +def create_project(name: str, template: str, options: Dict[str,Any])->ProjectResult: + """Create a new FastAPI project.""" + return ProjectResult(name=name,template=template) +``` + +### Import-Organisation + +Verwenden Sie **isort**, um Imports zu organisieren: + +```python +# Standard library imports +import os +import sys +from pathlib import Path +from typing import Dict, List, Optional, Union + +# Third-party imports +import click +import pydantic +from fastapi import FastAPI + +# Local imports +from fastapi_fastkit.commands import BaseCommand +from fastapi_fastkit.utils import validation +from fastapi_fastkit.templates.manager import TemplateManager +``` + +## Type Hints + +### Erforderliche Type Hints + +Alle öffentlichen Funktionen und Methoden müssen Type Hints enthalten: + +```python +# Gut ✅ +def validate_project_name(name: str) -> bool: + """Validate project name format.""" + return name.isidentifier() and not name.startswith('_') + +def create_files( + files: List[Path], + template_data: Dict[str, Any] +) -> List[Path]: + """Create files from template data.""" + created_files = [] + for file_path in files: + # Implementation... + created_files.append(file_path) + return created_files + +# Nicht gut ❌ +def validate_project_name(name): + return name.isidentifier() and not name.startswith('_') +``` + +### Komplexe Type-Annotationen + +Verwenden Sie passende Annotationen für komplexe Strukturen: + +```python +from typing import Dict, List, Optional, Union, Tuple, Any +from pathlib import Path + +# Type aliases for complex types +ProjectConfig = Dict[str, Union[str, bool, List[str]]] +FileMapping = Dict[Path, str] +ValidationResult = Tuple[bool, Optional[str]] + +def process_template( + template_path: Path, + config: ProjectConfig, + output_dir: Optional[Path] = None, +) -> ValidationResult: + """Process template with configuration.""" + # Implementation... + return True, None +``` + +## Namenskonventionen + +### Variablen und Funktionen + +- **snake_case** für Variablen und Funktionen +- **Aussagekräftige Namen**, die den Zweck erklären +- **Abkürzungen vermeiden**, sofern nicht gängig + +```python +# Gut ✅ +project_name = "my-api" +template_directory = Path("templates") +user_input_data = get_user_input() + +def validate_email_address(email: str) -> bool: + """Validate email address format.""" + return "@" in email and "." in email + +# Nicht gut ❌ +proj_nm = "my-api" +temp_dir = Path("templates") +usr_data = get_input() + +def validate_email(e): + return "@" in e and "." in e +``` + +### Klassen + +- **PascalCase** für Klassennamen +- **Beschreibende und spezifische** Namen + +```python +# Gut ✅ +class SomeClass: + """Represents example class of FastAPI-fastkit.""" + pass + +class SomeClassValidationError(Exception): + """Raised when example class validation fails.""" + pass + +class UserInputHandler: + """Handles user input validation and processing.""" + pass + +# Nicht gut ❌ +class Class: + pass + +class Error(Exception): + pass + +class Handler: + pass +``` + +### Konstanten + +- **UPPER_CASE** mit Unterstrichen +- Konstanten **nur auf Modul-Ebene** + +```python +# Gut ✅ +DEFAULT_TEMPLATE_NAME = "fastapi-default" +MAX_PROJECT_NAME_LENGTH = 50 +SUPPORTED_PYTHON_VERSIONS = ["3.8", "3.9", "3.10", "3.11", "3.12"] + +# Nicht gut ❌ +default_template = "fastapi-default" +maxLength = 50 +versions = ["3.8", "3.9", "3.10", "3.11", "3.12"] +``` + +## Dokumentations-Standards + +### Docstrings + +Verwenden Sie **Google-Style-Docstrings** für alle öffentlichen APIs: + +```python +def create_project_structure( + project_name: str, + template_path: Path, + output_directory: Optional[Path] = None, + overwrite: bool = False, +) -> List[Path]: + """Create project structure from template. + + Creates a new FastAPI project structure by copying and processing + template files. Supports variable substitution and file customization. + + Args: + project_name: Name of the project to create. Must be a valid + Python identifier. + template_path: Path to the template directory containing + source files and configuration. + output_directory: Directory where project will be created. + Defaults to current working directory. + overwrite: Whether to overwrite existing files. If False, + raises error when files exist. + + Returns: + List of created file paths in order of creation. + + Raises: + ValueError: If project_name is invalid or empty. + FileExistsError: If output directory exists and overwrite is False. + TemplateNotFoundError: If template_path doesn't exist. + PermissionError: If insufficient permissions to create files. + + Example: + ```python + template_path = Path("templates/fastapi-default") + created_files = create_project_structure( + project_name="my-api", + template_path=template_path, + output_directory=Path("./projects"), + overwrite=False + ) + print(f"Created {len(created_files)} files") + ``` + """ + # Implementation here... + pass +``` + +### Kommentare + +- **WARUM erklären, nicht WAS** +- **Sparsam einsetzen** — Code sollte selbsterklärend sein +- **Kommentare aktualisieren**, wenn sich Code ändert + +```python +# Gut ✅ +def validate_dependencies(requirements: List[str]) -> bool: + """Validate project dependencies.""" + # Skip validation in development mode to allow experimental packages + if os.getenv("FASTKIT_DEV_MODE"): + return True + + # Check each requirement against known security vulnerabilities + for requirement in requirements: + if is_vulnerable_package(requirement): + return False + + return True + +# Nicht gut ❌ +def validate_dependencies(requirements: List[str]) -> bool: + """Validate project dependencies.""" + # Check if dev mode + if os.getenv("FASTKIT_DEV_MODE"): + return True + + # Loop through requirements + for requirement in requirements: + # Check if vulnerable + if is_vulnerable_package(requirement): + return False + + # Return true + return True +``` + +## Fehlerbehandlung + +### Behandlung von Exceptions + +- **Spezifische Exceptions** soweit möglich abfangen +- **Aussagekräftige Fehlermeldungen** liefern +- **Fehler angemessen loggen** + +```python +# Gut ✅ +def load_template_config(template_path: Path) -> Dict[str, Any]: + """Load template configuration from file.""" + config_file = template_path / "template.yaml" + + try: + with open(config_file, 'r') as f: + return yaml.safe_load(f) + except FileNotFoundError: + raise TemplateNotFoundError( + f"Template configuration not found: {config_file}" + ) + except yaml.YAMLError as e: + raise TemplateConfigError( + f"Invalid YAML syntax in {config_file}: {e}" + ) + except PermissionError: + raise TemplateAccessError( + f"Permission denied reading {config_file}" + ) + +# Nicht gut ❌ +def load_template_config(template_path: Path) -> Dict[str, Any]: + """Load template configuration from file.""" + config_file = template_path / "template.yaml" + + try: + with open(config_file, 'r') as f: + return yaml.safe_load(f) + except Exception as e: + raise Exception(f"Error loading config: {e}") +``` + +### Benutzerdefinierte Exceptions + +Definieren Sie spezifische Exceptions für unterschiedliche Fehlerbedingungen: + +```python +class FastKitError(Exception): + """Base exception for FastAPI-fastkit errors.""" + pass + +class ProjectCreationError(FastKitError): + """Raised when project creation fails.""" + pass + +class TemplateNotFoundError(FastKitError): + """Raised when template is not found.""" + pass + +class ValidationError(FastKitError): + """Raised when input validation fails.""" + + def __init__(self, message: str, field: str = None): + super().__init__(message) + self.field = field +``` + +## Test-Standards + +### Teststruktur + +Organisieren Sie Tests mit klarer Struktur und klarer Benennung: + +```python +class TestProjectCreation: + """Test project creation functionality.""" + + def test_create_project_with_valid_name(self, tmp_path): + """Test project creation with valid project name.""" + project_name = "test-project" + result = create_project(project_name, template="minimal", output=tmp_path) + + assert result.success is True + assert (tmp_path / project_name).exists() + assert (tmp_path / project_name / "src" / "main.py").exists() + + def test_create_project_with_invalid_name(self): + """Test project creation fails with invalid name.""" + with pytest.raises(ValueError, match="Invalid project name"): + create_project("invalid-project-name!", template="minimal") + + def test_create_project_overwrites_existing(self, tmp_path): + """Test project creation overwrites existing directory when forced.""" + project_name = "existing-project" + project_dir = tmp_path / project_name + project_dir.mkdir() + + result = create_project( + project_name, + template="minimal", + output=tmp_path, + overwrite=True + ) + + assert result.success is True + assert project_dir.exists() +``` + +### Testabdeckung + +- **Streben Sie 90 %+ Abdeckung** bei neuem Code an +- **Grenzfälle und Fehlerbedingungen** testen +- **Externe Abhängigkeiten mocken** + +```python +def test_template_download_with_network_error(mock_requests): + """Test template download handles network errors gracefully.""" + mock_requests.get.side_effect = requests.ConnectionError("Network unreachable") + + with pytest.raises(TemplateDownloadError, match="Network error"): + download_template("https://example.com/template.zip") + +def test_file_creation_with_permission_error(mock_open): + """Test file creation handles permission errors.""" + mock_open.side_effect = PermissionError("Permission denied") + + with pytest.raises(FileCreationError, match="Permission denied"): + create_file(Path("/restricted/file.py"), content="test") +``` + +## Import-Richtlinien + +### Import-Organisation + +!!! note + + `isort` ordnet Imports automatisch — Sie können Imports einfach mit `bash scripts/format.sh` ordnen. + +1. **Standardbibliothek** zuerst +2. **Drittanbieter** als Zweites +3. **Lokale Anwendung** zuletzt +4. **Leerzeile** zwischen jeder Gruppe + +```python +# Standard library +import os +import sys +from pathlib import Path +from typing import Dict, List, Optional + +# Third-party +import click +import pydantic +import yaml +from fastapi import FastAPI + +# Local application +from fastapi_fastkit.commands.base import BaseCommand +from fastapi_fastkit.utils.validation import validate_project_name +from fastapi_fastkit.templates import TemplateManager +``` + +### Best Practices für Imports + +- **Wildcard-Imports vermeiden** (`from module import *`) +- **Absolute Imports** für Klarheit verwenden +- **Module importieren, nicht einzelne Symbole**, wenn Sie viele Symbole brauchen + +```python +# Gut ✅ +from fastapi_fastkit.utils import validation, files, formatting + +# Gut ✅ (wenn nur wenige Elemente importiert werden) +from fastapi_fastkit.utils.validation import validate_email, validate_project_name + +# Nicht gut ❌ +from fastapi_fastkit.utils.validation import * + +# Nicht gut ❌ (wenn viele Elemente importiert werden) +from fastapi_fastkit.utils.validation import ( + validate_email, validate_project_name, validate_template_name, + validate_dependencies, validate_python_version, validate_directory +) +``` + +## Sicherheitsrichtlinien + +### Eingabevalidierung + +Nutzereingaben immer validieren und säubern: + +```python +def validate_project_name(name: str) -> str: + """Projektname validieren und bereinigen.""" + if not name: + raise ValueError("Project name cannot be empty") + + if not name.isidentifier(): + raise ValueError("Project name must be a valid Python identifier") + + if name.startswith('_'): + raise ValueError("Project name cannot start with underscore") + + if len(name) > 50: + raise ValueError("Project name too long (max 50 characters)") + + # Potenziell problematische Zeichen entfernen + sanitized = re.sub(r'[^a-zA-Z0-9_-]', '', name) + + return sanitized +``` + +### Dateioperationen + +Seien Sie vorsichtig mit Dateipfaden und -operationen: + +```python +def create_file_safely(file_path: Path, content: str, base_dir: Path) -> None: + """Datei sicher innerhalb des Basisverzeichnisses anlegen.""" + # Pfad auflösen, um Directory-Traversal-Angriffe zu verhindern + resolved_path = file_path.resolve() + resolved_base = base_dir.resolve() + + # Sicherstellen, dass die Datei innerhalb des Basisverzeichnisses liegt + try: + resolved_path.relative_to(resolved_base) + except ValueError: + raise SecurityError(f"File path outside base directory: {file_path}") + + # Übergeordnete Verzeichnisse sicher anlegen + resolved_path.parent.mkdir(parents=True, exist_ok=True) + + # Datei mit passenden Berechtigungen schreiben + resolved_path.write_text(content, encoding='utf-8') + resolved_path.chmod(0o644) # Besitzer: lesen/schreiben, andere: nur lesen +``` + +## Performance-Richtlinien + +### Effiziente Code-Praktiken + +- **Generatoren** für große Datenmengen verwenden +- **Vorzeitige Optimierung vermeiden** +- **Vor dem Optimieren profilen** + +```python +# Gut ✅ - Generator spart Arbeitsspeicher +def process_large_template(template_files: List[Path]) -> Iterator[ProcessedFile]: + """Vorlagendateien effizient verarbeiten.""" + for file_path in template_files: + content = file_path.read_text() + processed_content = process_template_content(content) + yield ProcessedFile(path=file_path, content=processed_content) + +# Nicht gut ❌ - lädt alles in den Arbeitsspeicher +def process_large_template(template_files: List[Path]) -> List[ProcessedFile]: + """Vorlagendateien verarbeiten.""" + results = [] + for file_path in template_files: + content = file_path.read_text() + processed_content = process_template_content(content) + results.append(ProcessedFile(path=file_path, content=processed_content)) + return results +``` + +### Caching + +Cache für teure Operationen nutzen: + +```python +from functools import lru_cache + +@lru_cache(maxsize=128) +def get_template_metadata(template_path: Path) -> TemplateMetadata: + """Get template metadata with caching.""" + config_file = template_path / "template.yaml" + + if not config_file.exists(): + return TemplateMetadata(name=template_path.name) + + config = yaml.safe_load(config_file.read_text()) + return TemplateMetadata.from_config(config) +``` + +## Git-Commit-Richtlinien + +### Format der Commit-Nachricht + +Verwenden Sie das Conventional-Commit-Format: + +``` +type(scope): description + +[optional body] + +[optional footer] +``` + +### Commit-Typen + +- **feat**: neue Funktion +- **fix**: Fehlerbehebung +- **docs**: Dokumentationsänderungen +- **style**: Code-Stil-Änderungen (Formatierung usw.) +- **refactor**: Refactoring +- **test**: Tests hinzufügen oder aktualisieren +- **chore**: Wartungsarbeiten + +### Beispiele + +```bash +# Gut ✅ +feat(cli): add template validation command + +Add new command to validate template structure and configuration. +The command checks for required files, validates YAML syntax, +and ensures template follows conventions. + +Closes #123 + +# Gut ✅ +fix(templates): handle missing dependency files gracefully + +When a template references a requirements file that doesn't exist, +show a clear error message instead of crashing. + +# Nicht gut ❌ +update stuff + +# Nicht gut ❌ +Fixed bug +``` + +## Code-Review-Richtlinien + +### Für Autoren + +Vor dem Einreichen zur Review: + +1. **Alle Tests ausführen** und sicherstellen, dass sie bestehen +2. **Code-Abdeckung prüfen**, damit sie erhalten bleibt +3. **Dokumentation aktualisieren**, falls nötig +4. **Commit-Konventionen befolgen** +5. **Pull-Requests** fokussiert und klein halten + +### Für Reviewer + +Beim Review: + +1. **Funktionsfähigkeit prüfen** — funktioniert es wie beabsichtigt? +2. **Tests überprüfen** — sind Grenzfälle abgedeckt? +3. **Dokumentation verifizieren** — klar und aktuell? +4. **Codestil prüfen** — folgt er den Projekt-Konventionen? +5. **Sicherheit beachten** — potenzielle Schwachstellen? + +### Review-Checkliste + +- [ ] Code folgt den Style-Richtlinien +- [ ] Tests sind umfassend und bestehen +- [ ] Dokumentation ist aktuell +- [ ] Keine Sicherheitslücken +- [ ] Performance-Überlegungen berücksichtigt +- [ ] Fehlerbehandlung ist angemessen +- [ ] Commit-Nachrichten folgen den Konventionen + +## Werkzeuge und Automatisierung + +### Pre-Commit-Hooks + +Wir nutzen Pre-Commit-Hooks zur Durchsetzung der Standards: + +```yaml +# .pre-commit-config.yaml +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-toml + +- repo: local + hooks: + - id: format + name: format + entry: black --config pyproject.toml --check . + language: python + types: [python] + additional_dependencies: ['black>=24.10.0'] + pass_filenames: false + + - id: isort-check + name: isort check + entry: isort --sp pyproject.toml --check-only --diff . + language: python + types: [python] + additional_dependencies: ['isort>=5.13.2'] + pass_filenames: false + + - id: isort-fix + name: isort fix + entry: isort --sp pyproject.toml . + language: python + types: [python] + additional_dependencies: ['isort>=5.13.2'] + pass_filenames: false + + - id: black-fix + name: black fix + entry: black --config pyproject.toml . + language: python + types: [python] + additional_dependencies: ['black>=24.10.0'] + pass_filenames: false + + - id: mypy + name: mypy + entry: mypy --config-file pyproject.toml src + language: python + types: [python] + additional_dependencies: + - mypy>=1.12.0 + - rich>=13.9.2 + - click>=8.1.7 + - pyyaml>=6.0.0 + - types-PyYAML>=6.0.12 + pass_filenames: false + +ci: + autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks + autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate +``` + +!!! note + + Pre-Commit-Hooks verwenden isolierte Python-Umgebungen (`language: python`). + +### IDE-Konfiguration + +Empfohlene VS-Code-Einstellungen: + +```json +{ + "python.linting.enabled": true, + "python.linting.mypyEnabled": true, + "python.formatting.provider": "black", + "python.sortImports.path": "isort", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": true + } +} +``` + +## Nächste Schritte + +Nachdem Sie diese Richtlinien gelesen haben: + +1. **Entwicklungsumgebung einrichten** gemäß [Entwicklungsumgebung einrichten](development-setup.md) +2. **Mit kleinen Beiträgen üben**, um sich vertraut zu machen +3. **Fragen stellen** in GitHub Discussions, falls etwas unklar ist +4. **Vorhandenen Code lesen**, um diese Richtlinien in der Praxis zu sehen + +!!! tip "Schnelle Referenz" + - Verwenden Sie `make check-all`, um zu prüfen, ob Ihr Code allen Richtlinien folgt + - Richten Sie Pre-Commit-Hooks ein, um Probleme früh zu erkennen + - Im Zweifel: Beispiele im bestehenden Code anschauen + - Zögern Sie nicht, in Code-Reviews um Hilfe zu bitten + +Diese Richtlinien zu befolgen hilft, die hohe Code-Qualität von FastAPI-fastkit zu erhalten und macht die Zusammenarbeit für alle einfacher! 🚀 diff --git a/docs/de/contributing/development-setup.md b/docs/de/contributing/development-setup.md new file mode 100644 index 0000000..f14cef6 --- /dev/null +++ b/docs/de/contributing/development-setup.md @@ -0,0 +1,816 @@ +# Entwicklungsumgebung einrichten + +Ein umfassender Leitfaden zum Einrichten einer Entwicklungsumgebung für Beiträge zu FastAPI-fastkit. + +## Voraussetzungen + +Bevor Sie starten, stellen Sie sicher, dass Sie haben: + +- **Python 3.12 oder höher** installiert +- **Git** installiert und konfiguriert +- **Grundkenntnisse** in Python und FastAPI +- **Einen Texteditor oder eine IDE** (VS Code, PyCharm usw.) + +## Schnelles Setup mit Makefile + +FastAPI-fastkit bietet ein Makefile für einfaches Entwicklungs-Setup: + +
+ +```console +$ git clone https://github.com/bnbong/FastAPI-fastkit.git +$ cd FastAPI-fastkit +$ make install-dev +Setting up development environment... +Creating virtual environment... +Installing dependencies... +Installing pre-commit hooks... +✅ Development environment ready! +``` + +
+ +Dieser einzelne Befehl: + +- Installiert das Paket im Editable-Modus mit Dev-Abhängigkeiten +- Richtet Pre-Commit-Hooks ein +- Konfiguriert Entwicklungstools + +!!! note + + Sie sollten eine virtuelle Umgebung erstellen und aktivieren, bevor Sie diesen Befehl ausführen. + +## Manuelles Setup + +Wenn Sie ein manuelles Setup bevorzugen oder das Makefile auf Ihrem System nicht funktioniert: + +### 1. Repository klonen + +
+ +```console +$ git clone https://github.com/bnbong/FastAPI-fastkit.git +$ cd FastAPI-fastkit +``` + +
+ +### 2. Virtuelle Umgebung erstellen + +
+ +```console +$ python -m venv .venv +$ source .venv/bin/activate # On Windows: .venv\Scripts\activate +``` + +
+ +### 3. Abhängigkeiten installieren + +
+ +```console +# Install package in editable mode with development dependencies +$ pip install -e ".[dev]" + +# Or install from requirements files +$ pip install -r requirements.txt +$ pip install -r requirements-dev.txt +``` + +
+ +### 4. Pre-Commit-Hooks einrichten + +
+ +```console +$ pre-commit install +pre-commit installed at .git/hooks/pre-commit +``` + +
+ +### 5. Installation überprüfen + +
+ +```console +$ fastkit --version +fastapi-fastkit, version 1.2.1 + +$ python -m pytest tests/ +======================== test session starts ======================== +collected 45 items +tests/test_cli.py::test_init_command PASSED +tests/test_templates.py::test_template_listing PASSED +... +======================== 45 passed in 2.34s ======================== +``` + +
+ +## Entwicklungstools + +Die Entwicklungsumgebung enthält mehrere Tools zur Code-Qualität: + +### Einzeilen-Befehle + +mit Makefile: + +```console +$ make format lint +Running isort... +Running black... +Running mypy... +✅ All checks passed! +``` + +mit den mitgelieferten Skripten: + +```console +$ ./scripts/format.sh +$ ./scripts/lint.sh +``` + +### Code-Formatierung + +**Black** — Code-Formatter: + +
+ +```console +$ black src/ tests/ +reformatted src/main.py +reformatted tests/test_cli.py +All done! ✨ 🍰 ✨ +``` + +
+ +**isort** — Import-Sortierer: + +
+ +```console +$ isort src/ tests/ +Fixing import order in src/main.py +``` + +
+ +### Code-Linting + +**mypy** — Typprüfung: + +
+ +```console +$ mypy src/ +Success: no issues found in 12 source files +``` + +
+ +## Verfügbare Make-Befehle + +Das Projekt-Makefile bietet praktische Befehle für gängige Entwicklungsaufgaben: + +### Setup-Befehle + +| Befehl | Beschreibung | +|---------|-------------| +| `make install` | Paket im Produktionsmodus installieren | +| `make install-dev` | Paket mit Dev-Abhängigkeiten installieren | +| `make install-test` | Paket für Tests installieren (deinstallieren + neu installieren) | +| `make uninstall` | Paket deinstallieren | +| `make clean` | Build-Artefakte und Cache-Dateien löschen | + +### Befehle zur Code-Qualität + +| Befehl | Beschreibung | +|---------|-------------| +| `make format` | Code mit black und isort formatieren | +| `make format-check` | Formatierung prüfen, ohne Änderungen vorzunehmen | +| `make lint` | Alle Linting-Prüfungen ausführen (isort, black, mypy) | + +### Test-Befehle + +| Befehl | Beschreibung | +|---------|-------------| +| `make test` | Alle Tests ausführen | +| `make test-verbose` | Tests mit ausführlicher Ausgabe ausführen | +| `make test-coverage` | Tests mit Coverage-Bericht ausführen | +| `make coverage-report` | Detaillierten Coverage-Bericht erzeugen (FORMAT=html/xml/json/all) | + +### Befehle zur Vorlagen-Inspektion + +| Befehl | Beschreibung | +|---------|-------------| +| `make inspect-templates` | Alle Vorlagen inspizieren | +| `make inspect-templates-verbose` | Mit ausführlicher Ausgabe inspizieren | +| `make inspect-template` | Eine oder mehrere Vorlagen inspizieren (Parameter TEMPLATES) | + +### Dokumentations-Befehle + +| Befehl | Beschreibung | +|---------|-------------| +| `make serve-docs` | Dokumentation lokal bereitstellen | +| `make build-docs` | Dokumentation bauen | + +### Übersetzungs-Befehle + +| Befehl | Beschreibung | +|---------|-----------| +| `make translate` | Dokumentation übersetzen (Parameter LANG, PROVIDER, MODEL) | + +### Beispiele + +
+ +```console +# Format code and run all checks +$ make format lint +Running isort... +Running black... +Running mypy... +✅ All checks passed! + +# Run tests with coverage +$ make test-coverage +======================== test session starts ======================== +collected 45 items +tests/test_cli.py::test_init_command PASSED +... +======================== 45 passed in 2.34s ======================== + +---------- coverage: platform darwin, python 3.12.1-final-0 ---------- +Name Stmts Miss Cover +-------------------------------------------- +src/main.py 45 2 96% +src/cli.py 89 5 94% +src/templates.py 67 3 96% +-------------------------------------------- +TOTAL 201 10 95% + +# Generate HTML coverage report +$ make coverage-report FORMAT=html +🌐 Opening HTML coverage report in browser... + +# Translate documentation to Korean +$ make translate LANG=ko PROVIDER=github MODEL=gpt-4o-mini +Starting translation... +Running: python scripts/translate.py --target-lang ko --api-provider github --model gpt-4o-mini +``` + +
+ +## Projektstruktur + +Die Projektstruktur zu verstehen, ist für die Entwicklung entscheidend: + +```bash +FastAPI-fastkit/ +├── src/ +│ ├── fastapi_fastkit/ +│ │ ├── __main__.py # Entry point of the application +│ │ ├── backend/ +│ │ │ ├── inspector.py # FastAPI-fastkit template inspector +│ │ │ ├── interactive/ +│ │ │ │ ├── config_builder.py # Configuration builder for interactive mode +│ │ │ │ ├── prompts.py # Prompts for interactive mode +│ │ │ │ ├── selectors.py # Selectors logic for interactive mode +│ │ │ │ └── validators.py # User input validators for interactive mode +│ │ │ ├── main.py # Backend's logic entry point +│ │ │ ├── package_managers/ +│ │ │ │ ├── base.py # Base class for package managers +│ │ │ │ ├── factory.py # Factory for package managers +│ │ │ │ ├── pdm_manager.py # PDM package manager +│ │ │ │ ├── pip_manager.py # pip package manager +│ │ │ │ ├── poetry_manager.py # Poetry package manager +│ │ │ │ └── uv_manager.py # uv package manager +│ │ │ ├── project_builder/ +│ │ │ │ ├── config_generator.py # Configuration generator for project builder +│ │ │ │ └── dependency_collector.py # Dependency collector for project builder +│ │ │ └── transducer.py # Transducer for project builder +│ │ ├── cli.py # FastAPI-fastkit main CLI entry point +│ │ ├── core/ +│ │ │ ├── exceptions.py # Exception handling +│ │ │ └── settings.py # Settings configuration +│ │ ├── fastapi_project_template/ +│ │ │ ├── PROJECT_README_TEMPLATE.md # fastkit template project base README file +│ │ │ ├── README.md # fastkit template README +│ │ │ ├── fastapi-async-crud/ +│ │ │ ├── fastapi-custom-response/ +│ │ │ ├── fastapi-default/ +│ │ │ ├── fastapi-dockerized/ +│ │ │ ├── fastapi-empty/ +│ │ │ ├── fastapi-mcp/ +│ │ │ ├── fastapi-psql-orm/ +│ │ │ ├── fastapi-single-module/ +│ │ │ └── modules/ +│ │ │ ├── api/ +│ │ │ │ └── routes/ +│ │ │ ├── crud/ +│ │ │ └── schemas/ +│ │ ├── py.typed +│ │ └── utils/ +│ │ ├── logging.py # Logging configuration +│ │ └── main.py # FastAPI-fastkit main entry point +│ └── logs +├── tests +│ ├── conftest.py # pytest configuration +│ ├── test_backends/ +│ ├── test_cli_operations/ +│ ├── test_core.py +│ ├── test_rich/ +│ ├── test_templates/ +│ └── test_utils.py +├── uv.lock +├── docs/ # Documentation +├── scripts/ # Development scripts +├── mkdocs.yml +├── overrides/ # mkdocs overrides +├── pdm.lock +├── pyproject.toml +├── requirements-docs.txt # requirements for documentation +├── requirements.txt # requirements for development +├── CHANGELOG.md +├── CITATION.cff +├── CODE_OF_CONDUCT.md +├── CONTRIBUTING.md +├── LICENSE +├── MANIFEST.in +├── Makefile +├── README.md +├── SECURITY.md +└── env.example # environment example(configures translation AI model env vars) +``` + +### Wichtige Verzeichnisse + +- **`src/fastapi_fastkit/`** — Hauptquellcode des Pakets + - **`cli.py`** — Haupteinstiegspunkt des CLI + - **`backend/`** — zentrale Backend-Logik + - **`inspector.py`** — Vorlagen-Inspektor + - **`interactive/`** — Komponenten des interaktiven Modus (Prompts, Selectors, Validators) + - **`package_managers/`** — Implementierungen der Paketmanager (pip, uv, pdm, poetry) + - **`project_builder/`** — Hilfsfunktionen zum Projektbau + - **`transducer.py`** — Vorlagen-Transducer + - **`core/`** — zentrale Konfiguration und Exceptions + - **`fastapi_project_template/`** — Projektvorlagen (fastapi-default, fastapi-async-crud usw.) + - **`utils/`** — gemeinsame Hilfsfunktionen +- **`tests/`** — Test-Suite + - **`test_backends/`** — Backend-spezifische Tests + - **`test_cli_operations/`** — Tests der CLI-Operationen + - **`test_templates/`** — Tests des Vorlagensystems +- **`docs/`** — Dokumentation (MkDocs) + - Nutzerhandbücher, Tutorials und API-Referenz + +## Entwicklungs-Workflow + +### 1. Feature-Branch erstellen + +
+ +```console +$ git checkout -b feature/add-new-template +Switched to a new branch 'feature/add-new-template' +``` + +
+ +### 2. Änderungen vornehmen + +Code bearbeiten, Funktionen ergänzen, Bugs beheben… + +### 3. Tests und Prüfungen ausführen + +
+ +```console +$ make dev-check +Running all quality checks... +Running all tests... +✅ All tests passed! +``` + +
+ +### 4. Änderungen committen + +Pre-Commit-Hooks laufen automatisch: + +
+ +```console +$ git add . +$ git commit -m "Add new FastAPI template with authentication" +format...................................................................Passed +isort-check..............................................................Passed +black-fix................................................................Passed +mypy.....................................................................Passed +[feature/add-new-template abc1234] Add new FastAPI template with authentication +``` + +
+ +### 5. Pushen und Pull-Request erstellen + +
+ +```console +$ git push origin feature/add-new-template +$ gh pr create --title "Add new FastAPI template with authentication" +``` + +
+ +## Testen + +### Tests ausführen + +**Alle Tests:** + +
+ +```console +$ make test +# or +$ python -m pytest +``` + +
+ +**Bestimmte Testdatei:** + +
+ +```console +$ python -m pytest tests/test_cli.py -v +``` + +
+ +**Mit Coverage:** + +
+ +```console +$ make test-coverage +# or +$ python -m pytest --cov=src --cov-report=html +``` + +
+ +### Tests schreiben + +Bei neuen Funktionen immer Tests beifügen: + +```python +# tests/test_commands/test_new_feature.py +import pytest +from fastapi_fastkit.commands.new_feature import NewFeatureCommand + +class TestNewFeatureCommand: + def test_command_success(self): + """Test successful command execution""" + command = NewFeatureCommand() + result = command.execute(valid_args) + assert result.success is True + assert result.message == "Feature executed successfully" + + def test_command_validation_error(self): + """Test command with invalid arguments""" + command = NewFeatureCommand() + with pytest.raises(ValueError, match="Invalid argument"): + command.execute(invalid_args) + + def test_command_edge_case(self): + """Test edge case handling""" + command = NewFeatureCommand() + result = command.execute(edge_case_args) + assert result.success is True + assert "warning" in result.message.lower() +``` + +### Test-Kategorien + +**Unit-Tests** — einzelne Funktionen und Klassen testen: + +```python +def test_validate_project_name(): + assert validate_project_name("valid-name") is True + assert validate_project_name("invalid name!") is False +``` + +**Integrationstests** — Interaktionen zwischen Befehlen testen: + +```python +def test_init_command_creates_project(tmp_path): + result = runner.invoke(cli, ['init'], input='test-project\n...') + assert result.exit_code == 0 + assert (tmp_path / "test-project").exists() +``` + +**End-to-End-Tests** — komplette Flüsse testen: + +```python +def test_full_project_creation_workflow(tmp_path): + # Create project + result = runner.invoke(cli, ['init'], input='...') + assert result.exit_code == 0 + + # Add route + result = runner.invoke(cli, ['addroute', 'test-project', 'users']) + assert result.exit_code == 0 + + # Verify files exist + assert (tmp_path / "test-project" / "src" / "api" / "routes" / "users.py").exists() +``` + +## Dokumentation + +### Dokumentation lokal bereitstellen + +
+ +```console +$ make serve-docs +INFO - Building documentation... +INFO - Cleaning site directory +INFO - Documentation built in 0.43 seconds +INFO - [14:30:00] Serving on http://127.0.0.1:8000/ +``` + +
+ +### Dokumentation bauen + +
+ +```console +$ make build-docs +INFO - Building documentation... +INFO - Documentation built in 0.43 seconds +``` + +
+ +### Dokumentation schreiben + +Die Dokumentation wird in Markdown verfasst und mit MkDocs gebaut. Beispielstruktur: + +**Vorlage für einen Funktions-Leitfaden:** + +````markdown +# New Feature Guide + +This guide explains how to use the new feature. + +## Prerequisites + +- FastAPI-fastkit installed +- Basic Python knowledge + +## Usage + +
+ +```console +$ fastkit new-feature --option value +✅ Feature executed successfully! +``` + +
+ +!!! tip "Pro Tip" + Use `--help` to see all available options. +```` + +Für eine ausführliche Referenz zu `mkdocs-material` siehe [mkdocs-material-Dokumentation](https://squidfunk.github.io/mkdocs-material/reference/admonitions/). + +## Code-Stil-Richtlinien + +### Python-Codestil + +Folgen Sie [PEP 8](https://www.python.org/dev/peps/pep-0008/) mit folgenden Regeln: + +- **Zeilenlänge**: 88 Zeichen (Black-Standard) +- **Imports**: mit isort organisiert +- **Type Hints**: für alle öffentlichen Funktionen erforderlich +- **Docstrings**: Google-Style für alle öffentlichen APIs + +### Beispiel + +```python +from typing import List, Optional +from pathlib import Path + +def create_project_structure( + project_name: str, + template_path: Path, + output_dir: Optional[Path] = None, +) -> List[Path]: + """Create project structure from template. + + Args: + project_name: Name of the project to create + template_path: Path to the template directory + output_dir: Output directory, defaults to current directory + + Returns: + List of created file paths + + Raises: + ValueError: If project_name is invalid + FileNotFoundError: If template_path doesn't exist + """ + if not project_name.isidentifier(): + raise ValueError(f"Invalid project name: {project_name}") + + if not template_path.exists(): + raise FileNotFoundError(f"Template not found: {template_path}") + + # Implementation here... + return created_files +``` + +## Umgebungsvariablen + +Für die Entwicklung können Sie folgende Umgebungsvariablen setzen: + +| Variable | Beschreibung | Standard | +|----------|-------------|---------| +| `FASTKIT_DEBUG` | Debug-Logging aktivieren | `False` | +| `FASTKIT_DEV_MODE` | Entwicklungs-Funktionen aktivieren | `False` | +| `FASTKIT_TEMPLATE_DIR` | Verzeichnis für eigene Vorlagen | Eingebaute Vorlagen | +| `FASTKIT_CONFIG_DIR` | Konfigurationsverzeichnis | `~/.fastkit` | +| `TRANSLATION_API_KEY` | API-Schlüssel für Übersetzungen (verwenden Sie ein GitHub-PAT, wenn Sie den [GitHub-KI-Modell-Anbieter](https://github.com/marketplace/models/azure-openai) nutzen) | `None` | + +
+ +```console +$ export FASTKIT_DEBUG=true +$ export FASTKIT_DEV_MODE=true +$ fastkit init +DEBUG: Loading configuration from /home/user/.fastkit/ +DEBUG: Available templates: ['fastapi-default', ...] +``` + +
+ +Für weitere Einstellungen zu Umgebungsvariablen siehe das Modul [@settings.py](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/core/settings.py). + +## Fehlerbehebung + +### Häufige Probleme + +**1. Pre-Commit-Hooks schlagen fehl:** + +
+ +```console +$ git commit -m "Fix bug" +black....................................................................Failed +hookid: black + +Files were modified by this hook. Additional output: + +would reformat src/cli.py +``` + +
+ +**Lösung:** Formatter ausführen und erneut committen: + +
+ +```console +$ make format +$ git add . +$ git commit -m "Fix bug" +``` + +
+ +**2. Tests schlagen bei unterschiedlichen Python-Versionen fehl:** + +**Lösung:** tox verwenden, um mehrere Python-Versionen zu testen: + +
+ +```console +$ pip install tox +$ tox +py38: commands succeeded +py39: commands succeeded +py310: commands succeeded +py311: commands succeeded +py312: commands succeeded +``` + +
+ +**3. Import-Fehler in der Entwicklung:** + +**Lösung:** Paket im Editable-Modus installieren: +
+ +```console +$ pip install -e . +``` + +
+ +### Hilfe bekommen + +- **[GitHub Issues](https://github.com/bnbong/FastAPI-fastkit/issues)**: Bugs und Feature-Requests melden +- **[GitHub Discussions](https://github.com/bnbong/FastAPI-fastkit/discussions)**: Fragen stellen und Ideen teilen +- **Dokumentation**: siehe [Benutzerhandbuch](../user-guide/installation.md) + +## Beitrags-Richtlinien + +### Vor dem Einreichen einer PR + +1. **Alle Prüfungen ausführen:** `make dev-check` +2. **Dokumentation aktualisieren**, falls nötig +3. **Tests** für neue Funktionen hinzufügen +4. **Commit-Konventionen** befolgen + +### Format der Commit-Nachricht + +``` +type(scope): brief description + +Longer description if needed + +Fixes #123 +``` + +**Typen:** + +- `feat`: neue Funktion +- `fix`: Bugfix +- `docs`: Dokumentationsänderungen +- `style`: Code-Stil-Änderungen +- `refactor`: Refactoring +- `test`: Tests hinzufügen/ändern +- `chore`: Wartungsaufgaben + +**Beispiele:** + +``` +feat(cli): add new template command + +Add support for creating projects from custom templates. +The command accepts a template path and creates a new +project with the specified configuration. + +Fixes #45 + +fix(templates): handle missing template files gracefully + +When a template file is missing, show a clear error message +instead of crashing with a stack trace. + +Fixes #67 +``` + +## Release-Prozess + +Für Maintainer sieht der Release-Prozess so aus: + +1. **Version aktualisieren** in `setup.py` und `__init__.py` +2. **CHANGELOG.md aktualisieren** +3. **Release-PR erstellen** +4. **Release nach dem Merge taggen** +5. **GitHub Actions** baut und veröffentlicht automatisch + +
+ +```console +$ git tag v1.2.0 +$ git push origin v1.2.0 +``` + +
+ +## Nächste Schritte + +Nachdem Ihre Entwicklungsumgebung eingerichtet ist: + +1. **[Code erkunden](https://github.com/bnbong/FastAPI-fastkit/tree/main/src/fastapi_fastkit)**, um die Architektur zu verstehen +2. **Test-Suite ausführen**, um sicherzustellen, dass alles funktioniert +3. **Ein [Issue](https://github.com/bnbong/FastAPI-fastkit/issues)** auf GitHub auswählen +4. **Sich an [Discussions](https://github.com/bnbong/FastAPI-fastkit/discussions)** beteiligen, um sich mit anderen Mitwirkenden zu vernetzen + +Viel Spaß beim Programmieren! 🚀 + +!!! tip "Entwicklungs-Tipps" + - Vor dem Committen `make dev-check` ausführen + - Tests zuerst schreiben (TDD-Ansatz) + - Commits klein und fokussiert halten + - Dokumentation mit neuen Funktionen aktualisieren diff --git a/docs/de/contributing/template-creation-guide.md b/docs/de/contributing/template-creation-guide.md new file mode 100644 index 0000000..0c51521 --- /dev/null +++ b/docs/de/contributing/template-creation-guide.md @@ -0,0 +1,574 @@ +# Leitfaden zur Erstellung von FastAPI-Vorlagen + +Ein umfassender Leitfaden zum Hinzufügen neuer FastAPI-Projektvorlagen zu FastAPI-fastkit. + +## 🎯 Überblick + +Das Hinzufügen einer neuen Vorlage erfolgt in einem Prozess aus 5 Schritten: + +1. **📋 Planung und Entwurf** — Zweck und Struktur der Vorlage definieren +2. **🏗️ Implementierung der Vorlage** — die erforderliche Struktur und Dateien erstellen +3. **🔍 Lokale Validierung** — die Vorlage mit dem Inspektor prüfen +4. **📚 Dokumentation** — README und Nutzungsleitfaden verfassen +5. **🚀 Einreichung und Review** — PR erstellen und Community-Review + +## 📋 Schritt 1: Planung und Entwurf + +### Zweck der Vorlage definieren + +Bevor Sie eine neue Vorlage erstellen, beantworten Sie folgende Fragen: + +- **Welchen einzigartigen Wert bietet diese Vorlage?** +- **Wie unterscheidet sie sich von bestehenden Vorlagen?** +- **Welche Nutzergruppe ist die Zielgruppe?** +- **Welche Tech-Stack-Komponenten wird sie enthalten?** + +### Namenskonvention für Vorlagen + +``` +fastapi-{purpose}-{stack} +``` + +Beispiele: + +- `fastapi-microservice` (Microservice-Vorlage) +- `fastapi-graphql` (GraphQL-Integrationsvorlage) +- `fastapi-auth-jwt` (JWT-Authentifizierungsvorlage) + +### Planung des Tech-Stacks + +Definieren Sie die wichtigsten Technologien vorab: + +```yaml +# Example: fastapi-microservice template +core_dependencies: + - fastapi + - uvicorn + - pydantic + - pydantic-settings + +additional_features: + - sqlalchemy (ORM) + - alembic (migrations) + - redis (caching) + - celery (background tasks) + - pytest (testing) + +development_tools: + - black (code formatting) + - isort (import sorting) + - mypy (type checking) + - pre-commit (Git hooks) +``` + +## 🏗️ Schritt 2: Implementierung der Vorlage + +### Erforderliche Verzeichnisstruktur + +``` +fastapi-{template-name}/ +├── src/ # Application source code +│ ├── main.py-tpl # ✅ FastAPI app entry point (required) +│ ├── __init__.py-tpl +│ ├── api/ # API routers +│ │ ├── __init__.py-tpl +│ │ ├── api.py-tpl # Main API router +│ │ └── routes/ # Individual routes +│ │ ├── __init__.py-tpl +│ │ └── items.py-tpl # Example route +│ ├── core/ # Core configuration +│ │ ├── __init__.py-tpl +│ │ └── config.py-tpl # Settings management +│ ├── crud/ # CRUD logic +│ │ ├── __init__.py-tpl +│ │ └── items.py-tpl +│ ├── schemas/ # Pydantic models +│ │ ├── __init__.py-tpl +│ │ └── items.py-tpl +│ └── utils/ # Utility functions +│ ├── __init__.py-tpl +│ └── helpers.py-tpl +├── tests/ # ✅ Tests (required) +│ ├── __init__.py-tpl +│ ├── conftest.py-tpl # pytest configuration +│ └── test_items.py-tpl # Example tests +├── scripts/ # Scripts +│ ├── format.sh-tpl # Code formatting +│ ├── lint.sh-tpl # Linting +│ ├── run-server.sh-tpl # Server execution +│ └── test.sh-tpl # Test execution +├── pyproject.toml-tpl # ✅ Primary metadata (PEP 621, preferred) +├── setup.py-tpl # 🟡 Legacy metadata (accepted for back-compat) +├── requirements.txt-tpl # 🟡 Optional when pyproject declares deps +├── setup.cfg-tpl # Development tools configuration +├── README.md-tpl # ✅ Project documentation (required) +├── .env-tpl # Environment variables template +└── .gitignore-tpl # Git ignore file +``` + +**Minimal erforderliche Dateien.** Eine Vorlage muss bereitstellen: + +- ein `tests/`-Verzeichnis +- eine `README.md-tpl`-Datei +- mindestens eine Metadaten-Datei: `pyproject.toml-tpl` (bevorzugt, PEP 621) oder `setup.py-tpl` (legacy, weiterhin akzeptiert) +- eine Deklaration von `fastapi` als Abhängigkeit in mindestens einem von: `pyproject.toml-tpl` `[project].dependencies`, `requirements.txt-tpl` oder `setup.py-tpl` `install_requires` + +`requirements.txt-tpl` ist nicht mehr zwingend erforderlich, wenn `pyproject.toml-tpl` `[project].dependencies` deklariert. Moderne Vorlagen SOLLTEN `pyproject.toml-tpl` als primäre Metadaten-Datei adoptieren. + +### Anleitung zum Schreiben der Dateien + +#### 1. main.py-tpl schreiben + +```python +""" +FastAPI application entry point + +This file is the main application for the project created with FastAPI-fastkit. +""" +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from api.api import api_router +from core.config import settings + +# Create FastAPI app (required for inspector validation) +app = FastAPI( + title="", + description="Project created with FastAPI-fastkit", + version="1.0.0", +) + +# CORS middleware configuration +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Register API router +app.include_router(api_router, prefix="/api/v1") + +@app.get("/") +async def root(): + """Root endpoint""" + return {"message": "Hello from !"} + +@app.get("/health") +async def health_check(): + """Health check endpoint""" + return {"status": "healthy"} + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) +``` + +#### 2. pyproject.toml-tpl schreiben (bevorzugt) + +Moderne Vorlagen sollten Metadaten und Abhängigkeiten in einem PEP-621-`pyproject.toml-tpl` deklarieren. Mindestens muss die Datei einen `[project]`-Abschnitt mit `name`, `version`, einer `description` und einer `dependencies`-Liste, die `fastapi` enthält, bereitstellen. Vorlagen müssen außerdem zwei FastAPI-fastkit-Identitätsmarker tragen, damit `is_fastkit_project()` generierte Projekte von unverwandten FastAPI-Projekten im Workspace des Nutzers unterscheiden kann: + +- `[FastAPI-fastkit templated]`-Präfix in `description` +- eine eigene Tabelle `[tool.fastapi-fastkit]` mit `managed = true` + +Die Erkennung akzeptiert jeden der beiden Marker (Vergleich ist case-insensitive). Die Metadaten-Injektion fügt beide bei der Projektgenerierung hinzu, falls die Vorlage sie weglässt — Autoren sollten sie aber explizit angeben. + +```toml +[project] +name = "" +version = "0.1.0" +description = "[FastAPI-fastkit templated] " +authors = [ + {name = "", email = ""}, +] +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "fastapi>=0.115.0", + "uvicorn[standard]>=0.34.0", + "pydantic>=2.10.0", + "pydantic-settings>=2.7.0", + "python-dotenv>=1.0.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.0.0", + "httpx>=0.28.0", +] + +[tool.fastapi-fastkit] +managed = true + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" +``` + +#### 3. requirements.txt-tpl schreiben (optional) + +Optional, wenn `pyproject.toml-tpl` `[project].dependencies` deklariert. Weiterhin nützlich für Vorlagen, die reine `pip`-Workflows bevorzugen. + +```txt +# FastAPI core dependencies (required) +fastapi==0.104.1 +uvicorn[standard]==0.24.0 + +# Data validation +pydantic==2.5.0 +pydantic-settings==2.1.0 + +# Environment variable management +python-dotenv==1.0.0 + +# Database (if needed) +sqlalchemy==2.0.23 +alembic==1.13.0 + +# Development tools +pytest==7.4.3 +pytest-asyncio==0.21.1 +httpx==0.25.2 + +# Code quality +black==23.11.0 +isort==5.12.0 +mypy==1.7.1 +``` + +#### 4. setup.py-tpl schreiben (legacy — optional, falls pyproject vorhanden) + +Erhalten für Legacy-Vorlagen. Neue Vorlagen benötigen diese Datei nicht, wenn sie `pyproject.toml-tpl` mitliefern. + +```python +""" + package setup + +Project created with FastAPI-fastkit. +""" +from setuptools import find_packages, setup + +# Dependencies list (type annotation required) +install_requires: list[str] = [ + "fastapi>=0.104.0", + "uvicorn[standard]>=0.24.0", + "pydantic>=2.5.0", + "pydantic-settings>=2.1.0", + "python-dotenv>=1.0.0", +] + +setup( + name="", + version="1.0.0", + description="[FastAPI-fastkit templated] ", # Identity marker used by is_fastkit_project() + long_description=open("README.md").read(), + long_description_content_type="text/markdown", + author="", + author_email="", + packages=find_packages(), + install_requires=install_requires, + python_requires=">=3.8", + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + ], +) +``` + +#### 5. Test-Dateien schreiben + +```python +# tests/test_items.py-tpl +""" +Items API test module +""" +import pytest +from fastapi.testclient import TestClient +from main import app + +client = TestClient(app) + +def test_read_root(): + """Test root endpoint""" + response = client.get("/") + assert response.status_code == 200 + assert "message" in response.json() + +def test_health_check(): + """Test health check""" + response = client.get("/health") + assert response.status_code == 200 + assert response.json() == {"status": "healthy"} + +def test_create_item(): + """Test item creation""" + item_data = { + "name": "Test Item", + "description": "Test Description" + } + response = client.post("/api/v1/items/", json=item_data) + assert response.status_code == 200 + data = response.json() + assert data["name"] == item_data["name"] + assert data["description"] == item_data["description"] + +def test_read_items(): + """Test reading items list""" + response = client.get("/api/v1/items/") + assert response.status_code == 200 + assert isinstance(response.json(), list) +``` + +## 🔍 Schritt 3: Lokale Validierung + +### Automatisierte Validierungsskripte ausführen + +Sobald Ihre neue Vorlage fertig ist, prüfen Sie sie mit folgenden Befehlen: + +```bash +# Validate all templates +make inspect-templates + +# Validate specific template only +make inspect-template TEMPLATES="fastapi-your-template" + +# Validate with verbose output +python scripts/inspect-templates.py --templates "fastapi-your-template" --verbose +``` + +!!! note + + Wenn Sie eine PR einreichen, läuft der **Template PR Inspection**-Workflow automatisch und validiert Ihre Vorlagenänderungen. Sie erhalten das Feedback direkt in Ihrer PR. + +### Validierungs-Checkliste + +Der Inspektor validiert automatisch die folgenden Punkte: + +#### ✅ Validierung der Dateistruktur + +- [ ] `tests/`-Verzeichnis existiert +- [ ] `README.md-tpl`-Datei existiert +- [ ] Mindestens eine von `pyproject.toml-tpl` (bevorzugt) oder `setup.py-tpl` (legacy) existiert + +#### ✅ Validierung der Dateiendungen + +- [ ] Alle Python-Dateien verwenden die Endung `.py-tpl` +- [ ] Keine `.py`-Dateien existieren + +#### ✅ Validierung der Abhängigkeiten + +- [ ] `fastapi` ist in mindestens einem von Folgendem deklariert: + - [ ] `pyproject.toml-tpl` unter `[project].dependencies` (bevorzugt) + - [ ] `requirements.txt-tpl` + - [ ] `setup.py-tpl` unter `install_requires` + +#### ✅ Validierung der FastAPI-Implementierung + +- [ ] `FastAPI`-Import existiert in `main.py-tpl` +- [ ] App-Erstellung wie `app = FastAPI()` existiert in `main.py-tpl` + +#### ✅ Validierung der Testausführung + +- [ ] Erstellung der virtuellen Umgebung erfolgreich +- [ ] Installation der Abhängigkeiten erfolgreich +- [ ] Alle pytest-Tests bestehen + +#### ✅ Automatisiertes Vorlagen-Testen + +FastAPI-fastkit enthält **automatisierte Vorlagen-Tests**, die umfassende Tests für alle Vorlagen ausführen: + +**Testabdeckung:** + +- ✅ Prozess der Projektanlegung +- ✅ Injektion der Projekt-Metadaten +- ✅ Einrichtung der virtuellen Umgebung +- ✅ Installation der Abhängigkeiten (alle Paketmanager) +- ✅ Validierung der grundlegenden Projektstruktur +- ✅ Identifikation als FastAPI-Projekt + +**Testausführung:** +```console +# Test all templates automatically +$ pytest tests/test_templates/test_all_templates.py -v + +# Test specific template +$ pytest tests/test_templates/test_all_templates.py::TestAllTemplates::test_template_creation[your-template-name] -v +``` + +**Erkennung der Vorlagen-Tests:** +Neue Vorlagen werden **automatisch erkannt** und ohne manuelle Konfiguration getestet: + +1. ✅ **Null Konfiguration**: Vorlage hinzufügen → automatische Tests +2. ✅ **Einheitliche Tests**: gleiche Qualitätsstandards für alle Vorlagen +3. ✅ **Mehrere Paketmanager**: Tests mit UV, PDM, Poetry und PIP +4. ✅ **Umfassende Validierung**: Struktur, Metadaten und Funktionsprüfungen + +**Was das für Sie bedeutet:** + +- 🚀 **Keine zusätzlichen Test-Dateien in der Hauptquelle von `FastAPI-fastkit`**: Ihre Vorlage wird automatisch getestet +- ⚡ **Schnellere Entwicklung**: Fokus auf Vorlageninhalt, nicht auf Test-Setup +- 🛡️ **Qualitätssicherung**: einheitliche Tests über alle Vorlagen hinweg +- 🔄 **CI/CD-Integration**: automatische Tests in Pull-Requests + +**Manuelle Tests bleiben weiterhin nötig:** + +- 🧪 **Vorlagenspezifische Funktionen**: Geschäftslogik und individuelle Features +- 🔧 **Integrationstests**: externe Dienste und komplexe Workflows +- 📱 **End-to-End-Szenarien**: vollständige Nutzerflüsse + +**Best Practices für Tests:** +```console +# 1. Test your template locally +$ fastkit startdemo your-template-name + +# 2. Run automated tests +$ pytest tests/test_templates/test_all_templates.py::TestAllTemplates::test_template_creation[your-template-name] -v + +# 3. Test with different package managers +$ fastkit startdemo your-template-name --package-manager poetry +$ fastkit startdemo your-template-name --package-manager pdm +$ fastkit startdemo your-template-name --package-manager uv +``` + +### Manuelle Validierungs-Checkliste + +Zusätzlich zur automatisierten Validierung manuell überprüfen: + +#### 🔧 Code-Qualität + +- [ ] Code folgt dem PEP-8-Styleguide +- [ ] Angemessene Verwendung von Type Hints +- [ ] Sinnvolle Variablen- und Funktionsnamen +- [ ] Angemessene Kommentare und Docstrings + +#### 🏗️ Architektur + +- [ ] Trennung der Belange (API, Geschäftslogik, Datenzugriff getrennt) +- [ ] Wiederverwendbare Komponenten +- [ ] Skalierbare Struktur +- [ ] Best Practices für Sicherheit angewendet + +#### 📚 Dokumentation + +- [ ] README.md-tpl folgt dem Format von PROJECT_README_TEMPLATE.md +- [ ] Installations- und Ausführungsmethoden beschrieben +- [ ] API-Dokumentation (OpenAPI/Swagger) +- [ ] Erläuterung der Umgebungsvariablen + +## 📚 Schritt 4: Dokumentation + +### README.md-tpl verfassen + +Schreiben Sie auf Basis des Leitfadens [PROJECT_README_TEMPLATE.md](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/PROJECT_README_TEMPLATE.md). + +### Beschreibung der Vorlage schreiben + +Fügen Sie eine Beschreibung Ihrer neuen Vorlage zu `src/fastapi_fastkit/fastapi_project_template/README.md` hinzu: + +```markdown +## fastapi-your-template + +Write a brief description and use cases for your new template here. + +### Features: +- Feature 1 +- Feature 2 +- Feature 3 + +### Use Cases: +- Use case 1 +- Use case 2 +``` + +## 🚀 Schritt 5: Einreichung und Review + +### Checkliste vor der PR + +- [ ] Alle automatisierten Validierungen bestanden (`make inspect-templates`) +- [ ] Code-Formatierung abgeschlossen (`make format`) +- [ ] Linting-Prüfungen bestanden (`make lint`) +- [ ] Alle Tests bestanden (`make test`) +- [ ] Dokumentation abgeschlossen +- [ ] Richtlinien aus CONTRIBUTING.md befolgt + +### PR-Titel und -Beschreibung + +``` +[TEMPLATE] Add fastapi-{template-name} template + +## Overview +Adds a new {purpose} template. + +## Key Features +- Feature 1 +- Feature 2 +- Feature 3 + +## Validation Results +- [ ] Inspector validation passed +- [ ] All tests passed +- [ ] Documentation completed + +## Usage Example +\```bash +fastkit startdemo +# Select template: fastapi-{template-name} +\``` + +## Related Issues +Closes #issue-number +``` + +### Review-Prozess + +1. **Automatisierte Validierung**: GitHub Actions validiert die Vorlage automatisch + - **Template PR Inspection**: führt `inspect-changed-templates.py` bei PRs aus, die Vorlagen ändern + - **Wöchentliche Inspektion**: vollständige Validierung der Vorlagen jeden Mittwoch +2. **Code-Review**: Maintainer und Community prüfen den Code +3. **Tests**: Die Vorlage wird in verschiedenen Umgebungen getestet +4. **Dokumentations-Review**: Genauigkeit und Vollständigkeit der Dokumentation prüfen +5. **Freigabe und Merge**: Merge in den Main-Branch, sobald alle Anforderungen erfüllt sind + +!!! note + + Sie erhalten automatische PR-Kommentare mit den Validierungsergebnissen. Prüfen Sie diese, bevor Sie ein Review anfordern! + +## 🎯 Best Practices + +### Sicherheitsüberlegungen + +- Sensible Informationen über Umgebungsvariablen verwalten +- Korrekte CORS-Konfiguration +- Eingabedaten validieren +- SQL-Injection verhindern + +### Performance-Optimierung + +- Asynchrone Verarbeitung nutzen +- Datenbankabfragen optimieren +- Passende Caching-Strategien +- Antwortkompression konfigurieren + +### Wartbarkeit + +- Klare Codestruktur +- Umfassende Testabdeckung +- Detaillierte Dokumentation +- Logging und Monitoring einrichten + +## 🆘 Brauchen Sie Hilfe? + +- 📖 [Anleitung zur Entwicklungsumgebung](development-setup.md) +- 📋 [Code-Richtlinien](code-guidelines.md) +- 💬 [GitHub Discussions](https://github.com/bnbong/FastAPI-fastkit/discussions) +- 📧 [Maintainer kontaktieren](mailto:bbbong9@gmail.com) + +Eine neue Vorlage hinzuzufügen ist ein wertvoller Beitrag zur FastAPI-fastkit-Community. Ihre Ideen und Ihr Einsatz werden anderen Entwicklern enorm helfen! 🚀 diff --git a/docs/de/contributing/translation-guide.md b/docs/de/contributing/translation-guide.md new file mode 100644 index 0000000..da68bb1 --- /dev/null +++ b/docs/de/contributing/translation-guide.md @@ -0,0 +1,367 @@ +# Übersetzungsleitfaden + +Dieser Leitfaden erklärt, wie Sie Übersetzungen zur Dokumentation von FastAPI-fastkit beitragen. + +## Maßgebliche Referenz und Übersetzungsrichtlinie + +> **Englisch (`en`) ist die maßgebliche Referenz** für die FastAPI-fastkit-Dokumentation. Jede andere Sprache ist ein Übersetzungsziel, das dem Englischen um ein Release oder einzelne Seiten hinterherhängen kann. +> +> Wenn eine übersetzte Seite der englischen Seite widerspricht, **vertrauen Sie der englischen Seite**, bis die Übersetzung nachzieht. Übersetzungen werden in dem Vollständigkeitsgrad ausgeliefert, den die Mitwirkenden erreicht haben — eine teilweise Abdeckung ist normal und zu erwarten. + +Das nutzerseitige Pendant zu dieser Politik ist die Seite [Übersetzungsstatus](../reference/translation-status.md), die die tatsächliche Vollständigkeit jeder Sprache auflistet und erklärt, wie MkDocs noch nicht übersetzte Seiten rendert (kurz: sie fallen auf Englisch zurück). + +Die `CHANGELOG.md` im Stammverzeichnis des Repositorys bleibt ebenfalls als kanonische Releasehistorie auf Englisch. Falls eine Sprache eine `changelog.md`-Seite anbietet, sollte diese auf den kanonischen englischen Changelog verlinken oder ihn einbinden, statt einen separaten übersetzten Changelog zu pflegen — sofern die Projektrichtlinie das später nicht ändert. + +Wenn Sie eine Übersetzung beitragen, aktualisieren Sie auch die Tabelle der Statusseite, damit Nutzer den Stand sehen können, ohne ihn aus dem Sprachumschalter erraten zu müssen. + +## Übersicht + +FastAPI-fastkit nutzt ein automatisiertes Übersetzungssystem auf KI-Basis, um die Dokumentation in mehrere Sprachen zu übersetzen. Das System: + +- Liest die Quelldokumentation auf Englisch +- Übersetzt Inhalte über KI-APIs (OpenAI oder Anthropic) +- Speichert Übersetzungen in sprachspezifischen Verzeichnissen +- Erstellt GitHub-Pull-Requests zur Überprüfung + +Die Automatisierung liefert einen Ausgangspunkt; eine menschliche Überprüfung ist vor dem Merge weiterhin erforderlich. KI-generierte Übersetzungen sollten in ihren PRs als „draft" markiert und von einem fließenden Sprecher überprüft werden, bevor sie gemergt werden. + +## Unterstützte Sprachen + +Dies sind die Sprachen, für die die Dokumentations-Site derzeit einen Build erzeugt. Die bloße Konfiguration eines Sprach-Build-Ziels **bedeutet nicht**, dass die Seiten dieser Sprache bereits übersetzt sind — den tatsächlichen Stand sehen Sie im [Übersetzungsstatus](../reference/translation-status.md). + +- 🇰🇷 Koreanisch (ko) +- 🇯🇵 Japanisch (ja) +- 🇨🇳 Chinesisch (zh) +- 🇪🇸 Spanisch (es) +- 🇫🇷 Französisch (fr) +- 🇩🇪 Deutsch (de) + +## Voraussetzungen + +### 1. Abhängigkeiten für Übersetzungen installieren + +```bash +# Mit pip installieren +pip install openai anthropic + +# Oder mit pdm +pdm install -G translation +``` + +### 2. API-Schlüssel einrichten + +Sie benötigen einen API-Schlüssel von OpenAI oder Anthropic: + +```bash +# Für OpenAI +export TRANSLATION_API_KEY="sk-..." + +# Oder für Anthropic +export TRANSLATION_API_KEY="sk-ant-..." +``` + +### 3. GitHub CLI installieren (optional) + +Für die automatische PR-Erstellung: + +```bash +# macOS +brew install gh + +# Linux +curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg +echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null +sudo apt update +sudo apt install gh + +# Anmelden +gh auth login +``` + +## Nutzung + +### Mit Make-Befehlen (empfohlen) + +Der einfachste Weg, Übersetzungen auszuführen: + +```bash +# Alle Dokumente in alle Sprachen übersetzen +make translate + +# In eine bestimmte Sprache übersetzen +make translate LANG=ko + +# API-Anbieter und Modell festlegen +make translate LANG=ko PROVIDER=openai MODEL=gpt-4 +make translate LANG=ko PROVIDER=github MODEL=gpt-4o-mini +``` + +### Mit dem Skript direkt + +#### Die gesamte Dokumentation übersetzen + +Die gesamte Dokumentation in alle unterstützten Sprachen übersetzen: + +```bash +python scripts/translate.py --api-provider openai +``` + +### In eine bestimmte Sprache übersetzen + +Nur ins Koreanische übersetzen: + +```bash +python scripts/translate.py --target-lang ko --api-provider openai +``` + +### Bestimmte Dateien übersetzen + +Nur bestimmte Dokumentationsdateien übersetzen: + +```bash +python scripts/translate.py \ + --target-lang ko \ + --files user-guide/installation.md user-guide/quick-start.md \ + --api-provider openai +``` + +### PR-Erstellung überspringen + +Übersetzen, ohne eine GitHub-PR zu erstellen: + +```bash +python scripts/translate.py --target-lang ko --no-pr --api-provider openai +``` + +### Anthropic Claude verwenden + +Anthropic Claude statt OpenAI verwenden: + +```bash +python scripts/translate.py \ + --target-lang ko \ + --api-provider anthropic \ + --api-key "sk-ant-..." +``` + +## Verzeichnisstruktur + +Nach der Übersetzung sieht die Dokumentationsstruktur so aus: + +``` +docs/ +├── en/ # Englisch (Original) +│ ├── index.md +│ ├── user-guide/ +│ │ ├── installation.md +│ │ ├── quick-start.md +│ │ └── ... +│ ├── tutorial/ +│ └── ... +├── ko/ # Koreanisch +│ ├── index.md +│ ├── user-guide/ +│ └── ... +├── ja/ # Japanisch +├── zh/ # Chinesisch +├── es/ # Spanisch +├── fr/ # Französisch +├── de/ # Deutsch +├── css/ # Gemeinsame Assets +├── js/ # Gemeinsame Assets +└── img/ # Gemeinsame Assets +``` + +## Übersetzungs-Workflow + +### 1. Dokumentation auf Englisch schreiben + +Alle Dokumentation sollte zuerst auf Englisch im Verzeichnis `docs/` geschrieben werden: + +```bash +# Neue Dokumentationsseite anlegen +vim docs/user-guide/new-feature.md +``` + +### 2. Übersetzung starten + +Sobald die englische Dokumentation fertig ist, führen Sie das Übersetzungsskript aus: + +```bash +python scripts/translate.py --target-lang ko +``` + +### 3. Pull-Request prüfen + +Das Skript erstellt eine Pull-Request mit den Übersetzungen. Prüfen Sie die PR: + +1. Markdown-Formatierung wurde erhalten +2. Technische Begriffe sind korrekt behandelt +3. Code-Beispiele bleiben unverändert +4. Sprachspezifische Eigenheiten beachten + +### Changelog-Politik + +- Halten Sie die `CHANGELOG.md` im Stammverzeichnis auf Englisch. +- Eröffnen Sie keine Übersetzungs-PRs, deren Ziel es ist, die Releasehistorie im Stamm-Changelog in einer anderen Sprache neu zu schreiben. +- Falls eine Sprache eine Changelog-Seite benötigt, behandeln Sie `docs//changelog.md` als Einstiegsseite oder Verweis auf den kanonischen englischen Changelog. + +### 4. Freigeben und mergen (für Maintainer) + +Sobald die Übersetzung verifiziert ist: + +```bash +gh pr review --approve +gh pr merge +``` + +### 5. Dokumentation veröffentlichen + +Die Dokumentations-Site wird automatisch mit den neuen Übersetzungen neu gebaut. + +## Übersetzungs-Konfiguration + +Bearbeiten Sie `scripts/translation_config.json`, um anzupassen: + +```json +{ + "source_language": "en", + "target_languages": [ + { + "code": "ko", + "name": "Korean", + "native_name": "한국어", + "enabled": true + } + ], + "translation_settings": { + "default_api_provider": "openai", + "batch_size": 5, + "preserve_formatting": true + }, + "github_settings": { + "create_pr_by_default": true, + "branch_prefix": "translation" + } +} +``` + +## Best Practices + +### Für die Quelldokumentation + +1. **Klare Sprache verwenden**: schreiben Sie klares, einfaches Englisch, das sich gut übersetzen lässt +2. **Einheitliche Terminologie**: verwenden Sie einheitliche Fachbegriffe +3. **Saubere Code-Blöcke**: geben Sie immer die Sprache im Code-Block an +4. **Linkprüfung**: stellen Sie sicher, dass alle internen Links relative Pfade nutzen + +### Für die Überprüfung von Übersetzungen + +1. **Fachbegriffe**: prüfen Sie, dass Fachbegriffe für die Zielsprache angemessen sind +2. **Kultureller Kontext**: prüfen Sie, ob Beispiele lokalisiert werden müssen +3. **Formatierung**: stellen Sie sicher, dass die gesamte Markdown-Formatierung erhalten bleibt +4. **Code-Integrität**: prüfen Sie, dass Code-Blöcke unverändert sind + +## Fehlerbehebung + +### API-Ratenbeschränkungen + +Bei API-Ratenlimits in kleineren Chargen übersetzen: + +```bash +# Translate only user guide +python scripts/translate.py \ + --target-lang ko \ + --files user-guide/*.md +``` + +### Probleme mit der Übersetzungsqualität + +Wenn die Übersetzungen schlecht sind: + +1. Prüfen Sie, dass Ihr API-Schlüssel gültig ist +2. Versuchen Sie einen anderen KI-Anbieter +3. Zerlegen Sie komplexe Dokumente in kleinere Abschnitte +4. Manuell überprüfen und nachbearbeiten + +### GitHub-PR schlägt fehl + +Wenn die PR-Erstellung fehlschlägt: + +```bash +# Translate without PR +python scripts/translate.py --target-lang ko --no-pr + +# Manually create PR +git checkout -b translation/ko +git add docs/ko/ +git commit -m "Add Korean translations" +git push -u origin translation/ko +gh pr create --title "Add Korean translations" +``` + +## Manuelle Übersetzung + +Sie können auch manuell übersetzen: + +1. Englische Datei in das Zielsprachenverzeichnis kopieren: +```bash +mkdir -p docs/ko/user-guide +cp docs/en/user-guide/installation.md docs/ko/user-guide/installation.md +``` + +2. Die Datei in Ihrem bevorzugten Editor bearbeiten +3. Committen und eine PR erstellen + +## Sprachumschalter + +Die Dokumentations-Site bietet einen Sprachumschalter in der oberen Navigation. Nutzer können: + +1. Auf den Sprachumschalter klicken +2. Ihre bevorzugte Sprache wählen +3. Durch die übersetzte Dokumentation navigieren + +## Neue Sprachen beitragen + +Um eine neue Sprache hinzuzufügen: + +1. `scripts/translation_config.json` bearbeiten: +```json +{ + "code": "pt", + "name": "Portuguese", + "native_name": "Português", + "enabled": true +} +``` + +2. `mkdocs.yml` aktualisieren: +```yaml +- locale: pt + name: Português + build: true +``` + +3. Übersetzung starten: +```bash +python scripts/translate.py --target-lang pt +``` + +## Brauchen Sie Hilfe? + +- **Issues**: Übersetzungsprobleme auf [GitHub Issues](https://github.com/bnbong/FastAPI-fastkit/issues) melden +- **Discussions**: Fragen in [GitHub Discussions](https://github.com/bnbong/FastAPI-fastkit/discussions) stellen +- **Mitwirken**: siehe [CONTRIBUTING.md](https://github.com/bnbong/FastAPI-fastkit/blob/main/CONTRIBUTING.md) + +## Qualitätsstandards für Übersetzungen + +Alle Übersetzungen müssen diese Standards erfüllen: + +- ✅ Die gesamte Markdown-Formatierung erhalten +- ✅ Code-Blöcke unverändert lassen +- ✅ Fachterminologie angemessen beibehalten +- ✅ Korrekte Grammatik und Rechtschreibung verwenden +- ✅ Sprachspezifische Konventionen befolgen +- ✅ Alle Links auf Funktionsfähigkeit testen + +Vielen Dank, dass Sie zu den FastAPI-fastkit-Übersetzungen beitragen! 🌍 diff --git a/docs/de/index.md b/docs/de/index.md new file mode 100644 index 0000000..86f1b04 --- /dev/null +++ b/docs/de/index.md @@ -0,0 +1,577 @@ +

+ FastAPI-fastkit +

+

+FastAPI-fastkit: schnelles, einfach zu nutzendes Starter-Kit für Neulinge in Python und FastAPI +

+

+ + PyPI - Version + + + GitHub Release + + + PyPI Downloads + +

+ +--- + +Dieses Projekt wurde entwickelt, um die Konfiguration der Entwicklungsumgebung zu beschleunigen, die für die Entwicklung von Python-basierten Web-Apps für Neulinge in Python und [FastAPI](https://github.com/fastapi/fastapi) nötig ist. + +Inspiriert wurde dieses Projekt vom `SpringBoot initializer` und vom CLI-Werkzeug `django-admin` von Python Django. + +!!! info "Übersetzungsstatus" + Englisch ist die maßgebliche Quelle dieser Dokumentation. Andere Sprachen im Sprachumschalter können unvollständig sein oder seitenweise auf die englische Version zurückfallen. Siehe [Übersetzungsstatus](reference/translation-status.md) für die tatsächliche Abdeckung jeder Locale. + +## Hauptmerkmale + +- **⚡ Sofortige Erstellung von FastAPI-Projekten**: blitzschnelles Anlegen von FastAPI-Workspaces und -Projekten über CLI, inspiriert vom `django-admin`-Werkzeug von [Python Django](https://github.com/django/django) +- **✨ Interaktiver Projekt-Builder**: geführte schrittweise Auswahl von Funktionen für Datenbanken, Authentifizierung, Caching, Monitoring und mehr, mit automatisch generiertem Code +- **🎨 Schönere CLI-Ausgaben**: ansprechende CLI-Erfahrung dank der [rich-Bibliothek](https://github.com/Textualize/rich) +- **📋 Auf Standards basierende FastAPI-Projektvorlagen**: alle Vorlagen von FastAPI-fastkit folgen Python-Standards und den verbreiteten FastAPI-Nutzungsmustern +- **🔍 Automatisierte Qualitätssicherung der Vorlagen**: wöchentliche automatisierte Tests stellen sicher, dass alle Vorlagen funktionsfähig und aktuell bleiben +- **🚀 Mehrere Projektvorlagen**: Auswahl aus verschiedenen vorkonfigurierten Vorlagen für unterschiedliche Anwendungsfälle (async CRUD, Docker, PostgreSQL usw.) +- **📦 Unterstützung mehrerer Paketmanager**: wählen Sie Ihren bevorzugten Python-Paketmanager (pip, uv, pdm, poetry) für die Verwaltung der Abhängigkeiten + +## Installation + +Installieren Sie `FastAPI-fastkit` in Ihrer Python-Umgebung. + +
+ +```console +$ pip install FastAPI-fastkit +---> 100% +``` + +
+ + +## Nutzung + +### Sofort eine neue FastAPI-Projekt-Workspace-Umgebung erstellen + +Sie können jetzt sehr schnell ein neues FastAPI-Projekt mit FastAPI-fastkit starten! + +Erstellen Sie sofort einen neuen FastAPI-Projekt-Workspace mit: + +
+ +```console +$ fastkit init +Enter the project name: my-awesome-project +Enter the author name: John Doe +Enter the author email: john@example.com +Enter the project description: My awesome FastAPI project + + Project Information +┌──────────────┬────────────────────────────┐ +│ Project Name │ my-awesome-project │ +│ Author │ John Doe │ +│ Author Email │ john@example.com │ +│ Description │ My awesome FastAPI project │ +└──────────────┴────────────────────────────┘ + +Available Stacks and Dependencies: + MINIMAL Stack +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ pydantic │ +│ Dependency 4 │ pydantic-settings │ +└──────────────┴───────────────────┘ + + STANDARD Stack +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ sqlalchemy │ +│ Dependency 4 │ alembic │ +│ Dependency 5 │ pytest │ +│ Dependency 6 │ pydantic │ +│ Dependency 7 │ pydantic-settings │ +└──────────────┴───────────────────┘ + + FULL Stack +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ sqlalchemy │ +│ Dependency 4 │ alembic │ +│ Dependency 5 │ pytest │ +│ Dependency 6 │ redis │ +│ Dependency 7 │ celery │ +│ Dependency 8 │ pydantic │ +│ Dependency 9 │ pydantic-settings │ +└──────────────┴───────────────────┘ + +Select stack (minimal, standard, full): minimal + +Available Package Managers: + Package Managers +┌────────┬────────────────────────────────────────────┐ +│ PIP │ Standard Python package manager │ +│ UV │ Fast Python package manager │ +│ PDM │ Modern Python dependency management │ +│ POETRY │ Python dependency management and packaging │ +└────────┴────────────────────────────────────────────┘ + +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y +FastAPI project will deploy at '~your-project-path~' + +╭──────────────────────── Info ────────────────────────╮ +│ ℹ Injected metadata into setup.py │ +╰──────────────────────────────────────────────────────╯ +╭──────────────────────── Info ────────────────────────╮ +│ ℹ Injected metadata into config file │ +╰──────────────────────────────────────────────────────╯ + + Creating Project: + my-awesome-project +┌───────────────────┬───────────┐ +│ Component │ Collected │ +│ fastapi │ ✓ │ +│ uvicorn │ ✓ │ +│ pydantic │ ✓ │ +│ pydantic-settings │ ✓ │ +└───────────────────┴───────────┘ + +Creating virtual environment... + +╭──────────────────────── Info ────────────────────────╮ +│ ℹ venv created at │ +│ ~your-project-path~/my-awesome-project/.venv │ +│ To activate the virtual environment, run: │ +│ │ +│ source │ +│ ~your-project-path~/my-awesome-project/.venv/bin/act │ +│ ivate │ +╰──────────────────────────────────────────────────────╯ + +Installing dependencies... +⠙ Setting up project environment...Collecting + +---> 100% + +╭─────────────────────── Success ───────────────────────╮ +│ ✨ Dependencies installed successfully │ +╰───────────────────────────────────────────────────────╯ +╭─────────────────────── Success ───────────────────────╮ +│ ✨ FastAPI project 'my-awesome-project' has been │ +│ created successfully and saved to │ +│ ~your-project-path~! │ +╰───────────────────────────────────────────────────────╯ +╭──────────────────────── Info ────────────────────────╮ +│ ℹ To start your project, run 'fastkit runserver' at │ +│ newly created FastAPI project directory │ +╰──────────────────────────────────────────────────────╯ +``` + +
+ +Dieser Befehl erstellt eine neue FastAPI-Projekt-Workspace-Umgebung mit einer virtuellen Python-Umgebung. + +### Ein Projekt im interaktiven Modus erstellen ✨ NEU! + +Für komplexere Projekte nutzen Sie den **interaktiven Modus**, um Ihre FastAPI-Anwendung Schritt für Schritt mit intelligenter Funktionsauswahl aufzubauen: + +
+ +```console +$ fastkit init --interactive + +⚡ FastAPI-fastkit Interactive Project Setup ⚡ + +📋 Basic Project Information +Enter the project name: my-fullstack-project +Enter the author name: John Doe +Enter the author email: john@example.com +Enter the project description: Full-stack FastAPI project with PostgreSQL and JWT + +🧱 Architecture Preset +Pick a project layout. Press Enter to accept the recommended default. + 1. minimal - Smallest viable FastAPI app + 2. single-module - Everything in one module (prototypes / scripts) + 3. classic-layered - api/routes + crud + schemas + core (à la fastapi-default) + 4. domain-starter - Domain-oriented src/app/domains// (recommended) + +Select architecture preset: [4] + +🗄️ Database Selection +Select database (PostgreSQL, MySQL, MongoDB, Redis, SQLite, None): + 1. PostgreSQL - PostgreSQL database with SQLAlchemy + 2. MySQL - MySQL database with SQLAlchemy + 3. MongoDB - MongoDB with motor async driver + 4. Redis - Redis for caching and session storage + 5. SQLite - SQLite database for development + 6. None - No database + +Select database: 1 + +🔐 Authentication Selection +Select authentication (JWT, OAuth2, FastAPI-Users, Session-based, None): + 1. JWT - JSON Web Token authentication + 2. OAuth2 - OAuth2 with password flow + 3. FastAPI-Users - Full featured user management + 4. Session-based - Cookie-based sessions + 5. None - No authentication + +Select authentication: 1 + +⚙️ Background Tasks Selection +Select background tasks (Celery, Dramatiq, None): + 1. Celery - Distributed task queue + 2. Dramatiq - Fast and reliable task processing + 3. None - No background tasks + +Select background tasks: 1 + +💾 Caching Selection +Select caching (Redis, fastapi-cache2, None): + 1. Redis - Redis caching + 2. fastapi-cache2 - Simple caching for FastAPI + 3. None - No caching + +Select caching: 1 + +📊 Monitoring Selection +Select monitoring (Loguru, OpenTelemetry, Prometheus, None): + 1. Loguru - Simple and powerful logging + 2. OpenTelemetry - Observability framework + 3. Prometheus - Metrics and monitoring + 4. None - No monitoring + +Select monitoring: 3 + +🧪 Testing Framework Selection +Select testing framework (Basic, Coverage, Advanced, None): + 1. Basic - pytest + httpx for API testing + 2. Coverage - Basic + code coverage + 3. Advanced - Coverage + faker + factory-boy for fixtures + 4. None - No testing framework + +Select testing framework: 2 + +🛠️ Additional Utilities +Select utilities (comma-separated numbers, e.g., 1,3,4): + 1. CORS - Cross-Origin Resource Sharing + 2. Rate-Limiting - Request rate limiting + 3. Pagination - Pagination support + 4. WebSocket - WebSocket support + +Select utilities: 1 + +🚀 Deployment Configuration +Select deployment option: + 1. Docker - Generate Dockerfile + 2. docker-compose - Generate docker-compose.yml (includes Docker) + 3. None - No deployment configuration + +Select deployment option: 2 + +📦 Package Manager Selection +Select package manager (pip, uv, pdm, poetry): uv + +📝 Custom Packages (optional) +Enter custom package names (comma-separated, press Enter to skip): + +📋 Project Configuration Summary +┌─────────────────────┬───────────────────────────────────────────────────────────────────────────┐ +│ Setting │ Value │ +├─────────────────────┼───────────────────────────────────────────────────────────────────────────┤ +│ Project Name │ my-fullstack-project │ +│ Author │ John Doe │ +│ Email │ john@example.com │ +│ Description │ Full-stack FastAPI project with PostgreSQL and JWT │ +│ Architecture Preset │ domain-starter — Domain-oriented: src/app/domains// (recommended)│ +│ Database │ PostgreSQL │ +│ Authentication │ JWT │ +│ Async Tasks │ Celery │ +│ Caching │ Redis │ +│ Monitoring │ Prometheus │ +│ Testing │ Coverage │ +│ Utilities │ CORS │ +│ Package Manager │ uv │ +└─────────────────────┴───────────────────────────────────────────────────────────────────────────┘ + +Total dependencies to install: 18 + +Proceed with project creation? [Y/n]: y + +╭──────────────────────── Info ────────────────────────╮ +│ ℹ Injected metadata into pyproject.toml │ +╰──────────────────────────────────────────────────────╯ +╭─────────────────────── Success ───────────────────────╮ +│ ✨ Generated dependency file with 18 packages │ +╰───────────────────────────────────────────────────────╯ +╭──────────────────────── Info ────────────────────────╮ +│ ℹ Preserving template-shipped main.py for preset │ +│ 'domain-starter'. │ +╰──────────────────────────────────────────────────────╯ +╭─────────────────────── Success ───────────────────────╮ +│ ✨ Generated Docker deployment files │ +╰───────────────────────────────────────────────────────╯ +╭────────────────────── Warning ────────────────────────╮ +│ ⚠ Preset compatibility │ +│ fastapi-domain-starter's shipped src/app/main.py is │ +│ preserved. The selections below need manual wiring │ +│ there (CORS is already wired — set │ +│ BACKEND_CORS_ORIGINS in .env to activate it). │ +│ Affected selections (packages installed, but no │ +│ dynamic main.py edits applied for the │ +│ 'domain-starter' preset): Prometheus │ +╰───────────────────────────────────────────────────────╯ +╭─────────────────────── Success ───────────────────────╮ +│ ✨ Generated configuration files for selected stack │ +╰───────────────────────────────────────────────────────╯ + +Creating virtual environment... +Installing dependencies... + +----> 100% + +╭─────────────────────── Success ───────────────────────╮ +│ ✨ FastAPI project 'my-fullstack-project' from │ +│ 'fastapi-domain-starter' has been created! │ +╰───────────────────────────────────────────────────────╯ +``` + +
+ +Der interaktive Modus bietet: + +- **Auswahl eines Architektur-Presets** (`minimal` / `single-module` / `classic-layered` / `domain-starter`), das die passende Basisvorlage und das Projekt-Layout auswählt +- **Geführte Auswahl** für Datenbanken, Authentifizierung, Hintergrundaufgaben, Caching, Monitoring und mehr +- **Automatisch generierter Code** für die ausgewählten Funktionen — je nach Preset unterschiedlich (regenerierte `main.py` für `minimal` / `single-module`; bei `classic-layered` / `domain-starter` bleibt die mitgelieferte `main.py` erhalten und passende Konfigurationsmodule werden ergänzt) +- **Docker-Generierung passend zum gewählten Preset** — das `CMD` des generierten `Dockerfile` verweist auf den tatsächlichen Einstiegspunkt des Presets (`src.main:app` oder `src.app.main:app`) +- **Intelligente Verwaltung der Abhängigkeiten** mit automatischer pip-Kompatibilität +- **Funktionsvalidierung** mit Hinweisen auf manuelle Verkabelung für Auswahlmöglichkeiten, die das Preset nicht automatisch verdrahten kann +- **Identitätsmarker** in der generierten `pyproject.toml` (Beschreibungs-Marker + `[tool.fastapi-fastkit]`-Tabelle), damit `is_fastkit_project()` generierte Projekte später wiedererkennt + +### Eine neue Route zum FastAPI-Projekt hinzufügen + +`FastAPI-fastkit` macht es einfach, Ihr FastAPI-Projekt zu erweitern. + +Fügen Sie einen neuen Routen-Endpunkt zu Ihrem FastAPI-Projekt hinzu mit: + +
+ +```console +$ fastkit addroute user my-awesome-project + Adding New Route +┌──────────────────┬──────────────────────────────────────────┐ +│ Project │ my-awesome-project │ +│ Route Name │ user │ +│ Target Directory │ ~your-project-path~ │ +└──────────────────┴──────────────────────────────────────────┘ + +Do you want to add route 'user' to project 'my-awesome-project'? [Y/n]: y + +╭──────────────────────── Info ────────────────────────╮ +│ ℹ Updated main.py to include the API router │ +╰──────────────────────────────────────────────────────╯ +╭─────────────────────── Success ───────────────────────╮ +│ ✨ Successfully added new route 'user' to project │ +│ `my-awesome-project` │ +╰───────────────────────────────────────────────────────╯ +``` + +
+ +### Sofort ein strukturiertes FastAPI-Demoprojekt anlegen + +Sie können auch mit einem strukturierten FastAPI-Demoprojekt starten. + +Demoprojekte bestehen aus verschiedenen Tech-Stacks mit implementierten einfachen Item-CRUD-Endpunkten. + +Legen Sie sofort ein strukturiertes FastAPI-Demoprojekt an mit: + +
+ +```console +$ fastkit startdemo +Enter the project name: my-awesome-demo +Enter the author name: John Doe +Enter the author email: john@example.com +Enter the project description: My awesome FastAPI demo +Deploying FastAPI project using 'fastapi-default' template +Template path: +/~fastapi_fastkit-package-path~/fastapi_project_template/fastapi-default + + Project Information +┌──────────────┬─────────────────────────┐ +│ Project Name │ my-awesome-demo │ +│ Author │ John Doe │ +│ Author Email │ john@example.com │ +│ Description │ My awesome FastAPI demo │ +└──────────────┴─────────────────────────┘ + + Template Dependencies +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ pydantic │ +│ Dependency 4 │ pydantic-settings │ +│ Dependency 5 │ python-dotenv │ +└──────────────┴───────────────────┘ + +Available Package Managers: + Package Managers +┌────────┬────────────────────────────────────────────┐ +│ PIP │ Standard Python package manager │ +│ UV │ Fast Python package manager │ +│ PDM │ Modern Python dependency management │ +│ POETRY │ Python dependency management and packaging │ +└────────┴────────────────────────────────────────────┘ + +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y +FastAPI template project will deploy at '~your-project-path~' + +---> 100% + +╭─────────────────────── Success ───────────────────────╮ +│ ✨ Dependencies installed successfully │ +╰───────────────────────────────────────────────────────╯ +╭─────────────────────── Success ───────────────────────╮ +│ ✨ FastAPI project 'my-awesome-demo' from │ +│ 'fastapi-default' has been created and saved to │ +│ ~your-project-path~! │ +╰───────────────────────────────────────────────────────╯ +``` + +
+ +Um die Liste der verfügbaren FastAPI-Demos anzuzeigen, prüfen Sie mit: + +
+ +```console +$ fastkit list-templates + Available Templates +┌────────────────────────┬───────────────────────────────────────────────────────┐ +│ fastapi-custom-response│ Async Item Management API with Custom Response System │ +│ fastapi-mcp │ FastAPI MCP Project │ +│ fastapi-domain-starter │ FastAPI Domain Starter │ +│ fastapi-dockerized │ Dockerized FastAPI Item Management API │ +│ fastapi-empty │ Minimal FastAPI Template │ +│ fastapi-async-crud │ Async Item Management API Server │ +│ fastapi-psql-orm │ Dockerized FastAPI Item Management API with │ +│ │ PostgreSQL │ +│ fastapi-default │ Simple FastAPI Project │ +│ fastapi-single-module │ FastAPI Single Module Template │ +└────────────────────────┴───────────────────────────────────────────────────────┘ +``` + +
+ +## Dokumentation + +Für umfassende Anleitungen und detaillierte Nutzungshinweise erkunden Sie unsere Dokumentation: + +- 📚 **[Benutzerhandbuch](user-guide/quick-start.md)** — ausführliche Installations- und Nutzungsanleitungen +- 🎯 **[Tutorial](tutorial/getting-started.md)** — Schritt-für-Schritt-Tutorials für Einsteiger +- 📖 **[CLI-Referenz](user-guide/cli-reference.md)** — vollständige Befehlsreferenz +- 🔍 **[Qualitätssicherung der Vorlagen](reference/template-quality-assurance.md)** — automatisierte Tests und Qualitätsstandards + +## 🚀 Vorlagenbasierte Tutorials + +Lernen Sie die FastAPI-Entwicklung anhand praktischer Anwendungsfälle mit unseren gebrauchsfertigen Vorlagen: + +### 📖 Kerntutorials + +- **[Einen einfachen API-Server bauen](tutorial/basic-api-server.md)** — erstellen Sie Ihren ersten FastAPI-Server mit der Vorlage `fastapi-default` +- **[Eine asynchrone CRUD-API bauen](tutorial/async-crud-api.md)** — entwickeln Sie eine leistungsstarke asynchrone API mit der Vorlage `fastapi-async-crud` +- **[Domänenorientiertes Projekt (Domain Starter)](tutorial/domain-starter.md)** — bauen Sie eine mittelgroße API mit der Vorlage `fastapi-domain-starter`, dem empfohlenen modernen Standard + +### 🗄️ Datenbank und Infrastruktur + +- **[Integration einer Datenbank](tutorial/database-integration.md)** — nutzen Sie PostgreSQL + SQLAlchemy mit der Vorlage `fastapi-psql-orm` +- **[Docker-Containerisierung und Deployment](tutorial/docker-deployment.md)** — richten Sie eine produktionsreife Deployment-Umgebung mit der Vorlage `fastapi-dockerized` ein + +### ⚡ Erweiterte Funktionen + +- **[Benutzerdefinierte Antwortbehandlung und fortgeschrittenes API-Design](tutorial/custom-response-handling.md)** — bauen Sie Enterprise-APIs mit der Vorlage `fastapi-custom-response` +- **[Integration mit MCP](tutorial/mcp-integration.md)** — erstellen Sie einen API-Server, der mit KI-Modellen integriert ist, mit der Vorlage `fastapi-mcp` + +Jedes Tutorial bietet: + +- ✅ **Praktische Beispiele** — Code, den Sie direkt in echten Projekten verwenden können +- ✅ **Schritt-für-Schritt-Anleitungen** — detaillierte Erklärungen, denen Einsteiger leicht folgen können +- ✅ **Best Practices** — branchenübliche Muster und Sicherheitserwägungen +- ✅ **Erweiterungsmethoden** — Hinweise, wie Sie Ihr Projekt auf die nächste Stufe heben + +## Mitwirken + +Wir freuen uns über Beiträge aus der Community! FastAPI-fastkit soll Einsteigern in Python und FastAPI helfen, und Ihre Beiträge können einen großen Unterschied machen. + +### Was Sie beitragen können + +- 🚀 **Neue FastAPI-Vorlagen** — fügen Sie Vorlagen für verschiedene Anwendungsfälle hinzu +- 🐛 **Bugfixes** — helfen Sie uns, Stabilität und Zuverlässigkeit zu verbessern +- 📚 **Dokumentation** — verbessern Sie Anleitungen, Beispiele und Übersetzungen +- 🧪 **Tests** — erhöhen Sie die Testabdeckung und ergänzen Sie Integrationstests +- 💡 **Funktionen** — schlagen Sie neue CLI-Funktionen vor und implementieren Sie diese + +### Erste Schritte zum Mitwirken + +Um mit der Mitarbeit an FastAPI-fastkit zu beginnen, sehen Sie sich unsere umfassenden Leitfäden an: + +- **[Entwicklungsumgebung einrichten](contributing/development-setup.md)** — vollständige Anleitung zum Einrichten Ihrer Entwicklungsumgebung +- **[Code-Richtlinien](contributing/code-guidelines.md)** — Programmierstandards und Best Practices +- **[CONTRIBUTING.md](https://github.com/bnbong/FastAPI-fastkit/blob/main/CONTRIBUTING.md)** — umfassender Beitragsleitfaden +- **[CODE_OF_CONDUCT.md](https://github.com/bnbong/FastAPI-fastkit/blob/main/CODE_OF_CONDUCT.md)** — Projektprinzipien und Community-Standards +- **[SECURITY.md](https://github.com/bnbong/FastAPI-fastkit/blob/main/SECURITY.md)** — Sicherheitsrichtlinien und Meldungen + +## Bedeutung von FastAPI-fastkit + +FastAPI-fastkit zielt darauf ab, ein schnelles und einfach zu nutzendes Starter-Kit für Neulinge in Python und FastAPI bereitzustellen. + +Diese Idee entstand mit dem Ziel, FastAPI-Einsteigern beim Lernen von Anfang an zu helfen — im Einklang mit der produktiven Bedeutung des FastAPI-cli-Pakets, das mit dem [FastAPI-0.111.0-Update](https://github.com/fastapi/fastapi/releases/tag/0.111.0) eingeführt wurde. + +Als jemand, der FastAPI seit langem nutzt und liebt, wollte ich ein Projekt entwickeln, das dazu beiträgt, [die wunderbare Motivation](https://github.com/fastapi/fastapi/pull/11522#issuecomment-2264639417) zu erfüllen, die der FastAPI-Entwickler [tiangolo](https://github.com/tiangolo) zum Ausdruck gebracht hat. + +FastAPI-fastkit schlägt eine Brücke zwischen dem Einstieg und dem Bau produktionsreifer Anwendungen, indem es Folgendes bietet: + +- **Sofortige Produktivität** für Neueinsteiger, die von der Einrichtung überfordert sein könnten +- **Best Practices** in jeder Vorlage, die Anwendern beim Erlernen korrekter FastAPI-Muster helfen +- **Skalierbare Grundlagen**, die mit den Anwendern wachsen, vom Anfänger zum Experten +- **Community-getriebene Vorlagen**, die reale FastAPI-Nutzungsmuster widerspiegeln + +## Nächste Schritte + +Bereit, mit FastAPI-fastkit zu starten? Folgen Sie diesen nächsten Schritten: + +### 🚀 Schnellstart + +1. **[Installation](user-guide/installation.md)**: FastAPI-fastkit installieren +2. **[Schnellstart](user-guide/quick-start.md)**: erstellen Sie Ihr erstes Projekt in 5 Minuten +3. **[Tutorial – Erste Schritte](tutorial/getting-started.md)**: detailliertes Schritt-für-Schritt-Tutorial + +### 📚 Vertiefung + +- **[Projekte erstellen](user-guide/creating-projects.md)**: Projekte mit verschiedenen Stacks erstellen +- **[Routen hinzufügen](user-guide/adding-routes.md)**: fügen Sie API-Endpunkte zu Ihrem Projekt hinzu +- **[Vorlagen verwenden](user-guide/using-templates.md)**: nutzen Sie vorgefertigte Projektvorlagen + +### 🛠️ Mitwirken + +Möchten Sie zu FastAPI-fastkit beitragen? + +- **[Entwicklungsumgebung einrichten](contributing/development-setup.md)**: richten Sie Ihre Entwicklungsumgebung ein +- **[Code-Richtlinien](contributing/code-guidelines.md)**: folgen Sie unseren Programmierstandards und Best Practices +- **[Contributing-Richtlinien](https://github.com/bnbong/FastAPI-fastkit/blob/main/CONTRIBUTING.md)**: umfassender Beitragsleitfaden + +### 🔍 Referenz + +- **[CLI-Referenz](user-guide/cli-reference.md)**: vollständige Referenz der CLI-Befehle +- **[Qualitätssicherung der Vorlagen](reference/template-quality-assurance.md)**: automatisierte Tests und Qualitätsstandards +- **[FAQ](reference/faq.md)**: häufig gestellte Fragen +- **[GitHub-Repository](https://github.com/bnbong/FastAPI-fastkit)**: Quellcode und Issue-Tracking + +## Lizenz + +Dieses Projekt steht unter der MIT-Lizenz — Details siehe die Datei [LICENSE](https://github.com/bnbong/FastAPI-fastkit/blob/main/LICENSE). diff --git a/docs/de/reference/faq.md b/docs/de/reference/faq.md new file mode 100644 index 0000000..b9f71db --- /dev/null +++ b/docs/de/reference/faq.md @@ -0,0 +1,784 @@ +# Häufig gestellte Fragen + +Allgemeine Fragen und Antworten zu FastAPI-fastkit. + +## Installation und Setup + +### F. Welche Python-Versionen werden unterstützt? + +**A.** FastAPI-fastkit erfordert **Python 3.12 oder höher**. Wir empfehlen, die neueste stabile Python-Version für die beste Erfahrung zu verwenden. + +
+ +```console +$ python --version +Python 3.12.1 + +$ pip install fastapi-fastkit +``` + +
+ +### F. Wie installiere ich FastAPI-fastkit? + +**A.** Sie können FastAPI-fastkit mit pip installieren: + +
+ +```console +# Latest stable version +$ pip install fastapi-fastkit + +# Development version from GitHub +$ pip install git+https://github.com/bnbong/FastAPI-fastkit.git + +# Specific version +$ pip install fastapi-fastkit==1.0.0 +``` + +
+ +### F. Die Installation schlägt mit Berechtigungsfehlern fehl + +**A.** Versuchen Sie es in einer virtuellen Umgebung oder mit Nutzerberechtigungen: + +
+ +```console +# Create virtual environment +$ python -m venv fastapi-env +$ source fastapi-env/bin/activate # On Windows: fastapi-env\Scripts\activate + +# Install in virtual environment +$ pip install fastapi-fastkit + +# Or install for current user only +$ pip install --user fastapi-fastkit +``` + +
+ +### F. Der Befehl `fastkit` wird nach der Installation nicht gefunden + +**A.** Das bedeutet meist, dass das Installationsverzeichnis nicht im PATH liegt: + +
+ +```console +# Check if installed +$ pip show fastapi-fastkit + +# Find installation location +$ python -c "import fastapi_fastkit; print(fastapi_fastkit.__file__)" + +# Try running directly +$ python -m fastapi_fastkit --version + +# Or add to PATH (Linux/macOS) +$ export PATH="$HOME/.local/bin:$PATH" +``` + +
+ +## Projekterstellung + +### F. Welche Abhängigkeits-Stacks gibt es? + +**A.** FastAPI-fastkit bietet drei Abhängigkeits-Stacks: + +- **MINIMAL**: FastAPI, Uvicorn, Pydantic, Pydantic-Settings (einfache Web-API) +- **STANDARD**: ergänzt SQLAlchemy, Alembic, pytest (Datenbankunterstützung) +- **FULL**: ergänzt Redis, Celery (Hintergrundaufgaben) + +!!! tip "Standard-Paketmanager" + Der Standard-Paketmanager ist `uv` für schnellere Installation. Sie können auch `pip`, `pdm` oder `poetry` wählen. + +
+ +```console +$ fastkit init +# Select your preferred stack during project creation +``` + +
+ +### F. Kann ich die Projektvorlage anpassen? + +**A.** Ja! Sie können: + +1. **Vorhandene Vorlagen nutzen** mit `fastkit startdemo` +2. **Eigene Vorlagen** durch Kopieren und Anpassen bestehender Vorlagen erstellen +3. **Routen schrittweise hinzufügen** mit `fastkit addroute` + +
+ +```console +# Use pre-built templates +$ fastkit list-templates +$ fastkit startdemo + +# Add routes to existing project +$ fastkit addroute users . # Add 'users' route to current directory +$ fastkit addroute users my-project # Add 'users' route to 'my-project' +``` + +
+ +### F. Wie erstelle ich ein Projekt mit einem bestimmten Namensformat? + +**A.** Projektnamen müssen gültige Python-Identifier sein: + +- ✅ `my-api`, `blog_system`, `UserService` +- ❌ `my api`, `123project`, `project-name!` + +
+ +```console +$ fastkit init +Enter the project name: my_awesome_api # Valid +Enter the project name: my-awesome-api # Valid (hyphens converted to underscores) +``` + +
+ +### F. Projekterstellung schlägt mit „directory already exists" fehl + +**A.** Das Projektverzeichnis existiert bereits. Wählen Sie: + +1. **Einen anderen Namen** +2. **Das vorhandene Verzeichnis entfernen** (sofern unbedenklich) +3. **Einen anderen Ablageort verwenden** + +
+ +```console +# Check if directory exists +$ ls my-project + +# Remove if safe (CAUTION!) +$ rm -rf my-project + +# Or create in different location +$ mkdir projects +$ cd projects +$ fastkit init +``` + +
+ +### F. Wie nutze ich den interaktiven Modus zur Projekteinrichtung? + +**A.** Verwenden Sie `fastkit init --interactive` für eine geführte, schrittweise Einrichtung mit intelligenter Funktionsauswahl: + +
+ +```console +$ fastkit init --interactive +``` + +
+ +Der interaktive Modus führt Sie der Reihe nach durch diese Schritte: + +1. **Projektinformationen** — Name, Autor, E-Mail, Beschreibung. +2. **Architektur-Preset** — wählt das Projekt-Layout. Die empfohlene Standardlösung ist `domain-starter`; drücken Sie Enter, um ihn zu übernehmen. In der [Preset-/Funktionsmatrix](preset-feature-matrix.md) sehen Sie, welches Layout jedes Preset erzeugt und welche Funktionskombinationen noch manuell eingebunden werden müssen. +3. **Funktionsauswahl** — Datenbank, Authentifizierung, Hintergrundaufgaben, Caching, Monitoring, Tests, Utilities, Deployment. +4. **Paketmanager und eigene Pakete** — pip / uv / pdm / poetry, plus zusätzliche Pakete, die Sie fixieren möchten. +5. **Bestätigung** — eine Übersichtstabelle zeigt jede Auswahl (einschließlich Architektur-Preset), bevor das Projekt erstellt wird. + +Der interaktive Modus erlaubt Ihnen, aus einem umfassenden Funktionskatalog zu wählen: + +| Kategorie | Verfügbare Optionen | +|----------|-------------------| +| **Architektur** | minimal, single-module, classic-layered, **domain-starter** (empfohlene Standardlösung) | +| **Datenbank** | PostgreSQL, MySQL, MongoDB, Redis, SQLite | +| **Authentifizierung** | JWT, OAuth2, FastAPI-Users, sessionbasiert | +| **Hintergrundaufgaben** | Celery, Dramatiq | +| **Tests** | Basic (pytest), Coverage, Advanced (mit faker, factory-boy) | +| **Caching** | Redis mit fastapi-cache2 | +| **Monitoring** | Loguru, OpenTelemetry, Prometheus | +| **Utilities** | CORS, Rate-Limiting, Pagination, WebSocket | +| **Deployment** | Docker, docker-compose mit automatisch generierten Konfigurationen | + +Der interaktive Modus erzeugt automatisch: + +- `main.py` mit den ausgewählten Funktionen integriert +- Datenbank- und Authentifizierungs-Konfigurationsdateien, sofern die ausgewählten Optionen Code-Generierung unterstützen (z. B. PostgreSQL/MySQL/SQLite/MongoDB für Datenbanken, JWT/FastAPI-Users für Authentifizierung); andere Optionen installieren nur die nötigen Pakete +- Deployment-Dateien passend zur gewählten Deployment-Option (`Dockerfile`, wenn `Docker` gewählt ist, `docker-compose.yml`, wenn `docker-compose` gewählt ist) +- Testkonfiguration basierend auf der gewählten Testoption (Coverage-Einstellungen werden nur eingefügt, wenn `Coverage` oder `Advanced` gewählt ist) + +### F. Wie sehe ich die verfügbaren Funktionen für den interaktiven Modus? + +**A.** Verwenden Sie den Befehl `list-features`, um alle verfügbaren Funktionen und ihre Pakete anzuzeigen: + +
+ +```console +$ fastkit list-features +# Shows all available features organized by category +# with their associated packages +``` + +
+ +So sehen Sie, welche Pakete für jede Auswahl installiert würden. + +## Routenentwicklung + +### F. Wie füge ich Authentifizierung zu meinen Routen hinzu? + +**A.** Erstellen Sie eine Dependency für die Authentifizierung: + +```python +# src/api/deps.py +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPBearer + +security = HTTPBearer() + +def get_current_user(token: str = Depends(security)): + # Verify token and return user + if not verify_token(token): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid authentication credentials" + ) + return get_user_from_token(token) + +# src/api/routes/users.py +@router.get("/me") +def get_current_user_profile(user = Depends(get_current_user)): + return user +``` + +### F. Wie füge ich Datenbank-Modelle zu meinem Projekt hinzu? + +**A.** Für die Stacks STANDARD oder FULL erstellen Sie SQLAlchemy-Modelle: + +```python +# src/models/users.py +from sqlalchemy import Column, Integer, String, Boolean +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() + +class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + email = Column(String, unique=True, index=True) + username = Column(String, unique=True, index=True) + hashed_password = Column(String) + is_active = Column(Boolean, default=True) +``` + +### F. Wie validiere ich Anfragedaten? + +**A.** Verwenden Sie Pydantic-Modelle in Ihren Schemas: + +```python +# src/schemas/users.py +from pydantic import BaseModel, EmailStr, Field + +class UserCreate(BaseModel): + email: EmailStr + username: str = Field(..., min_length=3, max_length=50) + password: str = Field(..., min_length=8) + + @validator('username') + def validate_username(cls, v): + if not v.isalnum(): + raise ValueError('Username must be alphanumeric') + return v +``` + +### F. Wie verarbeite ich Datei-Uploads? + +**A.** Verwenden Sie das `UploadFile` von FastAPI: + +```python +from fastapi import UploadFile, File + +@router.post("/upload") +async def upload_file(file: UploadFile = File(...)): + contents = await file.read() + + # Save file + with open(f"uploads/{file.filename}", "wb") as f: + f.write(contents) + + return {"filename": file.filename, "size": len(contents)} +``` + +## Vorlagen + +### F. Welche Vorlagen sind verfügbar? + +**A.** FastAPI-fastkit liefert mehrere einsatzbereite Vorlagen: + +
+ +```console +$ fastkit list-templates + Available Templates +┌─────────────────────────┬───────────────────────────────────┐ +│ fastapi-default │ Simple FastAPI Project │ +│ fastapi-async-crud │ Async Item Management API Server │ +│ fastapi-custom-response │ Custom Response System │ +│ fastapi-dockerized │ Dockerized FastAPI API │ +│ fastapi-empty │ Minimal FastAPI Project │ +│ fastapi-mcp │ MCP (Model Context Protocol) API │ +│ fastapi-psql-orm │ PostgreSQL FastAPI API │ +│ fastapi-single-module │ Single-file FastAPI Project │ +└─────────────────────────┴───────────────────────────────────┘ +``` + +
+ +### F. Wie nutze ich eine bestimmte Vorlage? + +**A.** Verwenden Sie den Befehl `startdemo`: + +
+ +```console +$ fastkit startdemo +Enter the project name: my-blog +Select template: fastapi-psql-orm +``` + +
+ +### F. Kann ich eigene Vorlagen erstellen? + +**A.** Ja! Legen Sie eine Verzeichnisstruktur an und nutzen Sie Vorlagenvariablen: + +``` +my-template/ +├── src/ +│ └── main.py-tpl +├── requirements.txt-tpl +└── template.yaml +``` + +```python +# main.py-tpl +from fastapi import FastAPI + +app = FastAPI(title="{{PROJECT_NAME}}") + +@app.get("/") +def read_root(): + return {"message": "Hello from {{PROJECT_NAME}}!"} +``` + +### F. Wie passe ich eine bestehende Vorlage an? + +**A.** Vorlagen liegen im Verzeichnis `fastapi_project_template`. Sie können: + +1. **Das Repository forken** und Vorlagen anpassen +2. **Eine eigene Vorlage** basierend auf vorhandenen erstellen +3. **Einzelne Dateien überschreiben**, nachdem das Projekt erstellt wurde + +## Entwicklungsserver + +### F. Wie starte ich den Entwicklungsserver? + +**A.** Verwenden Sie den Befehl `runserver` aus dem Projektverzeichnis: + +
+ +```console +$ cd my-project +$ source .venv/bin/activate # Activate virtual environment +$ fastkit runserver +INFO: Uvicorn running on http://127.0.0.1:8000 +``` + +
+ +### F. Der Server startet nicht — „Address already in use" + +**A.** Port 8000 ist belegt. Wählen Sie einen anderen Port oder beenden Sie den bestehenden Prozess: + +
+ +```console +# Use different port +$ fastkit runserver --port 8080 + +# Or find and kill existing process +$ lsof -ti:8000 | xargs kill -9 + +# On Windows +$ netstat -ano | findstr :8000 +$ taskkill /PID /F +``` + +
+ +### F. Automatisches Neuladen funktioniert nicht + +**A.** Stellen Sie sicher, dass Sie im Projektverzeichnis sind und die virtuelle Umgebung aktiv ist: + +
+ +```console +# Check current directory +$ pwd +/path/to/my-project + +# Check virtual environment +$ which python +/path/to/my-project/.venv/bin/python + +# Start with explicit reload +$ fastkit runserver --reload +``` + +
+ +### F. Wie konfiguriere ich den Server für die Produktion? + +**A.** Verwenden Sie den Entwicklungsserver nicht in der Produktion. Stattdessen: + +```python +# Use gunicorn or similar WSGI server +$ pip install gunicorn +$ gunicorn src.main:app -w 4 -k uvicorn.workers.UvicornWorker + +# Or use Docker with the fastapi-dockerized template +$ fastkit startdemo # Select fastapi-dockerized +$ docker build -t my-app . +$ docker run -p 8000:8000 my-app +``` + +## Performance und Optimierung + +### F. Wie verbessere ich die API-Leistung? + +**A.** Mehrere Optimierungsstrategien: + +1. **async/await für I/O** nutzen +2. **Caching für teure Operationen** einsetzen +3. **Datenbankabfragen optimieren** +4. **Hintergrundaufgaben für intensive Verarbeitung** nutzen + +```python +# Async endpoint +@router.get("/users/{user_id}") +async def get_user(user_id: int): + user = await users_service.get_user_async(user_id) + return user + +# Background task +from fastapi import BackgroundTasks + +@router.post("/send-email") +def send_email(background_tasks: BackgroundTasks, email: str): + background_tasks.add_task(send_notification_email, email) + return {"message": "Email will be sent in background"} +``` + +### F. Wie füge ich Caching hinzu? + +**A.** Verwenden Sie Redis als Cache: + +```python +import redis +from functools import wraps + +redis_client = redis.Redis(host='localhost', port=6379, db=0) + +def cache_result(expiration: int = 300): + def decorator(func): + @wraps(func) + async def wrapper(*args, **kwargs): + cache_key = f"{func.__name__}:{hash(str(args) + str(kwargs))}" + + # Try to get from cache + cached = redis_client.get(cache_key) + if cached: + return json.loads(cached) + + # Execute function and cache result + result = await func(*args, **kwargs) + redis_client.setex(cache_key, expiration, json.dumps(result)) + return result + return wrapper + return decorator + +@cache_result(expiration=600) +async def get_expensive_data(): + # Expensive operation + return complex_calculation() +``` + +### F. Wie verarbeite ich viele parallele Anfragen? + +**A.** Verwenden Sie eine passende Server-Konfiguration: + +
+ +```console +# Development +$ fastkit runserver --workers 1 # Single worker for development + +# Production +$ gunicorn src.main:app -w 4 -k uvicorn.workers.UvicornWorker +$ uvicorn src.main:app --workers 4 --host 0.0.0.0 --port 8000 +``` + +
+ +## Testen + +### F. Wie führe ich Tests aus? + +**A.** Verwenden Sie pytest aus dem Projektverzeichnis: + +
+ +```console +$ cd my-project +$ source .venv/bin/activate +$ python -m pytest + +# With coverage +$ python -m pytest --cov=src + +# Specific test file +$ python -m pytest tests/test_users.py + +# With verbose output +$ python -m pytest -v +``` + +
+ +### F. Wie schreibe ich API-Tests? + +**A.** Verwenden Sie den Test-Client von FastAPI: + +```python +from fastapi.testclient import TestClient +from src.main import app + +client = TestClient(app) + +def test_create_user(): + response = client.post( + "/api/v1/users/", + json={"email": "test@example.com", "username": "testuser"} + ) + assert response.status_code == 201 + assert response.json()["email"] == "test@example.com" + +def test_get_user(): + response = client.get("/api/v1/users/1") + assert response.status_code == 200 +``` + +### F. Wie mocke ich externe Abhängigkeiten? + +**A.** Verwenden Sie pytest-Fixtures und Mocking: + +```python +import pytest +from unittest.mock import Mock, patch + +@pytest.fixture +def mock_database(): + with patch('src.database.get_db') as mock_db: + mock_db.return_value = Mock() + yield mock_db + +def test_user_creation_with_mock_db(mock_database): + # Test with mocked database + response = client.post("/api/v1/users/", json=user_data) + assert response.status_code == 201 +``` + +## Mitwirken + +### F. Wie kann ich zu FastAPI-fastkit beitragen? + +**A.** Folgen Sie diesen Schritten: + +1. **Fork** des Repositorys auf GitHub +2. **Entwicklungsumgebung einrichten** +3. **Feature-Branch erstellen** +4. **Änderungen mit Tests** vornehmen +5. **Pull-Request einreichen** + +
+ +```console +$ git clone https://github.com/yourusername/FastAPI-fastkit.git +$ cd FastAPI-fastkit +$ make dev-setup # Set up development environment +$ git checkout -b feature/my-feature +# Make changes... +$ make dev-check # Format, lint, and test +$ git commit -m "feat: add new feature" +$ git push origin feature/my-feature +``` + +
+ +### F. Was sollte eine Pull-Request enthalten? + +**A.** Jede Pull-Request sollte enthalten: + +- [ ] **Klare Beschreibung** der Änderungen +- [ ] **Tests** für neue Funktionen +- [ ] **Dokumentations-Updates**, falls nötig +- [ ] **Code-Richtlinien** befolgen +- [ ] **Alle Prüfungen grün** + +### F. Wie melde ich einen Bug? + +**A.** Erstellen Sie ein Issue auf GitHub mit: + +1. **Fehlerbeschreibung** und erwartetem Verhalten +2. **Schritten zur Reproduktion** +3. **Umgebungsinformationen** (OS, Python-Version usw.) +4. **Fehlermeldungen** oder Logs +5. **Minimalbeispiel**, falls möglich + +### F. Wie schlage ich eine neue Funktion vor? + +**A.** Öffnen Sie ein Feature-Request-Issue mit: + +1. **Klarer Beschreibung** der Funktion +2. **Anwendungsfall** und Motivation +3. **Implementierungsvorschlag** (optional) +4. **Beispielen** ähnlicher Funktionen + +## Fehlerbehebung + +### F. Ich bekomme Import-Fehler + +**A.** Prüfen Sie Ihren Python-Pfad und die virtuelle Umgebung: + +
+ +```console +# Check virtual environment is activated +$ which python +/path/to/project/.venv/bin/python + +# Check Python path +$ python -c "import sys; print(sys.path)" + +# Reinstall in editable mode (for development) +$ pip install -e . +``` + +
+ +### F. Probleme mit der Datenbankverbindung + +**A.** Bei Datenbankvorlagen sicherstellen, dass die Datenbank läuft: + +
+ +```console +# PostgreSQL template +$ docker-compose up -d postgres # Start database +$ alembic upgrade head # Run migrations + +# Check connection +$ docker-compose logs postgres +``` + +
+ +### F. Vorlagendateien werden nicht gefunden + +**A.** Das deutet meist auf ein Problem mit dem Vorlagenpfad hin: + +
+ +```console +# Check available templates +$ fastkit list-templates + +# Check template directory +$ python -c "import fastapi_fastkit; print(fastapi_fastkit.__path__)" + +# Reinstall if templates missing +$ pip uninstall fastapi-fastkit +$ pip install fastapi-fastkit +``` + +
+ +### F. Pre-Commit-Hooks schlagen fehl + +**A.** Installieren und ausführen: + +
+ +```console +$ pip install pre-commit +$ pre-commit install +$ pre-commit run --all-files + +# Fix formatting issues +$ black src/ tests/ +$ isort src/ tests/ +``` + +
+ +### F. Tests scheitern in CI, laufen aber lokal durch + +**A.** Häufige Ursachen und Lösungen: + +1. **Umgebungsunterschiede**: prüfen, ob die Python-Versionen übereinstimmen +2. **Fehlende Abhängigkeiten**: sicherstellen, dass Test-Abhängigkeiten installiert sind +3. **Pfad-Probleme**: absolute Imports verwenden +4. **Timing-Probleme**: in asynchronen Tests passende Wartezeiten einbauen + +
+ +```console +# Test with same Python version as CI +$ python3.12 -m pytest + +# Check for missing dependencies +$ pip install -r requirements-dev.txt + +# Run tests in isolated environment +$ tox +``` + +
+ +## Hilfe finden + +### F. Wo bekomme ich Hilfe? + +**A.** Mehrere Möglichkeiten, Hilfe zu erhalten: + +- **GitHub Issues**: für Bugs und Feature-Requests +- **GitHub Discussions**: für Fragen und Community-Support +- **Dokumentation**: Nutzerhandbücher und Tutorials +- **Code-Beispiele**: vorhandene Vorlagen und Tests prüfen + +### F. Wie bleibe ich auf dem Laufenden? + +**A.** Verfolgen Sie die Projekt-Updates: + +- **Repository „Watchen"** auf GitHub +- **Releases prüfen** für neue Funktionen +- **Changelog lesen** für Breaking Changes +- **Best Practices** in der Dokumentation befolgen + +!!! tip "Profi-Tipps" + - Verwenden Sie immer virtuelle Umgebungen für Python-Projekte + - Halten Sie Ihre FastAPI-fastkit-Installation aktuell + - Verwenden Sie `fastkit --help`, um verfügbare Befehle zu sehen + - Schauen Sie in die Dokumentation, wenn Sie nicht weiterkommen + - Zögern Sie nicht, Fragen in GitHub Discussions zu stellen diff --git a/docs/de/reference/preset-feature-matrix.md b/docs/de/reference/preset-feature-matrix.md new file mode 100644 index 0000000..f1424bc --- /dev/null +++ b/docs/de/reference/preset-feature-matrix.md @@ -0,0 +1,60 @@ +# Architektur-Preset- / Funktionsmatrix + +Das interaktive `fastkit init --interactive` fragt nach einem **Architektur-Preset** ([Issue #44](https://github.com/bnbong/FastAPI-fastkit/issues/44)), bevor es die Funktionsauswahl erhebt. Das Preset prägt das Layout des generierten Projekts — jedes Preset bringt eine andere Basisvorlage mit und legt generierte Konfigurationsdateien an unterschiedlichen Orten ab, damit sie neben der vorhandenen Struktur sitzen und nicht in einer parallelen `src/config/`-Hierarchie. + +Diese Seite ist die maßgebliche Referenz dafür, was jedes Preset tut, wo Dateien landen und welche Funktionskombinationen manuell eingebunden werden müssen. + +## Preset → Basisvorlage + +| Preset | Basisvorlage | Beschreibung | +|---|---|---| +| `minimal` | `fastapi-empty` | Die kleinstmögliche lauffähige FastAPI-App — Platzhalter-`main.py` wird aus Ihren Funktionsauswahlen regeneriert. | +| `single-module` | `fastapi-single-module` | FastAPI-App in einer Datei — `main.py` wird regeneriert. | +| `classic-layered` | `fastapi-default` | Geschichtete Aufteilung (`api/routes`, `crud`, `schemas`, `core`). Die mitgelieferte `main.py` bleibt erhalten. | +| `domain-starter` | `fastapi-domain-starter` | Domänenorientiert (`src/app/domains//`). Die mitgelieferte `main.py` bleibt erhalten. **Empfohlene Standardlösung.** | + +## Speicherorte der generierten Dateien + +| Preset | `main.py`-Overlay | Zielpfad für DB-Konfig | Zielpfad für Auth-Konfig | +|---|---|---|---| +| `minimal` | regeneriert unter `src/main.py` | `src/config/database.py` | `src/config/auth.py` | +| `single-module` | regeneriert unter `src/main.py` | `src/config/database.py` | `src/config/auth.py` | +| `classic-layered` | erhalten (vorlagenseitig) | `src/core/database.py` | `src/core/auth.py` | +| `domain-starter` | erhalten (vorlagenseitig) | `src/app/core/database.py` | `src/app/core/auth.py` | + +## Unterstützung der Datenbank- / Auth-Funktionen je Preset + +Diese Funktionen werden in **jedem** Preset unterstützt — die Paketinstallation gelingt immer; der Unterschied ist, ob das dynamische `main.py`-Overlay sie auch automatisch verdrahtet. + +| Funktion | `minimal` / `single-module` | `classic-layered` / `domain-starter` | +|---|---|---| +| **Datenbank** (PostgreSQL, MySQL, SQLite, MongoDB) | Erzeugt das Konfigurationsmodul **und** legt `await init_db()`-Aufrufe in der regenerierten `main.py` an. | Erzeugt das Konfigurationsmodul am Pfad des Presets. Die mitgelieferte `main.py` bleibt **erhalten**, daher `get_db()` manuell in Router einbinden. | +| **Authentifizierung** (JWT, FastAPI-Users, OAuth2, sessionbasiert) | Erzeugt das Auth-Konfigurationsmodul. JWT importiert zusätzlich `HTTPBearer` in der regenerierten `main.py`. | Erzeugt das Auth-Konfigurationsmodul am Pfad des Presets. Keine Imports werden zu `main.py` hinzugefügt — Abhängigkeiten manuell verdrahten. | +| **Hintergrundaufgaben** (Celery, Dramatiq) | Pakete installiert; aktuell kein main.py-Overlay. | Wie nebenan. | +| **Caching** (Redis) | Pakete installiert; aktuell kein main.py-Overlay. | Wie nebenan. | +| **CORS** (Utility) | `CORSMiddleware` mit `allow_origins=['*']` wird in die regenerierte `main.py` eingefügt. | **Bereits verdrahtet** in der mitgelieferten `main.py` (bedingt durch `settings.all_cors_origins`). Aktivieren Sie es, indem Sie `BACKEND_CORS_ORIGINS` in `.env` setzen — keine Code-Änderungen nötig. | +| **Testen** (Basic / Coverage / Advanced) | `pytest.ini` wird im Projekt-Root erzeugt. | Wie nebenan. | +| **Deployment** (Docker, docker-compose) | `Dockerfile` und/oder `docker-compose.yml` werden im Projekt-Root abgelegt. | Wie nebenan. | + +## Wann eine Warnung zur Preset-Kompatibilität erscheint + +Bei Presets, die die **mitgelieferte `main.py` erhalten** (`classic-layered`, `domain-starter`), werden manche Funktionsauswahlen nicht automatisch in die App verdrahtet. Das CLI gibt am Ende der Generierung eine einmalige Warnung aus, die auflistet, welche Auswahlen manuelle Verdrahtung benötigen: + +| Ausgewählte Funktion | Löst Warnung unter `classic-layered` / `domain-starter` aus? | +|---|---| +| `CORS` (Utility) | ❌ — bereits in der mitgelieferten `main.py` verdrahtet. Setzen Sie nur `BACKEND_CORS_ORIGINS` in `.env`. | +| `Rate-Limiting` (Utility) | ✅ — Setup des `slowapi`-Limiters wird nicht ergänzt | +| `Prometheus` (Monitoring) | ✅ — `Instrumentator().instrument(app)` wird nicht aufgerufen | +| Jegliche Datenbank- / Auth-Auswahl | ⚠️ — Konfigurationsdateien werden erzeugt, aber Sie müssen sie per `Depends()` in Ihre Router einbinden | + +Für die Presets `minimal` und `single-module` übernimmt das dynamische `main.py`-Overlay CORS, Rate-Limiting und Prometheus-Instrumentierung automatisch; es werden keine Warnungen ausgelöst. + +## Nicht unterstützte Kombinationen (auf Nummer sicher gehen) + +Die Strategie versucht **bewusst nicht**, generierten Code in eine vorlagenseitig mitgelieferte `main.py` einzufügen. Das würde riskieren, kaputte Imports oder doppelte Router zu erzeugen. Der Vertrag lautet: + +- Ausgewählte Pakete werden immer installiert (damit `pip freeze` der Nutzerabsicht entspricht). +- Generierte Konfigurationsmodule landen immer am preset-passenden Pfad. +- Bei Presets, die `main.py` erhalten, wird dem Nutzer gesagt, welche Auswahlen noch manuelle Verdrahtung brauchen, statt stillschweigend kaputten Code zu liefern. + +Wenn Sie die vollautomatische Verdrahtung aller Funktionen benötigen, wählen Sie `minimal` oder `single-module` — diese regenerieren `main.py` aus den Funktions-Flags. diff --git a/docs/de/reference/template-quality-assurance.md b/docs/de/reference/template-quality-assurance.md new file mode 100644 index 0000000..83ce66f --- /dev/null +++ b/docs/de/reference/template-quality-assurance.md @@ -0,0 +1,218 @@ +# Qualitätssicherung der Vorlagen + +FastAPI-fastkit bietet eine umfassende automatisierte Validierung aller Vorlagen, um sicherzustellen, dass sie hohe Qualität halten und in verschiedenen Umgebungen und mit unterschiedlichen Paketmanagern funktionsfähig bleiben. + +## Mehrstufige Qualitätssicherung + +FastAPI-fastkit setzt **zwei sich ergänzende Qualitätssicherungssysteme** ein: + +### 1. Statische Vorlagen-Inspektion +**Wöchentliche automatisierte Validierung von Vorlagenstruktur und -syntax** + +### 2. Dynamische Vorlagen-Tests +**Umfassende End-to-End-Tests durch tatsächliches Anlegen von Projekten** + +## Automatisierte wöchentliche Inspektion + +Jeden Mittwoch um Mitternacht (UTC) inspiziert unser GitHub-Actions-Workflow automatisch alle FastAPI-Vorlagen, um sicherzustellen, dass sie die Qualitätsstandards erfüllen: + +- ✅ **Validierung der Dateistruktur** — stellt sicher, dass alle erforderlichen Dateien und Verzeichnisse vorhanden sind +- ✅ **Überprüfung der Dateiendungen** — validiert, dass Vorlagendateien die korrekten `.py-tpl`-Endungen verwenden +- ✅ **Abhängigkeitsprüfung** — bestätigt, dass FastAPI und benötigte Abhängigkeiten ordnungsgemäß deklariert sind +- ✅ **FastAPI-Implementierung** — prüft, dass Vorlagen eine korrekte FastAPI-App-Initialisierung enthalten +- ✅ **Testausführung** — führt die Tests der Vorlage aus, um die Funktionsfähigkeit zu bestätigen + +## Automatisiertes Vorlagen-Testsystem + +FastAPI-fastkit enthält ein **revolutionäres automatisiertes Testsystem**, das eine umfassende Validierung jeder Vorlage durchführt: + +### Dynamische Vorlagenerkennung + +Das Testsystem **erkennt alle Vorlagen automatisch**, ohne manuelle Konfiguration: + +```console +# Test all templates automatically +$ pytest tests/test_templates/test_all_templates.py -v + +# Results show all discovered templates +PASSED tests/test_templates/test_all_templates.py::TestAllTemplates::test_template_creation[fastapi-default] +PASSED tests/test_templates/test_all_templates.py::TestAllTemplates::test_template_creation[fastapi-async-crud] +PASSED tests/test_templates/test_all_templates.py::TestAllTemplates::test_template_creation[fastapi-dockerized] +PASSED tests/test_templates/test_all_templates.py::TestAllTemplates::test_template_creation[fastapi-psql-orm] +``` + +### Umfassende Testabdeckung + +Jede Vorlage durchläuft **umfassende End-to-End-Tests**: + +#### ✅ Projektanlegung +- Kopieren der Vorlage und Transformation der Dateien +- Injektion der Projekt-Metadaten (Name, Autor, Beschreibung) +- Validierung der Dateistruktur + +#### ✅ Kompatibilität mit Paketmanagern +- **UV** (Standard): schneller Rust-basierter Paketmanager +- **PDM**: moderne Python-Abhängigkeitsverwaltung +- **Poetry**: etablierte Abhängigkeitsverwaltung +- **PIP**: traditioneller Python-Paketmanager + +#### ✅ Verwaltung der virtuellen Umgebung +- Erstellen der Umgebung je Paketmanager +- Verifikation der Abhängigkeitsinstallation +- Workflows je Paketmanager + +#### ✅ Auflösung der Abhängigkeiten +- Erzeugung von `pyproject.toml` (UV, PDM, Poetry) +- Erzeugung von `requirements.txt` (PIP) +- Metadaten-Konformität (PEP 621) +- Konfiguration des Build-Systems + +#### ✅ Validierung der Projektstruktur +- Identifikation als FastAPI-Projekt +- Existenz der erforderlichen Dateien +- Verifikation der Verzeichnisstruktur + +### Beispiele für die Testausführung + +**Alle Vorlagen-Tests ausführen:** +```console +$ pytest tests/test_templates/test_all_templates.py -v +``` + +**Eine bestimmte Vorlage testen:** +```console +$ pytest tests/test_templates/test_all_templates.py::TestAllTemplates::test_template_creation[fastapi-default] -v +``` + +**Mit PDM-Umgebung testen:** +```console +$ pdm run pytest tests/test_templates/test_all_templates.py -v +``` + +### Continuous Integration + +Das automatisierte Testsystem läuft in **CI/CD-Pipelines**: + +- ✅ **PR-Validierung**: jede PR testet die betroffenen Vorlagen +- ✅ **Nightly-Tests**: vollständige Validierung der Vorlagen-Suite +- ✅ **Paketmanager-Tests**: Kreuzvalidierung mit allen Managern +- ✅ **Umgebungstests**: mehrere Python-Versionen und Plattformen + +### Vorteile für Mitwirkende + +**Tests ohne Konfiguration:** + +- 🚀 neue Vorlage hinzufügen → automatische Tests +- ⚡ keine manuelle Erstellung von Testdateien nötig +- 🛡️ konsistente Qualitätsstandards + +**Umfassende Abdeckung:** + +- 🔍 End-to-End-Tests der Projektanlegung +- 📦 Validierung mit mehreren Paketmanagern +- 🏗️ vollständige Tests der Abhängigkeitsauflösung +- ✅ Simulation realer Nutzung + +**Entwicklererfahrung:** + +- 🎯 **Konzentration auf den Vorlageninhalt**: Tests laufen automatisch +- 🔄 **Sofortiges Feedback**: schnelle Testausführung +- 📊 **Klare Ergebnisse**: detaillierte Testberichte +- 🚫 **Kein Boilerplate**: null Testkonfiguration nötig + +## Manuelle Vorlagen-Inspektion + +Für Entwicklung und Debugging können Sie Vorlagen manuell mit unserem lokalen Inspektionsskript oder den Makefile-Befehlen prüfen: + +### Das Inspektionsskript direkt verwenden + +```console +# Inspect all templates +$ python scripts/inspect-templates.py + +# Inspect specific templates +$ python scripts/inspect-templates.py --templates fastapi-default,fastapi-async-crud + +# Verbose output with detailed information +$ python scripts/inspect-templates.py --verbose + +# Save results to custom file +$ python scripts/inspect-templates.py --output my_results.json +``` + +### Makefile-Befehle verwenden + +```console +# Inspect all templates +$ make inspect-templates + +# Inspect with verbose output +$ make inspect-templates-verbose + +# Inspect specific templates +$ make inspect-template TEMPLATES="fastapi-default,fastapi-async-crud" +``` + +## Ergebnisse der Inspektion + +- **Erfolgreiche Inspektionen** werden in Workflow-Ausgaben und -Artefakten protokolliert +- **Fehlgeschlagene Inspektionen** erstellen automatisch GitHub-Issues mit detaillierten Fehlerberichten +- Die **Inspektionshistorie** wird 30 Tage lang in GitHub-Actions-Artefakten aufbewahrt + +## Die Inspektionsausgabe verstehen + +Beim Ausführen der Vorlagen-Inspektion sehen Sie eine Ausgabe wie diese: + +```console +📋 Found 6 templates to inspect: fastapi-async-crud, fastapi-custom-response, fastapi-default, fastapi-dockerized, fastapi-empty, fastapi-psql-orm +============================================================ +🔍 Inspecting template: fastapi-async-crud + Path: /path/to/src/fastapi_fastkit/fastapi_project_template/fastapi-async-crud +✅ fastapi-async-crud: PASSED +---------------------------------------- +🔍 Inspecting template: fastapi-custom-response + Path: /path/to/src/fastapi_fastkit/fastapi_project_template/fastapi-custom-response +✅ fastapi-custom-response: PASSED +---------------------------------------- +... +============================================================ +📊 INSPECTION SUMMARY + Total templates: 6 + ✅ Passed: 6 + ❌ Failed: 0 +🎉 All templates passed inspection! +📄 Results saved to: template_inspection_results.json +``` + +## Anforderungen an Vorlagen + +Damit eine Vorlage die Inspektion besteht, muss sie diese Anforderungen erfüllen: + +### Dateistruktur +- Muss ein `src/`-Verzeichnis mit Python-Quellcodedateien enthalten +- Python-Dateien müssen die Endung `.py-tpl` verwenden +- Muss ein `tests/`-Verzeichnis und eine `README.md-tpl`-Datei enthalten +- Muss **mindestens eine** Metadaten-Datei enthalten: + - `pyproject.toml-tpl` (bevorzugt, PEP 621), oder + - `setup.py-tpl` (legacy, weiterhin akzeptiert) +- `requirements.txt-tpl` ist optional, wenn `pyproject.toml-tpl` `[project].dependencies` deklariert + +### FastAPI-Anforderungen +- Muss die FastAPI-App-Initialisierung enthalten +- Muss `fastapi` als Abhängigkeit in mindestens einem von `pyproject.toml-tpl` `[project].dependencies`, `requirements.txt-tpl` oder `setup.py-tpl` `install_requires` deklarieren +- Muss in allen Vorlagendateien gültige Python-Syntax aufweisen + +### Identitätsmarker +Vorlagen sollten FastAPI-fastkit-Identitätsmarker tragen, damit generierte Projekte von unverwandten FastAPI-Projekten im Workspace des Nutzers unterscheidbar sind: + +- `pyproject.toml-tpl` — sowohl ein `[FastAPI-fastkit templated]`-Präfix in `description` als auch eine `[tool.fastapi-fastkit]`-Tabelle mit `managed = true`. +- `setup.py-tpl` — `[FastAPI-fastkit templated]`-Präfix im `description`-Argument von `setup()`. + +`is_fastkit_project()` akzeptiert jeden dieser Marker (pyproject hat Vorrang, setup.py ist der Legacy-Fallback; die Übereinstimmungsprüfung ist case-insensitive). Die Metadaten-Injektion sorgt dafür, dass die Marker in generierten Projekten landen, auch wenn eine Vorlage sie vergisst. + +### Qualitätsstandards +- Alle Vorlagendateien müssen syntaktisch korrekt sein +- Abhängigkeiten müssen korrekt spezifiziert sein +- Die Vorlagenstruktur muss den FastAPI-fastkit-Konventionen folgen + +Diese automatisierte Qualitätssicherung garantiert, dass alle Vorlagen zuverlässig und produktionsreif bleiben. diff --git a/docs/de/reference/translation-status.md b/docs/de/reference/translation-status.md new file mode 100644 index 0000000..f63f9ee --- /dev/null +++ b/docs/de/reference/translation-status.md @@ -0,0 +1,82 @@ +# Übersetzungsstatus + +FastAPI-fastkit stellt die Dokumentation in mehreren Sprachen bereit, diese Übersetzungen sind aber **nicht vollständig gleichauf**. Diese Seite ist die maßgebliche Übersicht dafür, was tatsächlich wo übersetzt ist, was angezeigt wird, wenn eine Seite noch nicht übersetzt wurde, und wie Sie helfen können. + +## Maßgebliche Referenz + +> **Englisch (`en`) ist die maßgebliche Referenz.** Alle in der Dokumentation beschriebenen Verhaltensweisen von Produkt, CLI und API werden zuerst in den englischen Dateien festgehalten. Andere Sprachen sind Übersetzungen dieser englischen Quellen und können einem Release hinterherhinken. +> +> Wenn eine übersetzte Seite der englischen Seite widerspricht, **vertrauen Sie der englischen Seite**, bis die Übersetzung nachgezogen wird. + +Die englischen Dateien liegen unter [`docs/en/`](https://github.com/bnbong/FastAPI-fastkit/tree/main/docs/en). Jede andere Sprache (`docs/ko/`, `docs/ja/`, …) ist ein Übersetzungsziel. + +Die `CHANGELOG.md` im Stammverzeichnis des Repositorys gehört ebenfalls zu dieser englischen Referenz. Sprachspezifische `changelog.md`-Seiten können als Einstiegspunkt dienen, verwenden aber bewusst die kanonische englische Releasehistorie statt eigener übersetzter Kopien. + +## Vollständigkeit pro Sprache + +Die unten stehenden Zahlen zählen die Markdown-Seiten im Verzeichnisbaum jeder Sprache relativ zur englischen Quelle. Sie spiegeln wider, was tatsächlich im Repository vorhanden ist — nicht das, was im Sprachumschalter angezeigt wird (siehe nächster Abschnitt). + +| Sprache | Status | Markdown-Seiten | Anmerkungen | +|---|---|---:|---| +| 🇬🇧 Englisch (`en`) | ✅ Maßgebliche Referenz | 26 / 26 | Maßgeblich. | +| 🇰🇷 Koreanisch (`ko`) | ✅ Vollständig | 26 / 26 | Alle Seiten dieser Sprachversion sind vorhanden. Phase 1: oberste Ebene + Kern des Benutzerhandbuchs; Phase 2: restliches Benutzerhandbuch + alle Tutorials; Phase 3: Beiträge + Referenzseiten. `docs/ko/changelog.md` verweist bewusst auf das kanonische englische `CHANGELOG.md`. | +| 🇯🇵 Japanisch (`ja`) | ✅ Vollständig | 26 / 26 | Alle Seiten dieser Sprachversion sind vorhanden. Phase 1: oberste Ebene + Kern des Benutzerhandbuchs; Phase 2: restliches Benutzerhandbuch + alle Tutorials; Phase 3: Beiträge + Referenzseiten. `docs/ja/changelog.md` verweist bewusst auf das kanonische englische `CHANGELOG.md`. | +| 🇨🇳 Chinesisch (`zh`) | 🔴 Skelett | 0 / 26 | Nur als Sprach-Build-Ziel vorhanden. Jede Seite fällt auf Englisch zurück. | +| 🇪🇸 Spanisch (`es`) | ✅ Vollständig | 26 / 26 | Alle Seiten dieser Sprachversion sind vorhanden. Phase 1: oberste Ebene + Kern des Benutzerhandbuchs; Phase 2: restliches Benutzerhandbuch + alle Tutorials; Phase 3: Beiträge + Referenzseiten. `docs/es/changelog.md` verweist bewusst auf das kanonische englische `CHANGELOG.md`. | +| 🇫🇷 Französisch (`fr`) | ✅ Vollständig | 26 / 26 | Alle Seiten dieser Sprachversion sind vorhanden. Phase 1: oberste Ebene + Kern des Benutzerhandbuchs; Phase 2: restliches Benutzerhandbuch + alle Tutorials; Phase 3: Beiträge + Referenzseiten. `docs/fr/changelog.md` verweist bewusst auf das kanonische englische `CHANGELOG.md`. | +| 🇩🇪 Deutsch (`de`) | ✅ Vollständig | 26 / 26 | Alle Seiten dieser Sprachversion sind vorhanden. Phase 1: oberste Ebene + Kern des Benutzerhandbuchs; Phase 2: restliches Benutzerhandbuch + alle Tutorials; Phase 3: Beiträge + Referenzseiten. `docs/de/changelog.md` verweist bewusst auf das kanonische englische `CHANGELOG.md`. | + +*Stand verifiziert am 2026-05-17; die Zeile `de` wurde für den aktuellen Branch nach dem Abschluss von Phase 3 (Beiträge + Referenzseiten) neu gezählt. Deutsch umfasst nun alle Seiten dieser Sprachversion, und `docs/de/changelog.md` verweist auf den kanonischen englischen Changelog.* Diese Zahlen werden manuell gepflegt; um den aktuellen Stand vom Repo-Root neu zu zählen, führen Sie aus: + +```console +$ for loc in en ko ja zh es fr de; do + echo "$loc: $(find docs/$loc -name '*.md' 2>/dev/null | wc -l | tr -d ' ')" + done +``` + +Wenn die Neuzählung nicht mit der Tabelle übereinstimmt, ist die Tabelle veraltet — bitte aktualisieren Sie sie (oder öffnen Sie eine PR / ein Issue, das die Abweichung meldet). + +Legende: + +- ✅ **Maßgebliche Referenz** — die Sprache, in der zuerst geschrieben wird. +- 🟡 **Teilweise** — einige Seiten sind übersetzt; fehlende Seiten fallen auf Englisch zurück. +- 🔴 **Skelett** — der Eintrag im Sprachumschalter existiert, es sind aber noch keine übersetzten Seiten eingecheckt. Die Seite rendert englische Inhalte unter den übersetzten Navigationsbeschriftungen. + +## Wie der Fallback funktioniert + +Die Dokumentations-Site verwendet [`mkdocs-static-i18n`](https://github.com/ultrabug/mkdocs-static-i18n) mit `fallback_to_default: true`. Das bedeutet: + +- Für jede übersetzte Sprache schreibt MkDocs nur die Seiten, die im Verzeichnis dieser Sprache existieren. +- Für jede Seite, die **nicht** in einer Sprache vorhanden ist, fällt der Build auf die englische Version dieser Seite zurück. +- Der seitenweite Sprachumschalter listet immer alle konfigurierten Sprachen auf, unabhängig davon, wie viele Seiten jede enthält, weil der Build für jeden Fall eine erreichbare URL erzeugt (Seite → ggf. Fallback auf Englisch). + +Ein 🔴 Skelett-Eintrag im Sprachumschalter ist also **kein** Versprechen, dass die Dokumentation übersetzt ist — er bedeutet nur, dass das Build-Ziel für diese Sprachversion konfiguriert ist. Das Verhalten ist beabsichtigt (externe Mitwirkende können Seite für Seite übersetzen, ohne die Linkstruktur zu zerstören), lässt den Sprachumschalter aber vollständiger wirken, als der zugrunde liegende Inhalt tatsächlich ist. + +## So lesen Sie die Dokumentationsseite + +- **Standardmäßig Englisch verwenden**, wenn Sie die genaueste, aktuellste Information möchten. +- **Eine übersetzte Sprache** nur verwenden, nachdem Sie auf dieser Seite den Status der Sprache geprüft haben. Ist er 🟡 oder 🔴 und stoßen Sie auf ein noch nicht übersetztes Thema, lesen Sie tatsächlich einen englischen Fallback unter einer übersetzten Navigationsbeschriftung. + +## Wie Sie helfen können + +Der aktuelle Rollout läuft nach dem Prinzip **ein Tracking-Issue pro Sprache**, mit der Arbeit aufgeteilt in **Phasen** — `ko` etwa wird in Phase 1 (oberste Ebene + Kern des Benutzerhandbuchs), Phase 2 (restliches Benutzerhandbuch + alle Tutorials) und Phase 3 (Beiträge + Referenzseiten) ausgeliefert. Jede Phase wird als eigene PR geliefert, sodass Reviewer einen zusammenhängenden Ausschnitt absegnen können, ohne auf die Fertigstellung der gesamten Sprachversion zu warten. + +Wenn Sie beitragen möchten: + +1. Lesen Sie den [Übersetzungsleitfaden](../contributing/translation-guide.md) für Workflow, Werkzeuge und Stilkonventionen. +2. **Prüfen oder eröffnen Sie zuerst das Tracking-Issue der Sprachversion.** Hat eine Sprachversion bereits ein offenes Tracking-Issue, reservieren Sie dort eine Phase (oder eine bestimmte Seite innerhalb einer Phase), damit die Arbeit nicht doppelt erfolgt. Existiert noch kein Tracking-Issue für die gewünschte Sprachversion, öffnen Sie eines, das die zugehörigen Seiten je Phase auflistet, und starten Sie mit Phase 1. +3. **Eine PR pro Phase ist die bevorzugte Form.** Kleinere PRs zum Korrigieren einzelner Seiten sind weiterhin willkommen — besonders, wenn eine Übersetzung veraltet ist — aber bei neuer Lokalisierungsarbeit hält das Bündeln pro Phase Glossar-Entscheidungen und Formulierungen für Querverweise über den gesamten Ausschnitt hinweg konsistent. +4. Öffnen Sie die PR und legen Sie Dateien unter `docs//` ab. Halten Sie die Dateinamen identisch zur englischen Quelle, damit MkDocs sie automatisch erfasst. +5. Behandeln Sie lokalisierte Changelog-Seiten als Einstiegsseite oder Verweis auf das kanonische englische `CHANGELOG.md`, sofern die Projektrichtlinie das nicht ausdrücklich ändert. +6. Aktualisieren Sie die Tabelle auf dieser Seite, um die neue Vollständigkeit widerzuspiegeln (verwenden Sie das Neuzählungs-Snippet am Anfang der Seite), und aktualisieren Sie das „Stand verifiziert"-Datum, damit Reviewer sehen, wann zuletzt abgeglichen wurde. Geben Sie in der Spalte „Anmerkungen" an, welche Phase abgeschlossen ist, falls die Sprache noch teilweise ist. + +Bug-Reports zu übersetzten Seiten, die mit dem englischen Original auseinanderlaufen, sind willkommen — bitte verlinken Sie die englische und die übersetzte Seite, damit wir triagieren können. + +## Warum wir 🔴 Skelett-Sprachen überhaupt ausliefern + +Zwei Gründe: + +1. **Vorhersehbare URL-Struktur.** Jede Sprache hat bereits ihren `//`-Unterbaum erreichbar, sodass der Link einer übersetzten Seite ab dem ersten Tag stabil ist — einschließlich der in diesem Leitfaden veröffentlichten Links. +2. **Geringere Hürde für Mitwirkende.** Wer eine einzelne Seite übersetzt, muss nicht zusätzlich ein neues Sprach-Build-Ziel in der MkDocs-Konfiguration einrichten — es reicht, die Datei an der richtigen Stelle abzulegen. + +Bleibt eine Sprache über einen längeren Zeitraum auf 🔴 Skelett ohne Beitragsaktivität, prüfen wir gegebenenfalls, ob das Build-Ziel weiter aktiv bleibt. Diese Entscheidung wird separat verfolgt und **wird** durch diese Statusseite nicht stillschweigend geändert. diff --git a/docs/de/tutorial/async-crud-api.md b/docs/de/tutorial/async-crud-api.md new file mode 100644 index 0000000..d0ae533 --- /dev/null +++ b/docs/de/tutorial/async-crud-api.md @@ -0,0 +1,665 @@ +# Asynchrone CRUD-APIs bauen + +Lernen Sie, wie Sie hochperformante CRUD-APIs mit den asynchronen Verarbeitungsmöglichkeiten von FastAPI bauen. In diesem Tutorial implementieren wir asynchrone Datei-I/O und effiziente Datenverarbeitung mit der Vorlage `fastapi-async-crud`. + +## Was Sie in diesem Tutorial lernen + +- Asynchrone FastAPI-Anwendungen verstehen +- Asynchrone CRUD-Operationen mit der `async/await`-Syntax +- Asynchrone Dateiverarbeitung mit aiofiles +- Asynchrone Tests schreiben und ausführen +- Techniken zur Leistungsoptimierung + +## Voraussetzungen + +- Das [Tutorial Einfacher API-Server](basic-api-server.md) abgeschlossen +- Verständnis der Grundkonzepte von Pythons `async/await` +- FastAPI-fastkit installiert + +## Warum asynchrone Verarbeitung nötig ist + +Verstehen wir den Unterschied zwischen synchroner und asynchroner Verarbeitung: + +### Synchrone Verarbeitung + +```python +def process_items(): + item1 = read_file("item1.json") # Wait 2 seconds + item2 = read_file("item2.json") # Wait 2 seconds + item3 = read_file("item3.json") # Wait 2 seconds + return [item1, item2, item3] # Total: 6 seconds +``` + +### Asynchrone Verarbeitung + +```python +async def process_items(): + item1_task = read_file_async("item1.json") # Start concurrently + item2_task = read_file_async("item2.json") # Start concurrently + item3_task = read_file_async("item3.json") # Start concurrently + + items = await asyncio.gather(item1_task, item2_task, item3_task) + return items # Total: 2 seconds +``` + +## Schritt 1: Ein asynchrones CRUD-Projekt erstellen + +Erstellen Sie ein Projekt mit der Vorlage `fastapi-async-crud`: + +
+ +```console +$ fastkit startdemo fastapi-async-crud +Enter the project name: async-todo-api +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: Asynchronous todo management API +Deploying FastAPI project using 'fastapi-async-crud' template + + Project Information +┌──────────────┬─────────────────────────────────────────┐ +│ Project Name │ async-todo-api │ +│ Author │ Developer Kim │ +│ Author Email │ developer@example.com │ +│ Description │ Asynchronous todo management API │ +└──────────────┴─────────────────────────────────────────┘ + + Template Dependencies +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ pydantic │ +│ Dependency 4 │ pydantic-settings │ +│ Dependency 5 │ aiofiles │ +│ Dependency 6 │ pytest-asyncio │ +└──────────────┴───────────────────┘ + +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y + +✨ FastAPI project 'async-todo-api' from 'fastapi-async-crud' has been created successfully! +``` + +
+ +## Schritt 2: Projektstruktur analysieren + +Sehen wir uns die wichtigsten Unterschiede des generierten Projekts an: + +``` +async-todo-api/ +├── src/ +│ ├── main.py # Asynchronous FastAPI application +│ ├── api/ +│ │ └── routes/ +│ │ └── items.py # Asynchronous CRUD endpoints +│ ├── crud/ +│ │ └── items.py # Asynchronous data processing logic +│ ├── schemas/ +│ │ └── items.py # Data models (same) +│ ├── mocks/ +│ │ └── mock_items.json # JSON file database +│ └── core/ +│ └── config.py # Configuration file +└── tests/ + ├── conftest.py # Asynchronous test configuration + └── test_items.py # Asynchronous test cases +``` + +### Wichtige Unterschiede + +1. **aiofiles**: asynchrone Datei-I/O-Verarbeitung +2. **pytest-asyncio**: Unterstützung für asynchrone Tests +3. **async/await-Muster**: alle CRUD-Operationen asynchron implementiert + +## Schritt 3: Die asynchrone CRUD-Logik verstehen + +### Asynchrone Datenverarbeitung (`src/crud/items.py`) + +```python +import json +import asyncio +from typing import List, Optional +from aiofiles import open as aio_open +from pathlib import Path + +from src.schemas.items import Item, ItemCreate, ItemUpdate + +class AsyncItemCRUD: + def __init__(self, data_file: str = "src/mocks/mock_items.json"): + self.data_file = Path(data_file) + + async def _read_data(self) -> List[dict]: + """Asynchronously read data from JSON file""" + try: + async with aio_open(self.data_file, 'r', encoding='utf-8') as f: + content = await f.read() + return json.loads(content) + except FileNotFoundError: + return [] + + async def _write_data(self, data: List[dict]) -> None: + """Asynchronously write data to JSON file""" + async with aio_open(self.data_file, 'w', encoding='utf-8') as f: + await f.write(json.dumps(data, indent=2, ensure_ascii=False)) + + async def get_items(self) -> List[Item]: + """Retrieve all items (asynchronous)""" + data = await self._read_data() + return [Item(**item) for item in data] + + async def get_item(self, item_id: int) -> Optional[Item]: + """Retrieve specific item (asynchronous)""" + data = await self._read_data() + item_data = next((item for item in data if item["id"] == item_id), None) + return Item(**item_data) if item_data else None + + async def create_item(self, item: ItemCreate) -> Item: + """Create new item (asynchronous)""" + data = await self._read_data() + new_id = max([item["id"] for item in data], default=0) + 1 + + new_item = Item(id=new_id, **item.dict()) + data.append(new_item.dict()) + + await self._write_data(data) + return new_item + + async def update_item(self, item_id: int, item_update: ItemUpdate) -> Optional[Item]: + """Update item (asynchronous)""" + data = await self._read_data() + + for i, item in enumerate(data): + if item["id"] == item_id: + update_data = item_update.dict(exclude_unset=True) + data[i].update(update_data) + await self._write_data(data) + return Item(**data[i]) + + return None + + async def delete_item(self, item_id: int) -> bool: + """Delete item (asynchronous)""" + data = await self._read_data() + original_length = len(data) + + data = [item for item in data if item["id"] != item_id] + + if len(data) < original_length: + await self._write_data(data) + return True + + return False +``` + +### Asynchrone API-Endpunkte (`src/api/routes/items.py`) + +```python +from typing import List +from fastapi import APIRouter, HTTPException, status + +from src.schemas.items import Item, ItemCreate, ItemUpdate +from src.crud.items import AsyncItemCRUD + +router = APIRouter() +crud = AsyncItemCRUD() + +@router.get("/", response_model=List[Item]) +async def read_items(): + """Retrieve all items (asynchronous)""" + return await crud.get_items() + +@router.get("/{item_id}", response_model=Item) +async def read_item(item_id: int): + """Retrieve specific item (asynchronous)""" + item = await crud.get_item(item_id) + if item is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Item with id {item_id} not found" + ) + return item + +@router.post("/", response_model=Item, status_code=status.HTTP_201_CREATED) +async def create_item(item: ItemCreate): + """Create new item (asynchronous)""" + return await crud.create_item(item) + +@router.put("/{item_id}", response_model=Item) +async def update_item(item_id: int, item_update: ItemUpdate): + """Update item (asynchronous)""" + updated_item = await crud.update_item(item_id, item_update) + if updated_item is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Item with id {item_id} not found" + ) + return updated_item + +@router.delete("/{item_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_item(item_id: int): + """Delete item (asynchronous)""" + deleted = await crud.delete_item(item_id) + if not deleted: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Item with id {item_id} not found" + ) +``` + +## Schritt 4: Server starten und testen + +Wechseln Sie ins Projektverzeichnis und starten Sie den Server: + +
+ +```console +$ cd async-todo-api +$ fastkit runserver +Starting FastAPI server at 127.0.0.1:8000... + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [12345] using WatchFiles +INFO: Started server process [12346] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +### Leistungstests + +Prüfen wir die Leistung der asynchronen Verarbeitung. Versuchen Sie, mehrere Anfragen gleichzeitig zu senden: + +**Test paralleler Anfragen (Python-Skript)** + +```python +import asyncio +import aiohttp +import time + +async def create_item(session, item_data): + async with session.post("http://127.0.0.1:8000/items/", json=item_data) as response: + return await response.json() + +async def test_concurrent_requests(): + start_time = time.time() + + items_to_create = [ + {"name": f"Item {i}", "description": f"Description {i}", "price": i * 10, "tax": i} + for i in range(1, 11) # Create 10 items concurrently + ] + + async with aiohttp.ClientSession() as session: + tasks = [create_item(session, item) for item in items_to_create] + results = await asyncio.gather(*tasks) + + end_time = time.time() + print(f"Created 10 items in: {end_time - start_time:.2f} seconds") + print(f"Number of items created: {len(results)}") + +# Run test +# asyncio.run(test_concurrent_requests()) +``` + +## Schritt 5: Asynchrone Tests schreiben + +### Test-Konfiguration (`tests/conftest.py`) + +```python +import pytest +import asyncio +from httpx import AsyncClient +from src.main import app + +@pytest.fixture(scope="session") +def event_loop(): + """Event loop configuration""" + loop = asyncio.get_event_loop_policy().new_event_loop() + yield loop + loop.close() + +@pytest.fixture +async def async_client(): + """Asynchronous test client""" + async with AsyncClient(app=app, base_url="http://test") as client: + yield client +``` + +### Asynchrone Testfälle (`tests/test_items.py`) + +```python +import pytest +from httpx import AsyncClient + +@pytest.mark.asyncio +async def test_create_item_async(async_client: AsyncClient): + """Asynchronous item creation test""" + item_data = { + "name": "Test Item", + "description": "Item for asynchronous testing", + "price": 100.0, + "tax": 10.0 + } + + response = await async_client.post("/items/", json=item_data) + + assert response.status_code == 201 + data = response.json() + assert data["name"] == item_data["name"] + assert data["price"] == item_data["price"] + assert "id" in data + +@pytest.mark.asyncio +async def test_read_items_async(async_client: AsyncClient): + """Asynchronous item list retrieval test""" + response = await async_client.get("/items/") + + assert response.status_code == 200 + items = response.json() + assert isinstance(items, list) + +@pytest.mark.asyncio +async def test_concurrent_operations(async_client: AsyncClient): + """Concurrent operations test""" + import asyncio + + # Create multiple items concurrently + tasks = [] + for i in range(5): + item_data = { + "name": f"ConcurrentItem{i}", + "description": f"Description{i}", + "price": i * 10, + "tax": i + } + task = async_client.post("/items/", json=item_data) + tasks.append(task) + + responses = await asyncio.gather(*tasks) + + # Verify all requests succeeded + for response in responses: + assert response.status_code == 201 + + # Verify created items + response = await async_client.get("/items/") + items = response.json() + assert len(items) >= 5 +``` + +### Tests ausführen + +
+ +```console +$ pytest tests/ -v --asyncio-mode=auto +======================== test session starts ======================== +collected 8 items + +tests/test_items.py::test_create_item_async PASSED [ 12%] +tests/test_items.py::test_read_items_async PASSED [ 25%] +tests/test_items.py::test_read_item_async PASSED [ 37%] +tests/test_items.py::test_update_item_async PASSED [ 50%] +tests/test_items.py::test_delete_item_async PASSED [ 62%] +tests/test_items.py::test_concurrent_operations PASSED [ 75%] +tests/test_items.py::test_item_not_found_async PASSED [ 87%] +tests/test_items.py::test_invalid_item_data_async PASSED [100%] + +======================== 8 passed in 0.24s ======================== +``` + +
+ +## Schritt 6: Performance-Monitoring und -Optimierung + +### Middleware zur Messung der Antwortzeit hinzufügen + +Fügen wir Performance-Monitoring in `src/main.py` hinzu: + +```python +import time +from fastapi import FastAPI, Request +from src.api.api import api_router +from src.core.config import settings + +app = FastAPI( + title=settings.PROJECT_NAME, + version=settings.VERSION, + description=settings.DESCRIPTION, +) + +@app.middleware("http") +async def add_process_time_header(request: Request, call_next): + """Add request processing time to headers""" + start_time = time.time() + response = await call_next(request) + process_time = time.time() - start_time + response.headers["X-Process-Time"] = str(process_time) + return response + +app.include_router(api_router) + +@app.get("/") +async def read_root(): + return {"message": "Welcome to the Asynchronous Todo API!"} +``` + +### Asynchrone Batch-Verarbeitung implementieren + +Fügen wir Batch-Endpunkte hinzu, um mehrere Items gleichzeitig zu verarbeiten: + +```python +# Add to src/api/routes/items.py + +@router.post("/batch", response_model=List[Item]) +async def create_items_batch(items: List[ItemCreate]): + """Create multiple items concurrently (batch processing)""" + import asyncio + + # Execute all item creation tasks concurrently + tasks = [crud.create_item(item) for item in items] + created_items = await asyncio.gather(*tasks) + + return created_items + +@router.get("/batch/{item_ids}") +async def read_items_batch(item_ids: str): + """Retrieve multiple items concurrently (batch processing)""" + import asyncio + + # Parse comma-separated IDs + ids = [int(id.strip()) for id in item_ids.split(",")] + + # Execute all item retrieval tasks concurrently + tasks = [crud.get_item(item_id) for item_id in ids] + items = await asyncio.gather(*tasks) + + # Return only non-None items + return [item for item in items if item is not None] +``` + +### Batch-Verarbeitung testen + +
+ +```console +# Batch creation test +$ curl -X POST "http://127.0.0.1:8000/items/batch" \ + -H "Content-Type: application/json" \ + -d '[ + {"name": "Item1", "description": "Description1", "price": 10.0, "tax": 1.0}, + {"name": "Item2", "description": "Description2", "price": 20.0, "tax": 2.0}, + {"name": "Item3", "description": "Description3", "price": 30.0, "tax": 3.0} + ]' + +# Batch retrieval test +$ curl -X GET "http://127.0.0.1:8000/items/batch/1,2,3" +``` + +
+ +## Schritt 7: Fortgeschrittene asynchrone Muster + +### Rate-Limiting implementieren + +```python +import asyncio +from collections import defaultdict +from fastapi import HTTPException, Request +from datetime import datetime, timedelta + +class AsyncRateLimiter: + def __init__(self, max_requests: int = 100, window_seconds: int = 60): + self.max_requests = max_requests + self.window_seconds = window_seconds + self.requests = defaultdict(list) + + async def is_allowed(self, client_ip: str) -> bool: + now = datetime.now() + cutoff = now - timedelta(seconds=self.window_seconds) + + # remove old request records + self.requests[client_ip] = [ + req_time for req_time in self.requests[client_ip] + if req_time > cutoff + ] + + # check current request count + if len(self.requests[client_ip]) >= self.max_requests: + return False + + # add current request record + self.requests[client_ip].append(now) + return True + +# global rate limiter instance +rate_limiter = AsyncRateLimiter() + +@app.middleware("http") +async def rate_limit_middleware(request: Request, call_next): + client_ip = request.client.host + + if not await rate_limiter.is_allowed(client_ip): + raise HTTPException( + status_code=429, + detail="Too many requests" + ) + + response = await call_next(request) + return response +``` + +### Asynchrones Caching implementieren + +```python +import asyncio +from typing import Optional, Any +from datetime import datetime, timedelta + +class AsyncCache: + def __init__(self): + self._cache = {} + self._expiry = {} + + async def get(self, key: str) -> Optional[Any]: + # remove expired items + if key in self._expiry and datetime.now() > self._expiry[key]: + del self._cache[key] + del self._expiry[key] + return None + + return self._cache.get(key) + + async def set(self, key: str, value: Any, ttl_seconds: int = 300): + self._cache[key] = value + self._expiry[key] = datetime.now() + timedelta(seconds=ttl_seconds) + + async def delete(self, key: str): + self._cache.pop(key, None) + self._expiry.pop(key, None) + +# global cache instance +cache = AsyncCache() + +# modify CRUD methods to use cache +async def get_items_cached(self) -> List[Item]: + """Retrieve items using cache""" + cache_key = "all_items" + cached_items = await cache.get(cache_key) + + if cached_items: + return cached_items + + # if cache is not found, read from file + items = await self.get_items() + await cache.set(cache_key, items, ttl_seconds=60) # 1 minute cache + + return items +``` + +## Schritt 8: Produktionsüberlegungen + +### Verbindungspools verwalten + +```python +# add to src/core/config.py +class Settings(BaseSettings): + # ... existing settings ... + + # asynchronous processing related settings + MAX_CONCURRENT_REQUESTS: int = 100 + REQUEST_TIMEOUT: int = 30 + CONNECTION_POOL_SIZE: int = 20 + +settings = Settings() +``` + +### Fehlerbehandlung verbessern + +```python +import logging +from fastapi import HTTPException +from typing import Union + +logger = logging.getLogger(__name__) + +async def safe_async_operation(operation, *args, **kwargs) -> Union[Any, None]: + """Execute safe asynchronous operation""" + try: + return await operation(*args, **kwargs) + except asyncio.TimeoutError: + logger.error(f"Timeout in {operation.__name__}") + raise HTTPException(status_code=504, detail="Request timeout") + except Exception as e: + logger.error(f"Error in {operation.__name__}: {str(e)}") + raise HTTPException(status_code=500, detail="Internal server error") + +# usage example +@router.get("/safe/{item_id}") +async def read_item_safe(item_id: int): + return await safe_async_operation(crud.get_item, item_id) +``` + +## Nächste Schritte + +Sie haben den Bau einer asynchronen CRUD-API abgeschlossen! Nächste Schritte zum Ausprobieren: + +1. **[Datenbankintegration](database-integration.md)** — PostgreSQL mit asynchronem SQLAlchemy nutzen +2. **[Docker-Containerisierung](docker-deployment.md)** — asynchrone Anwendungen containerisieren +3. **[Benutzerdefinierte Antwortbehandlung](custom-response-handling.md)** — fortgeschrittene Antwortformate und Fehlerbehandlung + + + +## Zusammenfassung + +In diesem Tutorial haben wir mit asynchronem FastAPI: + +- ✅ Asynchrone CRUD-Operationen implementiert +- ✅ Datei-I/O mit aiofiles optimiert +- ✅ Parallele Anfragen behandelt und Leistung getestet +- ✅ Asynchrone Tests geschrieben und ausgeführt +- ✅ Batch-Verarbeitung und fortgeschrittene asynchrone Muster implementiert +- ✅ Produktionsüberlegungen behandelt (Caching, Fehlerbehandlung, Verbindungsverwaltung) + +Asynchrone Verarbeitung zu beherrschen ermöglicht es Ihnen, hochperformante API-Server zu bauen! diff --git a/docs/de/tutorial/basic-api-server.md b/docs/de/tutorial/basic-api-server.md new file mode 100644 index 0000000..0271d91 --- /dev/null +++ b/docs/de/tutorial/basic-api-server.md @@ -0,0 +1,398 @@ +# Einen einfachen API-Server bauen + +Lernen Sie, wie Sie mit FastAPI-fastkit schnell einen einfachen REST-API-Server aufbauen. Dieses Tutorial richtet sich an FastAPI-Einsteiger und behandelt das Erstellen grundlegender CRUD-APIs. + +## Was Sie in diesem Tutorial lernen + +- Einen einfachen API-Server mit dem Befehl `fastkit startdemo` erstellen +- Die Struktur eines FastAPI-Projekts verstehen +- Grundlegende CRUD-Endpunkte nutzen +- Die API testen und dokumentieren +- Methoden zur Erweiterung des Projekts + +## Voraussetzungen + +- Python 3.12 oder höher installiert +- FastAPI-fastkit installiert (`pip install fastapi-fastkit`) +- Grundkenntnisse in Python + +## Schritt 1: Ein einfaches API-Projekt erstellen + +Erstellen wir einen einfachen API-Server mit der Vorlage `fastapi-default`. + +
+ +```console +$ fastkit startdemo fastapi-default +Enter the project name: my-first-api +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: My first FastAPI server +Deploying FastAPI project using 'fastapi-default' template + + Project Information +┌──────────────┬────────────────────────────┐ +│ Project Name │ my-first-api │ +│ Author │ Developer Kim │ +│ Author Email │ developer@example.com │ +│ Description │ My first FastAPI server │ +└──────────────┴────────────────────────────┘ + + Template Dependencies +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ pydantic │ +│ Dependency 4 │ pydantic-settings │ +│ Dependency 5 │ python-dotenv │ +└──────────────┴───────────────────┘ + +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y + +✨ FastAPI project 'my-first-api' from 'fastapi-default' has been created successfully! +``` + +
+ +## Schritt 2: Die generierte Projektstruktur verstehen + +Untersuchen wir die Struktur des generierten Projekts: + +``` +my-first-api/ +├── README.md # Projektdokumentation +├── requirements.txt # Liste der Abhängigkeiten +├── setup.py # Paketkonfiguration +├── scripts/ +│ └── run-server.sh # Skript zum Starten des Servers +├── src/ # Hauptquellcode +│ ├── main.py # Einstiegspunkt der FastAPI-Anwendung +│ ├── core/ +│ │ └── config.py # Verwaltung der Konfiguration +│ ├── api/ +│ │ ├── api.py # Sammlung der API-Router +│ │ └── routes/ +│ │ └── items.py # Endpunkte rund um Items +│ ├── schemas/ +│ │ └── items.py # Definitionen der Datenmodelle +│ ├── crud/ +│ │ └── items.py # Logik zur Datenverarbeitung +│ └── mocks/ +│ └── mock_items.json # Test data +└── tests/ # Test code + ├── __init__.py + ├── conftest.py + └── test_items.py +``` + +### Beschreibung der wichtigsten Dateien + +- **`src/main.py`**: Einstiegspunkt der FastAPI-Anwendung +- **`src/api/routes/items.py`**: Definitionen der item-bezogenen API-Endpunkte +- **`src/schemas/items.py`**: Definitionen der Datenstrukturen für Anfragen/Antworten +- **`src/crud/items.py`**: Logik für Datenoperationen +- **`src/mocks/mock_items.json`**: Beispieldaten für die Entwicklung + +## Schritt 3: Den Server starten + +Wechseln wir in das generierte Projektverzeichnis und starten den Server. + +
+ +```console +$ cd my-first-api +$ fastkit runserver +Starting FastAPI server at 127.0.0.1:8000... + +INFO: Will watch for changes in these directories: ['/path/to/my-first-api'] +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [12345] using WatchFiles +INFO: Started server process [12346] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +Sobald der Server erfolgreich läuft, können Sie folgende URLs in Ihrem Browser aufrufen: + +- **API-Server**: http://127.0.0.1:8000 +- **Swagger-UI-Dokumentation**: http://127.0.0.1:8000/docs +- **ReDoc-Dokumentation**: http://127.0.0.1:8000/redoc + +## Schritt 4: Die API-Endpunkte erkunden + +Die generierte API stellt standardmäßig folgende Endpunkte bereit: + +| Methode | Endpunkt | Beschreibung | +|--------|----------|-------------| +| GET | `/items/` | Alle Items abrufen | +| GET | `/items/{item_id}` | Bestimmtes Item abrufen | +| POST | `/items/` | Neues Item anlegen | +| PUT | `/items/{item_id}` | Item aktualisieren | +| DELETE | `/items/{item_id}` | Item löschen | + +### Die API testen + +**1. Alle Items abrufen** + +
+ +```console +$ curl -X GET "http://127.0.0.1:8000/items/" +[ + { + "id": 1, + "name": "Laptop", + "description": "High-performance laptop", + "price": 999.99, + "tax": 99.99 + }, + { + "id": 2, + "name": "Mouse", + "description": "Wireless mouse", + "price": 29.99, + "tax": 2.99 + } +] +``` + +
+ +**2. Ein neues Item anlegen** + +
+ +```console +$ curl -X POST "http://127.0.0.1:8000/items/" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Keyboard", + "description": "Mechanical keyboard", + "price": 150.00, + "tax": 15.00 + }' + +{ + "id": 3, + "name": "Keyboard", + "description": "Mechanical keyboard", + "price": 150.0, + "tax": 15.0 +} +``` + +
+ +**3. Ein bestimmtes Item abrufen** + +
+ +```console +$ curl -X GET "http://127.0.0.1:8000/items/1" +{ + "id": 1, + "name": "Laptop", + "description": "High-performance laptop", + "price": 999.99, + "tax": 99.99 +} +``` + +
+ +## Schritt 5: Die API mit Swagger UI testen + +Rufen Sie http://127.0.0.1:8000/docs in Ihrem Browser auf, um die automatisch generierte API-Dokumentation zu sehen. + +Was Sie mit Swagger UI machen können: + +1. **API-Endpunkte anzeigen**: alle verfügbaren Endpunkte sehen +2. **Anfrage-/Antwortschemas prüfen**: Eingabe-/Ausgabeformate jedes Endpunkts einsehen +3. **APIs direkt testen**: echte API-Aufrufe per „Try it out"-Button durchführen +4. **Beispieldaten ansehen**: Beispielanfragen/-antworten für jeden Endpunkt einsehen + +### So nutzen Sie Swagger UI + +1. Klicken Sie auf den GET-Endpunkt `/items/` +2. Klicken Sie auf die Schaltfläche „Try it out" +3. Klicken Sie auf „Execute" +4. Sehen Sie sich die Antwort des Servers an + +## Schritt 6: Die Code-Struktur verstehen + +### Hauptanwendung (`src/main.py`) + +```python +from fastapi import FastAPI +from src.api.api import api_router +from src.core.config import settings + +app = FastAPI( + title=settings.PROJECT_NAME, + version=settings.VERSION, + description=settings.DESCRIPTION, +) + +app.include_router(api_router) + +@app.get("/") +def read_root(): + return {"message": "Hello World"} +``` + +### Item-Schema (`src/schemas/items.py`) + +```python +from pydantic import BaseModel +from typing import Optional + +class ItemBase(BaseModel): + name: str + description: Optional[str] = None + price: float + tax: Optional[float] = None + +class ItemCreate(ItemBase): + pass + +class ItemUpdate(ItemBase): + name: Optional[str] = None + price: Optional[float] = None + +class Item(ItemBase): + id: int + + class Config: + from_attributes = True +``` + +### CRUD-Logik (`src/crud/items.py`) + +```python +from typing import List, Optional +from src.schemas.items import Item, ItemCreate, ItemUpdate + +class ItemCRUD: + def __init__(self): + self.items: List[Item] = [] + self.next_id = 1 + + def create_item(self, item: ItemCreate) -> Item: + new_item = Item(id=self.next_id, **item.dict()) + self.items.append(new_item) + self.next_id += 1 + return new_item + + def get_items(self) -> List[Item]: + return self.items + + def get_item(self, item_id: int) -> Optional[Item]: + return next((item for item in self.items if item.id == item_id), None) +``` + +## Schritt 7: Das Projekt erweitern + +### Neue Routen hinzufügen + +Sie können neue Endpunkte mit dem Befehl `fastkit addroute` hinzufügen: + +
+ +```console +$ fastkit addroute user + Adding New Route +┌──────────────────┬──────────────────────────────────────────┐ +│ Project │ my-first-api │ +│ Route Name │ user │ +│ Target Directory │ /path/to/my-first-api │ +└──────────────────┴──────────────────────────────────────────┘ + +Do you want to add route 'user' to the current project? [Y/n]: y + +✨ Successfully added new route 'user' to the current project! +``` + +
+ +Dieser Befehl erstellt die folgenden Dateien: + +- `src/api/routes/user.py` — Endpunkte zu Nutzern +- `src/schemas/user.py` — Datenmodelle für Nutzer +- `src/crud/user.py` — Verarbeitungslogik für Nutzerdaten + +### Umgebungskonfiguration anpassen + +Sie können die Datei `src/core/config.py` ändern, um Projektparameter anzupassen: + +```python +from pydantic_settings import BaseSettings + +class Settings(BaseSettings): + PROJECT_NAME: str = "My First API" + VERSION: str = "1.0.0" + DESCRIPTION: str = "My first FastAPI server" + API_V1_STR: str = "/api/v1" + + class Config: + env_file = ".env" + +settings = Settings() +``` + +## Schritt 8: Tests ausführen + +Das Projekt enthält grundlegende Tests: + +
+ +```console +$ pytest tests/ -v +======================== test session starts ======================== +collected 4 items + +tests/test_items.py::test_create_item PASSED [ 25%] +tests/test_items.py::test_read_items PASSED [ 50%] +tests/test_items.py::test_read_item PASSED [ 75%] +tests/test_items.py::test_update_item PASSED [100%] + +======================== 4 passed in 0.15s ======================== +``` + +
+ +## Nächste Schritte + +Sie haben den Bau eines einfachen API-Servers abgeschlossen! Nächste Schritte zum Ausprobieren: + +1. **[Asynchrone CRUD-APIs bauen](async-crud-api.md)** — lernen Sie komplexere asynchrone Verarbeitung +2. **[Datenbankintegration](database-integration.md)** — PostgreSQL und SQLAlchemy nutzen +3. **[Docker-Containerisierung](docker-deployment.md)** — auf ein Produktionsdeployment vorbereiten +4. **[Benutzerdefinierte Antwortbehandlung](custom-response-handling.md)** — erweiterte Konfiguration der Antwortformate + +## Fehlerbehebung + +### Häufige Probleme + +**F. Der Server startet nicht** +A. Stellen Sie sicher, dass Ihre virtuelle Umgebung aktiviert ist und die Abhängigkeiten korrekt installiert wurden. + +**F. Ich kann nicht auf API-Endpunkte zugreifen** +A. Prüfen Sie, dass der Server normal läuft und die Portnummer (Standard: 8000) korrekt ist. + +**F. APIs erscheinen nicht in Swagger UI** +A. Prüfen Sie, dass der Router in `src/main.py` korrekt eingebunden ist. + +## Zusammenfassung + +In diesem Tutorial haben wir mit FastAPI-fastkit: + +- ✅ Ein einfaches FastAPI-Projekt erstellt +- ✅ Die Projektstruktur verstanden +- ✅ CRUD-API-Endpunkte verwendet +- ✅ Die API dokumentiert und getestet +- ✅ Methoden zur Erweiterung des Projekts kennengelernt + +Jetzt, da Sie die Grundlagen von FastAPI kennen, wagen Sie sich an komplexere Projekte! diff --git a/docs/de/tutorial/custom-response-handling.md b/docs/de/tutorial/custom-response-handling.md new file mode 100644 index 0000000..92fcf10 --- /dev/null +++ b/docs/de/tutorial/custom-response-handling.md @@ -0,0 +1,1393 @@ +# Benutzerdefinierte Antwortbehandlung und fortgeschrittenes API-Design + +Lernen Sie, wie Sie mit den fortgeschrittenen Funktionen von FastAPI konsistente Antwortformate, Fehlerbehandlung, Paginierung und benutzerdefinierte OpenAPI-Dokumentation implementieren. Wir setzen Designmuster auf Enterprise-Niveau mit der Vorlage `fastapi-custom-response` um. + +## Was Sie in diesem Tutorial lernen + +- Standardisierte API-Antwortformate entwerfen +- Globale Ausnahmenbehandlung und benutzerdefinierte Fehlerantworten +- Paginierungssysteme implementieren +- Filterung und Sortierung +- OpenAPI-Dokumentation anpassen +- API-Versionsverwaltung +- Antwort-Caching und Optimierung + +## Voraussetzungen + +- Das [Tutorial Docker-Containerisierung](docker-deployment.md) abgeschlossen +- Verständnis der REST-API-Designprinzipien +- Kenntnisse der HTTP-Statuscodes +- Grundkonzepte von OpenAPI / Swagger + +## Die Bedeutung standardisierter API-Antworten + +### Inkonsistente vs. standardisierte Antworten + +**Problematisches Antwortformat:** +```json +// Success +{"id": 1, "name": "item"} + +// Error +{"detail": "Not found"} + +// List retrieval +[{"id": 1}, {"id": 2}] +``` + +**Standardisiertes Antwortformat:** +```json +// Success +{ + "success": true, + "data": {"id": 1, "name": "item"}, + "message": "Item retrieved successfully", + "timestamp": "2024-01-01T12:00:00Z" +} + +// Error +{ + "success": false, + "error": { + "code": "ITEM_NOT_FOUND", + "message": "Item not found", + "details": {"item_id": 123} + }, + "timestamp": "2024-01-01T12:00:00Z" +} +``` + +## Schritt 1: Ein Projekt für benutzerdefinierte Antworten erstellen + +Erstellen Sie ein Projekt mit der Vorlage `fastapi-custom-response`: + +
+ +```console +$ fastkit startdemo fastapi-custom-response +Enter the project name: advanced-api-server +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: API server with advanced response handling +Deploying FastAPI project using 'fastapi-custom-response' template + + Project Information +┌──────────────┬─────────────────────────────────────────────┐ +│ Project Name │ advanced-api-server │ +│ Author │ Developer Kim │ +│ Author Email │ developer@example.com │ +│ Description │ API server with advanced response handling │ +└──────────────┴─────────────────────────────────────────────┘ + + Template Dependencies +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ pydantic │ +│ Dependency 4 │ pydantic-settings │ +│ Dependency 5 │ aiofiles │ +│ Dependency 6 │ python-multipart │ +└──────────────┴───────────────────┘ + +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y + +✨ FastAPI project 'advanced-api-server' from 'fastapi-custom-response' has been created successfully! +``` + +
+ +## Schritt 2: Projektstruktur analysieren + +Sehen wir uns die fortgeschrittenen Funktionen des generierten Projekts an: + +``` +advanced-api-server/ +├── src/ +│ ├── main.py # FastAPI application +│ ├── schemas/ +│ │ ├── base.py # Base response schemas +│ │ ├── items.py # Item schemas +│ │ └── responses.py # Response format definitions +│ ├── helper/ +│ │ ├── exceptions.py # Custom exception classes +│ │ └── pagination.py # Pagination helpers +│ ├── utils/ +│ │ ├── responses.py # Response utilities +│ │ └── documents.py # OpenAPI documentation customization +│ ├── api/ +│ │ └── routes/ +│ │ └── items.py # Advanced API endpoints +│ ├── crud/ +│ │ └── items.py # CRUD logic +│ └── core/ +│ └── config.py # Configuration +└── tests/ + └── test_responses.py # Response format tests +``` + +## Schritt 3: Standardisierte Antwortschemas implementieren + +### Basis-Antwortschema (`src/schemas/base.py`) + +```python +from typing import Generic, TypeVar, Optional, Any, Dict, List +from pydantic import BaseModel, Field +from datetime import datetime +from enum import Enum + +T = TypeVar('T') + +class ResponseStatus(str, Enum): + """Response status""" + SUCCESS = "success" + ERROR = "error" + WARNING = "warning" + +class ErrorDetail(BaseModel): + """Error detail information""" + code: str = Field(..., description="Error code") + message: str = Field(..., description="Error message") + field: Optional[str] = Field(None, description="Field where error occurred") + details: Optional[Dict[str, Any]] = Field(None, description="Additional error information") + +class BaseResponse(BaseModel, Generic[T]): + """Base response format""" + success: bool = Field(..., description="Request success status") + status: ResponseStatus = Field(..., description="Response status") + data: Optional[T] = Field(None, description="Response data") + message: Optional[str] = Field(None, description="Response message") + timestamp: datetime = Field(default_factory=datetime.utcnow, description="Response timestamp") + request_id: Optional[str] = Field(None, description="Request tracking ID") + +class ErrorResponse(BaseModel): + """Error response format""" + success: bool = Field(False, description="Request success status") + status: ResponseStatus = Field(ResponseStatus.ERROR, description="Response status") + error: ErrorDetail = Field(..., description="Error information") + timestamp: datetime = Field(default_factory=datetime.utcnow, description="Response timestamp") + request_id: Optional[str] = Field(None, description="Request tracking ID") + +class PaginationMeta(BaseModel): + """Pagination metadata""" + page: int = Field(..., ge=1, description="Current page") + size: int = Field(..., ge=1, le=100, description="Page size") + total: int = Field(..., ge=0, description="Total number of items") + pages: int = Field(..., ge=0, description="Total number of pages") + has_next: bool = Field(..., description="Whether next page exists") + has_prev: bool = Field(..., description="Whether previous page exists") + +class PaginatedResponse(BaseModel, Generic[T]): + """Paginated response""" + success: bool = Field(True, description="Request success status") + status: ResponseStatus = Field(ResponseStatus.SUCCESS, description="Response status") + data: List[T] = Field(..., description="Data list") + meta: PaginationMeta = Field(..., description="Pagination information") + message: Optional[str] = Field(None, description="Response message") + timestamp: datetime = Field(default_factory=datetime.utcnow, description="Response time") + request_id: Optional[str] = Field(None, description="Request tracking ID") + +class ValidationErrorDetail(BaseModel): + """Validation error detail""" + field: str = Field(..., description="Validation failed field") + message: str = Field(..., description="Error message") + invalid_value: Any = Field(..., description="Invalid value") + +class ValidationErrorResponse(BaseModel): + """Validation error response""" + success: bool = Field(False, description="Request success status") + status: ResponseStatus = Field(ResponseStatus.ERROR, description="Response status") + error: ErrorDetail = Field(..., description="Error information") + validation_errors: List[ValidationErrorDetail] = Field(..., description="Validation error list") + timestamp: datetime = Field(default_factory=datetime.utcnow, description="Response time") + request_id: Optional[str] = Field(None, description="Request tracking ID") +``` + +### Hilfsfunktionen für Antworten (`src/utils/responses.py`) + +```python +from typing import Any, Optional, List, TypeVar +from fastapi import Request +from fastapi.responses import JSONResponse +import uuid + +from src.schemas.base import ( + BaseResponse, ErrorResponse, PaginatedResponse, + ResponseStatus, ErrorDetail, PaginationMeta +) + +T = TypeVar('T') + +def generate_request_id() -> str: + """Generate request tracking ID""" + return str(uuid.uuid4()) + +def success_response( + data: Any = None, + message: Optional[str] = None, + request_id: Optional[str] = None, + status_code: int = 200 +) -> JSONResponse: + """Generate success response""" + response_data = BaseResponse[Any]( + success=True, + status=ResponseStatus.SUCCESS, + data=data, + message=message or "Request processed successfully", + request_id=request_id or generate_request_id() + ) + + return JSONResponse( + status_code=status_code, + content=response_data.dict(exclude_none=True) + ) + +def error_response( + error_code: str, + error_message: str, + details: Optional[dict] = None, + status_code: int = 400, + request_id: Optional[str] = None +) -> JSONResponse: + """Generate error response""" + error_detail = ErrorDetail( + code=error_code, + message=error_message, + details=details + ) + + response_data = ErrorResponse( + error=error_detail, + request_id=request_id or generate_request_id() + ) + + return JSONResponse( + status_code=status_code, + content=response_data.dict(exclude_none=True) + ) + +def paginated_response( + data: List[T], + page: int, + size: int, + total: int, + message: Optional[str] = None, + request_id: Optional[str] = None +) -> JSONResponse: + """Generate paginated response""" + pages = (total + size - 1) // size # round up calculation + has_next = page < pages + has_prev = page > 1 + + meta = PaginationMeta( + page=page, + size=size, + total=total, + pages=pages, + has_next=has_next, + has_prev=has_prev + ) + + response_data = PaginatedResponse[T]( + data=data, + meta=meta, + message=message or f"Page {page}/{pages} data retrieved", + request_id=request_id or generate_request_id() + ) + + return JSONResponse( + status_code=200, + content=response_data.dict(exclude_none=True) + ) + +class ResponseHelper: + """Response helper class""" + + @staticmethod + def created(data: Any, message: str = "Resource created successfully") -> JSONResponse: + return success_response(data=data, message=message, status_code=201) + + @staticmethod + def updated(data: Any, message: str = "Resource updated successfully") -> JSONResponse: + return success_response(data=data, message=message, status_code=200) + + @staticmethod + def deleted(message: str = "Resource deleted successfully") -> JSONResponse: + return success_response(data=None, message=message, status_code=204) + + @staticmethod + def not_found(resource: str = "Resource") -> JSONResponse: + return error_response( + error_code="RESOURCE_NOT_FOUND", + error_message=f"{resource} not found", + status_code=404 + ) + + @staticmethod + def bad_request(message: str = "Bad request") -> JSONResponse: + return error_response( + error_code="BAD_REQUEST", + error_message=message, + status_code=400 + ) + + @staticmethod + def unauthorized(message: str = "Authentication required") -> JSONResponse: + return error_response( + error_code="UNAUTHORIZED", + error_message=message, + status_code=401 + ) + + @staticmethod + def forbidden(message: str = "Permission denied") -> JSONResponse: + return error_response( + error_code="FORBIDDEN", + error_message=message, + status_code=403 + ) + + @staticmethod + def server_error(message: str = "Server internal error occurred") -> JSONResponse: + return error_response( + error_code="INTERNAL_SERVER_ERROR", + error_message=message, + status_code=500 + ) +``` + +## Schritt 4: System zur Behandlung benutzerdefinierter Ausnahmen + +### Benutzerdefinierte Ausnahmeklassen (`src/helper/exceptions.py`) + +```python +from typing import Optional, Dict, Any +from fastapi import HTTPException + +class BaseAPIException(HTTPException): + """Base API exception class""" + + def __init__( + self, + error_code: str, + message: str, + status_code: int = 400, + details: Optional[Dict[str, Any]] = None + ): + self.error_code = error_code + self.message = message + self.details = details or {} + super().__init__(status_code=status_code, detail=message) + +class ValidationException(BaseAPIException): + """Validation exception""" + + def __init__(self, message: str, field: Optional[str] = None, details: Optional[Dict] = None): + super().__init__( + error_code="VALIDATION_ERROR", + message=message, + status_code=422, + details=details or {"field": field} if field else None + ) + +class ResourceNotFoundException(BaseAPIException): + """Resource not found exception""" + + def __init__(self, resource: str, resource_id: Any): + super().__init__( + error_code="RESOURCE_NOT_FOUND", + message=f"{resource}(ID: {resource_id}) not found", + status_code=404, + details={"resource": resource, "id": resource_id} + ) + +class DuplicateResourceException(BaseAPIException): + """Duplicate resource exception""" + + def __init__(self, resource: str, field: str, value: Any): + super().__init__( + error_code="DUPLICATE_RESOURCE", + message=f"{resource} {field} '{value}' already exists", + status_code=409, + details={"resource": resource, "field": field, "value": value} + ) + +class BusinessLogicException(BaseAPIException): + """Business logic exception""" + + def __init__(self, message: str, error_code: str = "BUSINESS_LOGIC_ERROR"): + super().__init__( + error_code=error_code, + message=message, + status_code=422 + ) + +class RateLimitException(BaseAPIException): + """Request limit exception""" + + def __init__(self, retry_after: int = 60): + super().__init__( + error_code="RATE_LIMIT_EXCEEDED", + message="Request limit exceeded. Please try again later", + status_code=429, + details={"retry_after": retry_after} + ) + +class AuthenticationException(BaseAPIException): + """Authentication exception""" + + def __init__(self, message: str = "Authentication required"): + super().__init__( + error_code="AUTHENTICATION_REQUIRED", + message=message, + status_code=401 + ) + +class AuthorizationException(BaseAPIException): + """Authorization exception""" + + def __init__(self, message: str = "Permission denied"): + super().__init__( + error_code="INSUFFICIENT_PERMISSIONS", + message=message, + status_code=403 + ) +``` + +### Globaler Exception-Handler (`src/main.py`) + +```python +from fastapi import FastAPI, Request, status +from fastapi.exceptions import RequestValidationError, HTTPException +from fastapi.responses import JSONResponse +from pydantic import ValidationError +import logging +import traceback + +from src.helper.exceptions import BaseAPIException +from src.utils.responses import error_response, generate_request_id +from src.schemas.base import ValidationErrorDetail, ValidationErrorResponse + +logger = logging.getLogger(__name__) + +app = FastAPI( + title="Advanced API Server", + description="API server with advanced response handling", + version="1.0.0" +) + +@app.exception_handler(BaseAPIException) +async def custom_api_exception_handler(request: Request, exc: BaseAPIException): + """Custom API exception handler""" + request_id = generate_request_id() + + logger.error( + f"API Exception: {exc.error_code} - {exc.message}", + extra={ + "request_id": request_id, + "path": request.url.path, + "method": request.method, + "details": exc.details + } + ) + + return error_response( + error_code=exc.error_code, + error_message=exc.message, + details=exc.details, + status_code=exc.status_code, + request_id=request_id + ) + +@app.exception_handler(RequestValidationError) +async def validation_exception_handler(request: Request, exc: RequestValidationError): + """Pydantic validation exception handler""" + request_id = generate_request_id() + + validation_errors = [] + for error in exc.errors(): + field = ".".join(str(loc) for loc in error["loc"]) + validation_errors.append( + ValidationErrorDetail( + field=field, + message=error["msg"], + invalid_value=error.get("input", "") + ) + ) + + error_response_data = ValidationErrorResponse( + error={ + "code": "VALIDATION_ERROR", + "message": "Input data validation failed", + "details": {"error_count": len(validation_errors)} + }, + validation_errors=validation_errors, + request_id=request_id + ) + + logger.warning( + f"Validation Error: {len(validation_errors)} validation errors", + extra={ + "request_id": request_id, + "path": request.url.path, + "method": request.method, + "errors": [err.dict() for err in validation_errors] + } + ) + + return JSONResponse( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + content=error_response_data.dict(exclude_none=True) + ) + +@app.exception_handler(HTTPException) +async def http_exception_handler(request: Request, exc: HTTPException): + """HTTP exception handler""" + request_id = generate_request_id() + + error_code_map = { + 400: "BAD_REQUEST", + 401: "UNAUTHORIZED", + 403: "FORBIDDEN", + 404: "NOT_FOUND", + 405: "METHOD_NOT_ALLOWED", + 500: "INTERNAL_SERVER_ERROR" + } + + error_code = error_code_map.get(exc.status_code, "HTTP_ERROR") + + return error_response( + error_code=error_code, + error_message=exc.detail, + status_code=exc.status_code, + request_id=request_id + ) + +@app.exception_handler(Exception) +async def general_exception_handler(request: Request, exc: Exception): + """General exception handler""" + request_id = generate_request_id() + + logger.error( + f"Unhandled Exception: {type(exc).__name__} - {str(exc)}", + extra={ + "request_id": request_id, + "path": request.url.path, + "method": request.method, + "traceback": traceback.format_exc() + } + ) + + return error_response( + error_code="INTERNAL_SERVER_ERROR", + error_message="Unexpected error occurred", + status_code=500, + request_id=request_id + ) +``` + +## Schritt 5: Fortgeschrittenes Paginierungssystem + +### Paginierungs-Helfer (`src/helper/pagination.py`) + +```python +from typing import List, Optional, Any, Dict, Callable +from pydantic import BaseModel, Field +from fastapi import Query +from enum import Enum + +class SortOrder(str, Enum): + """Sort order""" + ASC = "asc" + DESC = "desc" + +class PaginationParams(BaseModel): + """Pagination parameters""" + page: int = Field(1, ge=1, description="Page number") + size: int = Field(20, ge=1, le=100, description="Page size") + sort_by: Optional[str] = Field(None, description="Sort field") + sort_order: SortOrder = Field(SortOrder.ASC, description="Sort order") + +class FilterParams(BaseModel): + """Filtering parameters""" + search: Optional[str] = Field(None, description="Search term") + category: Optional[str] = Field(None, description="Category") + status: Optional[str] = Field(None, description="Status") + date_from: Optional[str] = Field(None, description="Start date (YYYY-MM-DD)") + date_to: Optional[str] = Field(None, description="End date (YYYY-MM-DD)") + +def pagination_params( + page: int = Query(1, ge=1, description="Page number"), + size: int = Query(20, ge=1, le=100, description="Page size"), + sort_by: Optional[str] = Query(None, description="Sort field"), + sort_order: SortOrder = Query(SortOrder.ASC, description="Sort order") +) -> PaginationParams: + """Pagination parameters dependency""" + return PaginationParams( + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order + ) + +def filter_params( + search: Optional[str] = Query(None, description="Search term"), + category: Optional[str] = Query(None, description="Category"), + status: Optional[str] = Query(None, description="Status"), + date_from: Optional[str] = Query(None, description="Start date (YYYY-MM-DD)"), + date_to: Optional[str] = Query(None, description="End date (YYYY-MM-DD)") +) -> FilterParams: + """Filtering parameters dependency""" + return FilterParams( + search=search, + category=category, + status=status, + date_from=date_from, + date_to=date_to + ) + +class AdvancedPaginator: + """Advanced pagination class""" + + def __init__(self, data: List[Any], pagination: PaginationParams, filters: FilterParams): + self.data = data + self.pagination = pagination + self.filters = filters + self.filtered_data = self._apply_filters() + self.sorted_data = self._apply_sorting() + + def _apply_filters(self) -> List[Any]: + """Apply filters""" + filtered = self.data + + if self.filters.search: + # Filter by search term (example: search in name or description fields) + search_term = self.filters.search.lower() + filtered = [ + item for item in filtered + if (hasattr(item, 'name') and search_term in item.name.lower()) or + (hasattr(item, 'description') and item.description and search_term in item.description.lower()) + ] + + if self.filters.category: + filtered = [item for item in filtered if hasattr(item, 'category') and item.category == self.filters.category] + + if self.filters.status: + filtered = [item for item in filtered if hasattr(item, 'status') and item.status == self.filters.status] + + # Implement date filtering (if date field exists) + if self.filters.date_from or self.filters.date_to: + from datetime import datetime + filtered = self._apply_date_filter(filtered) + + return filtered + + def _apply_date_filter(self, data: List[Any]) -> List[Any]: + """Apply date filter""" + from datetime import datetime + + if not self.filters.date_from and not self.filters.date_to: + return data + + filtered = [] + for item in data: + if not hasattr(item, 'created_at'): + continue + + item_date = item.created_at.date() if hasattr(item.created_at, 'date') else item.created_at + + if self.filters.date_from: + start_date = datetime.strptime(self.filters.date_from, "%Y-%m-%d").date() + if item_date < start_date: + continue + + if self.filters.date_to: + end_date = datetime.strptime(self.filters.date_to, "%Y-%m-%d").date() + if item_date > end_date: + continue + + filtered.append(item) + + return filtered + + def _apply_sorting(self) -> List[Any]: + """Apply sorting""" + if not self.pagination.sort_by: + return self.filtered_data + + reverse = self.pagination.sort_order == SortOrder.DESC + + try: + return sorted( + self.filtered_data, + key=lambda x: getattr(x, self.pagination.sort_by, 0), + reverse=reverse + ) + except (AttributeError, TypeError): + # Return original data if sort field is not found or cannot be sorted + return self.filtered_data + + def get_page(self) -> tuple[List[Any], int]: + """Return current page data and total count""" + total = len(self.sorted_data) + start = (self.pagination.page - 1) * self.pagination.size + end = start + self.pagination.size + + page_data = self.sorted_data[start:end] + return page_data, total + + def get_metadata(self) -> Dict[str, Any]: + """Return pagination metadata""" + total = len(self.sorted_data) + pages = (total + self.pagination.size - 1) // self.pagination.size + + return { + "page": self.pagination.page, + "size": self.pagination.size, + "total": total, + "pages": pages, + "has_next": self.pagination.page < pages, + "has_prev": self.pagination.page > 1, + "filters_applied": { + "search": self.filters.search, + "category": self.filters.category, + "status": self.filters.status, + "date_range": f"{self.filters.date_from} ~ {self.filters.date_to}" if self.filters.date_from or self.filters.date_to else None + }, + "sorting": { + "field": self.pagination.sort_by, + "order": self.pagination.sort_order + } if self.pagination.sort_by else None + } +``` + +## Schritt 6: Erweiterte API-Endpunkte implementieren + +### Item-API-Router (`src/api/routes/items.py`) + +```python +from typing import List, Optional +from fastapi import APIRouter, Depends, HTTPException, Query, Path, BackgroundTasks +from fastapi.responses import JSONResponse + +from src.schemas.items import Item, ItemCreate, ItemUpdate, ItemResponse +from src.helper.pagination import pagination_params, filter_params, PaginationParams, FilterParams, AdvancedPaginator +from src.helper.exceptions import ResourceNotFoundException, DuplicateResourceException, ValidationException +from src.utils.responses import success_response, paginated_response, ResponseHelper +from src.crud.items import ItemCRUD + +router = APIRouter(prefix="/items", tags=["items"]) +crud = ItemCRUD() + +@router.post("/", response_model=dict, status_code=201) +async def create_item( + item_create: ItemCreate, + background_tasks: BackgroundTasks +) -> JSONResponse: + """ + Create a new item + + - **name**: Item name (required) + - **description**: Item description (optional) + - **price**: Price (required, 0 or greater) + - **category**: Category (optional) + """ + # Check for duplicates + existing_item = await crud.get_by_name(item_create.name) + if existing_item: + raise DuplicateResourceException("Item", "name", item_create.name) + + # Business logic validation + if item_create.price < 0: + raise ValidationException("Price must be 0 or greater", "price") + + # Create item + created_item = await crud.create(item_create) + + # Background tasks (e.g. sending notifications, logging, etc.) + background_tasks.add_task(send_creation_notification, created_item.id) + + return ResponseHelper.created( + data=created_item.dict(), + message=f"Item '{created_item.name}' created successfully" + ) + +@router.get("/", response_model=dict) +async def list_items( + pagination: PaginationParams = Depends(pagination_params), + filters: FilterParams = Depends(filter_params) +) -> JSONResponse: + """ + Get item list (pagination, filtering, sorting supported) + + **Pagination:** + - page: Page number (default: 1) + - size: Page size (default: 20, maximum: 100) + + **Sorting:** + - sort_by: Sort field (name, price, created_at, etc.) + - sort_order: Sort order (asc, desc) + + **Filtering:** + - search: Search term (search in name or description fields) + - category: Category filter + - status: Status filter + - date_from: Start date (YYYY-MM-DD) + - date_to: End date (YYYY-MM-DD) + """ + # Get all items + all_items = await crud.get_all() + + # Apply advanced pagination + paginator = AdvancedPaginator(all_items, pagination, filters) + page_data, total = paginator.get_page() + + # Include additional metadata in response + metadata = paginator.get_metadata() + + # Create custom message + message = f"Total {total} items, {len(page_data)} items retrieved" + if filters.search: + message += f" (Search term: '{filters.search}')" + + return paginated_response( + data=[item.dict() for item in page_data], + page=pagination.page, + size=pagination.size, + total=total, + message=message + ) + +@router.get("/search/advanced", response_model=dict) +async def advanced_search( + q: str = Query(..., min_length=1, description="Search term"), + fields: List[str] = Query(["name", "description"], description="Search fields"), + exact_match: bool = Query(False, description="Exact match"), + case_sensitive: bool = Query(False, description="Case sensitive"), + pagination: PaginationParams = Depends(pagination_params) +) -> JSONResponse: + """ + Advanced search functionality + + - **q**: Search term (required) + - **fields**: Search fields list + - **exact_match**: Exact match + - **case_sensitive**: Case sensitive + """ + results = await crud.advanced_search( + query=q, + fields=fields, + exact_match=exact_match, + case_sensitive=case_sensitive + ) + + # Apply pagination + total = len(results) + start = (pagination.page - 1) * pagination.size + end = start + pagination.size + page_data = results[start:end] + + return paginated_response( + data=[item.dict() for item in page_data], + page=pagination.page, + size=pagination.size, + total=total, + message=f"'{q}' search results: {total} items" + ) + +@router.get("/{item_id}", response_model=dict) +async def get_item( + item_id: int = Path(..., gt=0, description="Item ID") +) -> JSONResponse: + """Get specific item""" + item = await crud.get_by_id(item_id) + if not item: + raise ResourceNotFoundException("Item", item_id) + + return success_response( + data=item.dict(), + message=f"Item '{item.name}' retrieved successfully" + ) + +@router.put("/{item_id}", response_model=dict) +async def update_item( + item_id: int = Path(..., gt=0, description="Item ID"), + item_update: ItemUpdate +) -> JSONResponse: + """Update item""" + existing_item = await crud.get_by_id(item_id) + if not existing_item: + raise ResourceNotFoundException("Item", item_id) + + # Check for duplicate name (with other items) + if item_update.name and item_update.name != existing_item.name: + duplicate = await crud.get_by_name(item_update.name) + if duplicate: + raise DuplicateResourceException("Item", "name", item_update.name) + + updated_item = await crud.update(item_id, item_update) + + return ResponseHelper.updated( + data=updated_item.dict(), + message=f"Item '{updated_item.name}' updated successfully" + ) + +@router.delete("/{item_id}", response_model=dict, status_code=204) +async def delete_item( + item_id: int = Path(..., gt=0, description="Item ID"), + force: bool = Query(False, description="Force delete") +) -> JSONResponse: + """Delete item""" + item = await crud.get_by_id(item_id) + if not item: + raise ResourceNotFoundException("Item", item_id) + + # Validation before deletion (e.g. related orders) + if not force and await crud.has_related_orders(item_id): + raise ValidationException( + "Related orders exist, cannot be deleted. Use force=true to force delete" + ) + + await crud.delete(item_id) + + return ResponseHelper.deleted( + message=f"Item '{item.name}' deleted successfully" + ) + +@router.post("/bulk", response_model=dict) +async def bulk_create_items( + items: List[ItemCreate], + skip_duplicates: bool = Query(False, description="Skip duplicates") +) -> JSONResponse: + """Bulk create items""" + if len(items) > 100: + raise ValidationException("Maximum 100 items can be created at once") + + created_items = [] + skipped_items = [] + errors = [] + + for i, item_create in enumerate(items): + try: + # Check for duplicates + existing = await crud.get_by_name(item_create.name) + if existing: + if skip_duplicates: + skipped_items.append({"index": i, "name": item_create.name, "reason": "Duplicate name"}) + continue + else: + errors.append({"index": i, "name": item_create.name, "error": "Duplicate name"}) + continue + + created_item = await crud.create(item_create) + created_items.append(created_item) + + except Exception as e: + errors.append({"index": i, "name": item_create.name, "error": str(e)}) + + result = { + "created_count": len(created_items), + "skipped_count": len(skipped_items), + "error_count": len(errors), + "created_items": [item.dict() for item in created_items], + "skipped_items": skipped_items, + "errors": errors + } + + message = f"{len(created_items)} items created" + if skipped_items: + message += f", {len(skipped_items)} skipped" + if errors: + message += f", {len(errors)} errors" + + return success_response(data=result, message=message) + +async def send_creation_notification(item_id: int): + """Benachrichtigung bei Item-Erstellung (Hintergrundaufgabe)""" + # In einer echten Implementierung würde hier z. B. per E-Mail oder Slack benachrichtigt + import asyncio + await asyncio.sleep(1) # Simulation + print(f"Item {item_id} creation notification sent") +``` + +## Schritt 7: OpenAPI-Dokumentation anpassen + +### OpenAPI-Anpassung (`src/utils/documents.py`) + +```python +from fastapi import FastAPI +from fastapi.openapi.utils import get_openapi +from typing import Dict, Any + +def custom_openapi(app: FastAPI) -> Dict[str, Any]: + """Create custom OpenAPI schema""" + if app.openapi_schema: + return app.openapi_schema + + openapi_schema = get_openapi( + title=app.title, + version=app.version, + description=app.description, + routes=app.routes, + ) + + # Add custom information + openapi_schema["info"].update({ + "contact": { + "name": "API Support", + "url": "https://example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "MIT", + "url": "https://opensource.org/licenses/MIT" + }, + "termsOfService": "https://example.com/terms" + }) + + # Add server information + openapi_schema["servers"] = [ + { + "url": "https://api.example.com", + "description": "Production server" + }, + { + "url": "https://staging-api.example.com", + "description": "Staging server" + }, + { + "url": "http://localhost:8000", + "description": "Entwicklungsserver" + } + ] + + # Add common response schema + openapi_schema["components"]["schemas"].update({ + "SuccessResponse": { + "type": "object", + "properties": { + "success": {"type": "boolean", "example": True}, + "status": {"type": "string", "example": "success"}, + "data": {"type": "object"}, + "message": {"type": "string", "example": "Request processed successfully"}, + "timestamp": {"type": "string", "format": "date-time"}, + "request_id": {"type": "string", "example": "123e4567-e89b-12d3-a456-426614174000"} + } + }, + "ErrorResponse": { + "type": "object", + "properties": { + "success": {"type": "boolean", "example": False}, + "status": {"type": "string", "example": "error"}, + "error": { + "type": "object", + "properties": { + "code": {"type": "string", "example": "RESOURCE_NOT_FOUND"}, + "message": {"type": "string", "example": "Resource not found"}, + "details": {"type": "object"} + } + }, + "timestamp": {"type": "string", "format": "date-time"}, + "request_id": {"type": "string", "example": "123e4567-e89b-12d3-a456-426614174000"} + } + } + }) + + # Add tag grouping and description + openapi_schema["tags"] = [ + { + "name": "items", + "description": "Item management API", + "externalDocs": { + "description": "More information", + "url": "https://example.com/docs/items" + } + }, + { + "name": "health", + "description": "System status check API" + } + ] + + # Add security schema + openapi_schema["components"]["securitySchemes"] = { + "BearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + }, + "ApiKeyAuth": { + "type": "apiKey", + "in": "header", + "name": "X-API-Key" + } + } + + app.openapi_schema = openapi_schema + return app.openapi_schema + +def setup_docs(app: FastAPI): + """Setup documentation""" + app.openapi = lambda: custom_openapi(app) + + # Swagger UI setup + app.docs_url = "/docs" + app.redoc_url = "/redoc" + + # Additional document endpoint + @app.get("/openapi.json", include_in_schema=False) + async def get_openapi_endpoint(): + return custom_openapi(app) +``` + +### Anwendung in der Hauptanwendung (Ergänzung in `src/main.py`) + +```python +from src.utils.documents import setup_docs +from src.api.routes import items + +# Include router +app.include_router(items.router, prefix="/api/v1") + +# Apply documentation setup +setup_docs(app) + +# Add request ID middleware +@app.middleware("http") +async def add_request_id(request: Request, call_next): + request_id = generate_request_id() + request.state.request_id = request_id + + response = await call_next(request) + response.headers["X-Request-ID"] = request_id + + return response +``` + +## Schritt 8: Caching-System implementieren + +### Antwort-Caching (`src/utils/cache.py`) + +```python +from typing import Optional, Any, Dict +from functools import wraps +import asyncio +import json +import hashlib +from datetime import datetime, timedelta + +class MemoryCache: + """Memory-based cache""" + + def __init__(self): + self._cache: Dict[str, Dict[str, Any]] = {} + + async def get(self, key: str) -> Optional[Any]: + """Get value from cache""" + if key not in self._cache: + return None + + item = self._cache[key] + if datetime.utcnow() > item["expires_at"]: + del self._cache[key] + return None + + return item["value"] + + async def set(self, key: str, value: Any, ttl_seconds: int = 300): + """Save value to cache""" + self._cache[key] = { + "value": value, + "expires_at": datetime.utcnow() + timedelta(seconds=ttl_seconds), + "created_at": datetime.utcnow() + } + + async def delete(self, key: str): + """Delete value from cache""" + self._cache.pop(key, None) + + async def clear(self): + """Delete all cache""" + self._cache.clear() + + def get_stats(self) -> Dict[str, Any]: + """Cache statistics""" + now = datetime.utcnow() + valid_items = [ + item for item in self._cache.values() + if now <= item["expires_at"] + ] + + return { + "total_items": len(self._cache), + "valid_items": len(valid_items), + "expired_items": len(self._cache) - len(valid_items), + "memory_usage_mb": len(str(self._cache)) / 1024 / 1024 + } + +# Global cache instance +cache = MemoryCache() + +def cache_response(ttl_seconds: int = 300, key_prefix: str = ""): + """Dekorator für Response-Caching""" + def decorator(func): + @wraps(func) + async def wrapper(*args, **kwargs): + # Generate cache key + cache_key = generate_cache_key(func.__name__, args, kwargs, key_prefix) + + # Get from cache + cached_response = await cache.get(cache_key) + if cached_response: + return cached_response + + # Execute function + response = await func(*args, **kwargs) + + # Cache response + await cache.set(cache_key, response, ttl_seconds) + + return response + return wrapper + return decorator + +def generate_cache_key(func_name: str, args: tuple, kwargs: dict, prefix: str = "") -> str: + """Generate cache key""" + # Generate unique key based on function name and arguments + key_data = { + "function": func_name, + "args": str(args), + "kwargs": sorted(kwargs.items()) + } + + key_string = json.dumps(key_data, sort_keys=True) + key_hash = hashlib.md5(key_string.encode()).hexdigest() + + return f"{prefix}:{func_name}:{key_hash}" if prefix else f"{func_name}:{key_hash}" + +# Cache management endpoint +@app.get("/admin/cache/stats") +async def get_cache_stats(): + """Get cache statistics""" + stats = cache.get_stats() + return success_response(data=stats, message="Cache statistics retrieved") + +@app.delete("/admin/cache/clear") +async def clear_cache(): + """Delete all cache""" + await cache.clear() + return success_response(message="Cache deleted successfully") +``` + +### Beispiel zur Anwendung des Caches + +```python +# Caching in `src/api/routes/items.py` anwenden + +from src.utils.cache import cache_response + +@router.get("/", response_model=dict) +@cache_response(ttl_seconds=60, key_prefix="items_list") # 1 Minute cachen +async def list_items( + pagination: PaginationParams = Depends(pagination_params), + filters: FilterParams = Depends(filter_params) +) -> JSONResponse: + # ... existing code ... + +@router.get("/{item_id}", response_model=dict) +@cache_response(ttl_seconds=300, key_prefix="item_detail") # 5 Minuten cachen +async def get_item(item_id: int = Path(..., gt=0)) -> JSONResponse: + # ... existing code ... +``` + +## Schritt 9: API testen + +### Server starten und Grundlagen testen + +
+ +```console +$ cd advanced-api-server +$ fastkit runserver +Starting FastAPI server at 127.0.0.1:8000... + +# Custom response format test +$ curl -X POST "http://localhost:8000/api/v1/items/" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Advanced notebook", + "description": "Notebook with latest technology", + "price": 2500000, + "category": "electronics" + }' + +{ + "success": true, + "status": "success", + "data": { + "id": 1, + "name": "Advanced notebook", + "description": "Notebook with latest technology", + "price": 2500000, + "category": "electronics", + "created_at": "2024-01-01T12:00:00Z" + }, + "message": "Item 'Advanced notebook' created successfully", + "timestamp": "2024-01-01T12:00:00.123456Z", + "request_id": "123e4567-e89b-12d3-a456-426614174000" +} + +# Pagination and filtering test +$ curl "http://localhost:8000/api/v1/items/?page=1&size=10&search=notebook&sort_by=price&sort_order=desc" + +# Advanced search test +$ curl "http://localhost:8000/api/v1/items/search/advanced?q=notebook&fields=name&fields=description&exact_match=false" + +# Error response test +$ curl "http://localhost:8000/api/v1/items/999" + +{ + "success": false, + "status": "error", + "error": { + "code": "RESOURCE_NOT_FOUND", + "message": "Item (ID: 999) not found", + "details": { + "resource": "Item", + "id": 999 + } + }, + "timestamp": "2024-01-01T12:00:00.123456Z", + "request_id": "123e4567-e89b-12d3-a456-426614174000" +} +``` + +
+ +### OpenAPI-Dokumentation prüfen + +Rufen Sie http://localhost:8000/docs auf, um die angepasste API-Dokumentation zu sehen. + +## Nächste Schritte + +Sie haben das System zur benutzerdefinierten Antwortbehandlung abgeschlossen! Nächste Schritte zum Ausprobieren: + +1. **[MCP-Integration](mcp-integration.md)** — das Model Context Protocol implementieren + + + + +## Zusammenfassung + +In diesem Tutorial haben wir ein fortgeschrittenes System zur Antwortbehandlung implementiert: + +- ✅ Standardisierte API-Antwortformate entworfen +- ✅ Globale Ausnahmenbehandlung und benutzerdefinierte Fehlerantworten +- ✅ Fortgeschrittene Paginierungs- und Filterungssysteme +- ✅ OpenAPI-Dokumentation angepasst +- ✅ Antwort-Caching und Leistungsoptimierung +- ✅ System zur Nachverfolgung von Anfragen +- ✅ Verarbeitung von Hintergrundaufgaben +- ✅ APIs für Batch-Operationen + +Jetzt können Sie alle Kernfunktionen von API-Servern auf Enterprise-Niveau umsetzen! diff --git a/docs/de/tutorial/database-integration.md b/docs/de/tutorial/database-integration.md new file mode 100644 index 0000000..0ad41bd --- /dev/null +++ b/docs/de/tutorial/database-integration.md @@ -0,0 +1,1028 @@ +# Datenbankintegration (PostgreSQL + SQLAlchemy) + +Bauen Sie eine FastAPI-Anwendung mit PostgreSQL-Datenbank und SQLAlchemy-ORM, die in echten Produktionsumgebungen eingesetzt werden kann. In diesem Tutorial implementieren wir ein vollständiges Datenbankintegrationssystem mit der Vorlage `fastapi-psql-orm`. + +## Was Sie in diesem Tutorial lernen + +- PostgreSQL-Datenbank einrichten und integrieren +- Datenmodellierung mit SQLAlchemy-ORM +- Datenbankmigrationen mit Alembic +- Entwicklungsumgebung mit Docker Compose einrichten +- Verbindungspool der Datenbank verwalten +- Transaktionsverarbeitung und Datenintegrität + +## Voraussetzungen + +- Das [Tutorial Asynchrone CRUD-API](async-crud-api.md) abgeschlossen +- Docker und Docker Compose installiert +- Grundkenntnisse von PostgreSQL +- Verständnis der Grundkonzepte des SQLAlchemy-ORM + +## Warum PostgreSQL und SQLAlchemy? + +### Vergleich JSON-Dateien vs. PostgreSQL + +| Kategorie | JSON-Dateien | PostgreSQL | +|----------|------------|------------| +| **Leistung** | Begrenzt | Hochleistungs-Indexierung | +| **Nebenläufigkeit** | Datei-Sperrprobleme | Transaktionsunterstützung | +| **Skalierbarkeit** | Speicherbegrenzt | Verarbeitung großer Datenmengen | +| **Integrität** | Nicht garantiert | ACID-Eigenschaften garantiert | +| **Abfragen** | Müssen alle Daten laden | Unterstützung komplexer Abfragen | +| **Backup** | Dateikopie | Vollständiges Backup/Restore | + +## Schritt 1: Ein PostgreSQL-+-ORM-Projekt erstellen + +Erstellen Sie ein Projekt mit der Vorlage `fastapi-psql-orm`: + +
+ +```console +$ fastkit startdemo fastapi-psql-orm +Enter the project name: todo-postgres-api +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: Todo management API using PostgreSQL +Deploying FastAPI project using 'fastapi-psql-orm' template + + Project Information +┌──────────────┬─────────────────────────────────────────┐ +│ Project Name │ todo-postgres-api │ +│ Author │ Developer Kim │ +│ Author Email │ developer@example.com │ +│ Description │ Todo management API using PostgreSQL │ +└──────────────┴─────────────────────────────────────────┘ + + Template Dependencies +┌──────────────┬────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ sqlalchemy │ +│ Dependency 4 │ alembic │ +│ Dependency 5 │ psycopg2 │ +│ Dependency 6 │ asyncpg │ +│ Dependency 7 │ sqlmodel │ +└──────────────┴────────────────┘ + +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y + +✨ FastAPI project 'todo-postgres-api' from 'fastapi-psql-orm' has been created successfully! +``` + +
+ +## Schritt 2: Projektstruktur analysieren + +Das generierte Projekt bietet eine vollständige Datenbankintegrationsumgebung: + +``` +todo-postgres-api/ +├── docker-compose.yml # PostgreSQL container configuration +├── Dockerfile # Application container +├── alembic.ini # Alembic configuration +├── template-config.yml # Template configuration +├── scripts/ +│ ├── pre-start.sh # Pre-start initialization +│ └── test.sh # Test execution script +├── src/ +│ ├── main.py # FastAPI application +│ ├── core/ +│ │ ├── config.py # Environment configuration +│ │ └── db.py # Database connection setup +│ ├── api/ +│ │ ├── deps.py # Dependency injection +│ │ └── routes/ +│ │ └── items.py # API endpoints +│ ├── crud/ +│ │ └── items.py # Database operations +│ ├── schemas/ +│ │ └── items.py # Pydantic models +│ ├── utils/ +│ │ ├── backend_pre_start.py # Backend initialization +│ │ ├── init_data.py # Initial data loading +│ │ └── tests_pre_start.py # Test preparation +│ └── alembic/ +│ ├── env.py # Alembic environment configuration +│ └── versions/ # Migration files +└── tests/ + ├── conftest.py # Test configuration + └── test_items.py # API tests +``` + +### Kernkomponenten + +1. **SQLModel**: SQLAlchemy + Pydantic-Integration +2. **Alembic**: Schema-Migration für die Datenbank +3. **asyncpg**: asynchroner PostgreSQL-Treiber +4. **Docker Compose**: Containerisierung der Entwicklungsumgebung + +## Schritt 3: Die Datenbankkonfiguration verstehen + +### Konfiguration der Datenbankverbindung (`src/core/db.py`) + +```python +from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine +from sqlalchemy.orm import sessionmaker +from sqlmodel import SQLModel + +from src.core.config import settings + +# Create asynchronous PostgreSQL engine +engine = create_async_engine( + settings.DATABASE_URL, + echo=settings.DEBUG, # Output SQL logs + pool_size=20, # Connection pool size + max_overflow=0, # Number of additional connections allowed + pool_pre_ping=True, # Check connection status +) + +# Asynchronous session factory +AsyncSessionLocal = sessionmaker( + autocommit=False, + autoflush=False, + bind=engine, + class_=AsyncSession, + expire_on_commit=False, +) + +async def create_tables(): + """Create database tables""" + async with engine.begin() as conn: + await conn.run_sync(SQLModel.metadata.create_all) + +async def get_session() -> AsyncSession: + """Provide database session (for dependency injection)""" + async with AsyncSessionLocal() as session: + try: + yield session + finally: + await session.close() +``` + +### Umgebungskonfiguration (`src/core/config.py`) + +```python +from pydantic_settings import BaseSettings +from typing import Optional + +class Settings(BaseSettings): + PROJECT_NAME: str = "Todo PostgreSQL API" + VERSION: str = "1.0.0" + DESCRIPTION: str = "Todo management API using PostgreSQL" + + # Database configuration + POSTGRES_SERVER: str = "localhost" + POSTGRES_USER: str = "postgres" + POSTGRES_PASSWORD: str = "password" + POSTGRES_DB: str = "todoapp" + POSTGRES_PORT: int = 5432 + + # Test database + TEST_DATABASE_URL: Optional[str] = None + + # Debug mode + DEBUG: bool = False + + @property + def DATABASE_URL(self) -> str: + """Generate PostgreSQL connection URL""" + return ( + f"postgresql+asyncpg://{self.POSTGRES_USER}:" + f"{self.POSTGRES_PASSWORD}@{self.POSTGRES_SERVER}:" + f"{self.POSTGRES_PORT}/{self.POSTGRES_DB}" + ) + + class Config: + env_file = ".env" + +settings = Settings() +``` + +## Schritt 4: Datenmodell definieren + +### Datenmodell mit SQLModel (`src/schemas/items.py`) + +```python +from sqlmodel import SQLModel, Field +from typing import Optional +from datetime import datetime + +# Define common fields +class ItemBase(SQLModel): + name: str = Field(index=True, max_length=100) + description: Optional[str] = Field(default=None, max_length=500) + price: float = Field(gt=0, description="Price must be greater than 0") + tax: Optional[float] = Field(default=None, ge=0) + is_active: bool = Field(default=True) + +# Database table model +class Item(ItemBase, table=True): + __tablename__ = "items" + + id: Optional[int] = Field(default=None, primary_key=True) + created_at: datetime = Field(default_factory=datetime.utcnow) + updated_at: Optional[datetime] = Field(default=None) + + # Set index + class Config: + schema_extra = { + "example": { + "name": "notebook", + "description": "High-performance gaming notebook", + "price": 1500000.0, + "tax": 150000.0, + "is_active": True + } + } + +# API request/response model +class ItemCreate(ItemBase): + pass + +class ItemUpdate(SQLModel): + name: Optional[str] = Field(default=None, max_length=100) + description: Optional[str] = Field(default=None, max_length=500) + price: Optional[float] = Field(default=None, gt=0) + tax: Optional[float] = Field(default=None, ge=0) + is_active: Optional[bool] = Field(default=None) + +class ItemResponse(ItemBase): + id: int + created_at: datetime + updated_at: Optional[datetime] +``` + +## Schritt 5: CRUD-Operationen implementieren + +### Datenbank-CRUD-Logik (`src/crud/items.py`) + +```python +from typing import List, Optional +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, update, delete +from sqlalchemy.orm import selectinload +from datetime import datetime + +from src.schemas.items import Item, ItemCreate, ItemUpdate + +class ItemCRUD: + def __init__(self, db: AsyncSession): + self.db = db + + async def create(self, item_create: ItemCreate) -> Item: + """Create new item""" + db_item = Item(**item_create.dict()) + + self.db.add(db_item) + await self.db.commit() + await self.db.refresh(db_item) + + return db_item + + async def get_by_id(self, item_id: int) -> Optional[Item]: + """Get item by ID""" + statement = select(Item).where(Item.id == item_id) + result = await self.db.execute(statement) + return result.scalar_one_or_none() + + async def get_many( + self, + skip: int = 0, + limit: int = 100, + active_only: bool = True + ) -> List[Item]: + """Get multiple items (pagination supported)""" + statement = select(Item) + + if active_only: + statement = statement.where(Item.is_active == True) + + statement = statement.offset(skip).limit(limit) + result = await self.db.execute(statement) + return result.scalars().all() + + async def update(self, item_id: int, item_update: ItemUpdate) -> Optional[Item]: + """Update item""" + # Prepare update data + update_data = item_update.dict(exclude_unset=True) + if update_data: + update_data["updated_at"] = datetime.utcnow() + + # Execute update + statement = ( + update(Item) + .where(Item.id == item_id) + .values(**update_data) + .returning(Item) + ) + + result = await self.db.execute(statement) + await self.db.commit() + + return result.scalar_one_or_none() + + async def delete(self, item_id: int) -> bool: + """Delete item (soft delete)""" + statement = ( + update(Item) + .where(Item.id == item_id) + .values(is_active=False, updated_at=datetime.utcnow()) + ) + + result = await self.db.execute(statement) + await self.db.commit() + + return result.rowcount > 0 + + async def hard_delete(self, item_id: int) -> bool: + """Delete item completely""" + statement = delete(Item).where(Item.id == item_id) + result = await self.db.execute(statement) + await self.db.commit() + + return result.rowcount > 0 + + async def search(self, query: str) -> List[Item]: + """Search item (name, description)""" + statement = select(Item).where( + (Item.name.ilike(f"%{query}%")) | + (Item.description.ilike(f"%{query}%")) + ).where(Item.is_active == True) + + result = await self.db.execute(statement) + return result.scalars().all() + + async def get_total_count(self, active_only: bool = True) -> int: + """Get total item count""" + from sqlalchemy import func + + statement = select(func.count(Item.id)) + if active_only: + statement = statement.where(Item.is_active == True) + + result = await self.db.execute(statement) + return result.scalar() +``` + +## Schritt 6: API-Endpunkte implementieren + +### Dependency-Injection einrichten (`src/api/deps.py`) + +```python +from typing import AsyncGenerator +from fastapi import Depends +from sqlalchemy.ext.asyncio import AsyncSession + +from src.core.db import get_session +from src.crud.items import ItemCRUD + +async def get_db() -> AsyncGenerator[AsyncSession, None]: + """Database session dependency""" + async for session in get_session(): + yield session + +def get_item_crud(db: AsyncSession = Depends(get_db)) -> ItemCRUD: + """Item CRUD dependency""" + return ItemCRUD(db) +``` + +### API-Router-Implementierung (`src/api/routes/items.py`) + +```python +from typing import List +from fastapi import APIRouter, Depends, HTTPException, Query, status + +from src.api.deps import get_item_crud +from src.crud.items import ItemCRUD +from src.schemas.items import Item, ItemCreate, ItemUpdate, ItemResponse + +router = APIRouter() + +@router.post("/", response_model=ItemResponse, status_code=status.HTTP_201_CREATED) +async def create_item( + item_create: ItemCreate, + crud: ItemCRUD = Depends(get_item_crud) +): + """Create new item""" + return await crud.create(item_create) + +@router.get("/", response_model=List[ItemResponse]) +async def read_items( + skip: int = Query(0, ge=0, description="Skip items"), + limit: int = Query(100, ge=1, le=1000, description="Maximum items to retrieve"), + active_only: bool = Query(True, description="Only active items"), + crud: ItemCRUD = Depends(get_item_crud) +): + """Get item list (pagination supported)""" + return await crud.get_many(skip=skip, limit=limit, active_only=active_only) + +@router.get("/search", response_model=List[ItemResponse]) +async def search_items( + q: str = Query(..., min_length=1, description="Search term"), + crud: ItemCRUD = Depends(get_item_crud) +): + """Search item""" + return await crud.search(q) + +@router.get("/count") +async def get_items_count( + active_only: bool = Query(True, description="Only active items"), + crud: ItemCRUD = Depends(get_item_crud) +): + """Get total item count""" + count = await crud.get_total_count(active_only) + return {"total": count} + +@router.get("/{item_id}", response_model=ItemResponse) +async def read_item( + item_id: int, + crud: ItemCRUD = Depends(get_item_crud) +): + """Get specific item""" + item = await crud.get_by_id(item_id) + if not item: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Item ID {item_id} not found" + ) + return item + +@router.put("/{item_id}", response_model=ItemResponse) +async def update_item( + item_id: int, + item_update: ItemUpdate, + crud: ItemCRUD = Depends(get_item_crud) +): + """Update item""" + updated_item = await crud.update(item_id, item_update) + if not updated_item: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Item ID {item_id} not found" + ) + return updated_item + +@router.delete("/{item_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_item( + item_id: int, + hard_delete: bool = Query(False, description="Complete delete"), + crud: ItemCRUD = Depends(get_item_crud) +): + """Delete item""" + if hard_delete: + deleted = await crud.hard_delete(item_id) + else: + deleted = await crud.delete(item_id) + + if not deleted: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Item ID {item_id} not found" + ) +``` + +## Schritt 7: Docker-Container starten + +### Docker-Compose-Konfiguration prüfen (`docker-compose.yml`) + +```yaml +version: '3.8' + +services: + db: + image: postgres:15 + restart: always + environment: + POSTGRES_DB: todoapp + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + + app: + build: . + restart: always + ports: + - "8000:8000" + environment: + POSTGRES_SERVER: db + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + POSTGRES_DB: todoapp + depends_on: + - db + volumes: + - ./src:/app/src + +volumes: + postgres_data: +``` + +### Container starten + +
+ +```console +$ cd todo-postgres-api + +# Start service in background +$ docker-compose up -d +Creating network "todo-postgres-api_default" with the default driver +Creating volume "todo-postgres-api_postgres_data" with default driver +Pulling db (postgres:15)... +Creating todo-postgres-api_db_1 ... done +Building app +Creating todo-postgres-api_app_1 ... done + +# Check service status +$ docker-compose ps + Name Command State Ports +------------------------------------------------------------------------------------- +todo-postgres-api_app_1 uvicorn src.main:app --host=0.0.0.0 --port=8000 Up 0.0.0.0:8000->8000/tcp +todo-postgres-api_db_1 docker-entrypoint.sh postgres Up 0.0.0.0:5432->5432/tcp + +# Check log +$ docker-compose logs app +``` + +
+ +## Schritt 8: Datenbankmigration + +### Erste Migration mit Alembic erstellen + +
+ +```console +# Run migration inside container +$ docker-compose exec app alembic revision --autogenerate -m "Create items table" +INFO [alembic.runtime.migration] Context impl PostgresqlImpl. +INFO [alembic.runtime.migration] Will assume transactional DDL. +INFO [alembic.autogenerate.compare] Detected added table 'items' +Generating migration script /app/src/alembic/versions/001_create_items_table.py ... done + +# Apply migration +$ docker-compose exec app alembic upgrade head +INFO [alembic.runtime.migration] Context impl PostgresqlImpl. +INFO [alembic.runtime.migration] Will assume transactional DDL. +INFO [alembic.runtime.migration] Running upgrade -> 001, Create items table +``` + +
+ +### Migrationsdatei prüfen + +Sehen Sie sich die erstellte Migrationsdatei an: + +```python +# src/alembic/versions/001_create_items_table.py +"""Create items table + +Revision ID: 001 +Revises: +Create Date: 2024-01-01 12:00:00.000000 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel + +# revision identifiers +revision = '001' +down_revision = None +branch_labels = None +depends_on = None + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('items', + sa.Column('name', sqlmodel.sql.sqltypes.AutoString(length=100), nullable=False), + sa.Column('description', sqlmodel.sql.sqltypes.AutoString(length=500), nullable=True), + sa.Column('price', sa.Float(), nullable=False), + sa.Column('tax', sa.Float(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_items_name'), 'items', ['name'], unique=False) + # ### end Alembic commands ### + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_items_name'), table_name='items') + op.drop_table('items') + # ### end Alembic commands ### +``` + +## Schritt 9: API testen + +### Grundlegender CRUD-Test + +
+ +```console +# Create new item +$ curl -X POST "http://localhost:8000/items/" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "MacBook Pro", + "description": "M2 chipset-equipped high-performance notebook", + "price": 2500000, + "tax": 250000 + }' + +{ + "id": 1, + "name": "MacBook Pro", + "description": "M2 chipset-equipped high-performance notebook", + "price": 2500000.0, + "tax": 250000.0, + "is_active": true, + "created_at": "2024-01-01T12:00:00.123456", + "updated_at": null +} + +# Get item list +$ curl "http://localhost:8000/items/" + +# Get item list with pagination +$ curl "http://localhost:8000/items/?skip=0&limit=10" + +# Search item +$ curl "http://localhost:8000/items/search?q=MacBook" + +# Get item count +$ curl "http://localhost:8000/items/count" +{"total": 1} +``` + +
+ +### Test fortgeschrittener Abfragefunktionen + +
+ +```console +# Get item list with inactive items +$ curl "http://localhost:8000/items/?active_only=false" + +# Update item +$ curl -X PUT "http://localhost:8000/items/1" \ + -H "Content-Type: application/json" \ + -d '{ + "price": 2300000, + "tax": 230000 + }' + +# Soft delete item +$ curl -X DELETE "http://localhost:8000/items/1" + +# Hard delete item +$ curl -X DELETE "http://localhost:8000/items/1?hard_delete=true" +``` + +
+ +## Schritt 10: Erweiterte Datenbankfunktionen + +### Transaktionsverarbeitung + +```python +# Add to src/crud/items.py + +from sqlalchemy.exc import SQLAlchemyError + +async def create_items_batch(self, items_create: List[ItemCreate]) -> List[Item]: + """Create multiple items in a transaction""" + created_items = [] + + try: + for item_create in items_create: + db_item = Item(**item_create.dict()) + self.db.add(db_item) + created_items.append(db_item) + + await self.db.commit() + + # Refresh all items + for item in created_items: + await self.db.refresh(item) + + return created_items + + except SQLAlchemyError: + await self.db.rollback() + raise +``` + +### Relationale Datenmodellierung + +```python +# Add to src/schemas/items.py + +from sqlmodel import Relationship + +class Category(SQLModel, table=True): + __tablename__ = "categories" + + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(max_length=50, unique=True) + description: Optional[str] = None + + # Set relationship + items: List["Item"] = Relationship(back_populates="category") + +class Item(ItemBase, table=True): + __tablename__ = "items" + + id: Optional[int] = Field(default=None, primary_key=True) + created_at: datetime = Field(default_factory=datetime.utcnow) + updated_at: Optional[datetime] = Field(default=None) + + # Add foreign key + category_id: Optional[int] = Field(foreign_key="categories.id") + + # Set relationship + category: Optional[Category] = Relationship(back_populates="items") +``` + +### Index-Optimierung + +```python +# Add to src/schemas/items.py + +from sqlalchemy import Index + +class Item(ItemBase, table=True): + __tablename__ = "items" + + # ... existing fields ... + + # Set composite index + __table_args__ = ( + Index('ix_items_price_active', 'price', 'is_active'), + Index('ix_items_created_at', 'created_at'), + Index('ix_items_name_description', 'name', 'description'), # For full text search + ) +``` + +## Schritt 11: Tests schreiben + +### Test-Setup für die Datenbank (`tests/conftest.py`) + +```python +import pytest +import asyncio +from httpx import AsyncClient +from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine +from sqlalchemy.orm import sessionmaker +from sqlmodel import SQLModel + +from src.main import app +from src.core.db import get_session +from src.core.config import settings + +# Test database engine +test_engine = create_async_engine( + settings.TEST_DATABASE_URL or "sqlite+aiosqlite:///./test.db", + echo=False, +) + +TestSessionLocal = sessionmaker( + autocommit=False, + autoflush=False, + bind=test_engine, + class_=AsyncSession, + expire_on_commit=False, +) + +@pytest.fixture(scope="session") +def event_loop(): + loop = asyncio.get_event_loop_policy().new_event_loop() + yield loop + loop.close() + +@pytest.fixture(scope="function") +async def db_session(): + # Create test table + async with test_engine.begin() as conn: + await conn.run_sync(SQLModel.metadata.create_all) + + # Provide session + async with TestSessionLocal() as session: + yield session + + # Delete table after test + async with test_engine.begin() as conn: + await conn.run_sync(SQLModel.metadata.drop_all) + +@pytest.fixture +async def client(db_session: AsyncSession): + # Override dependency + async def override_get_session(): + yield db_session + + app.dependency_overrides[get_session] = override_get_session + + async with AsyncClient(app=app, base_url="http://test") as client: + yield client + + app.dependency_overrides.clear() +``` + +### Integrationstests (`tests/test_items.py`) + +```python +import pytest +from httpx import AsyncClient + +@pytest.mark.asyncio +async def test_create_and_read_item(client: AsyncClient): + """Integration test for creating and reading item""" + # Create item + item_data = { + "name": "Test Item", + "description": "Database test", + "price": 50000, + "tax": 5000 + } + + response = await client.post("/items/", json=item_data) + assert response.status_code == 201 + + created_item = response.json() + assert created_item["name"] == item_data["name"] + assert "id" in created_item + assert "created_at" in created_item + + # Get created item + item_id = created_item["id"] + response = await client.get(f"/items/{item_id}") + assert response.status_code == 200 + + retrieved_item = response.json() + assert retrieved_item["id"] == item_id + assert retrieved_item["name"] == item_data["name"] + +@pytest.mark.asyncio +async def test_item_pagination(client: AsyncClient): + """Test pagination feature""" + # Create multiple items + for i in range(15): + item_data = { + "name": f"Item {i}", + "description": f"Description {i}", + "price": i * 1000, + "tax": i * 100 + } + await client.post("/items/", json=item_data) + + # Get first page + response = await client.get("/items/?skip=0&limit=10") + assert response.status_code == 200 + + items = response.json() + assert len(items) == 10 + + # Get second page + response = await client.get("/items/?skip=10&limit=10") + assert response.status_code == 200 + + items = response.json() + assert len(items) == 5 + +@pytest.mark.asyncio +async def test_item_search(client: AsyncClient): + """Test search feature""" + # Create test items + items = [ + {"name": "iPhone 15", "description": "Latest smartphone", "price": 1200000, "tax": 120000}, + {"name": "Galaxy S24", "description": "Samsung flagship", "price": 1100000, "tax": 110000}, + {"name": "MacBook Air", "description": "Apple notebook", "price": 1500000, "tax": 150000}, + ] + + for item in items: + await client.post("/items/", json=item) + + # Search "iPhone" + response = await client.get("/items/search?q=iPhone") + assert response.status_code == 200 + + results = response.json() + assert len(results) == 1 + assert results[0]["name"] == "iPhone 15" + + # Search "smartphone" (description) + response = await client.get("/items/search?q=smartphone") + assert response.status_code == 200 + + results = response.json() + assert len(results) == 1 + assert results[0]["description"] == "Latest smartphone" +``` + +### Tests ausführen + +
+ +```console +# Tests im Container ausführen +$ docker-compose exec app python -m pytest tests/ -v +======================== test session starts ======================== +collected 12 items + +tests/test_items.py::test_create_and_read_item PASSED [ 8%] +tests/test_items.py::test_item_pagination PASSED [16%] +tests/test_items.py::test_item_search PASSED [25%] +tests/test_items.py::test_update_item PASSED [33%] +tests/test_items.py::test_delete_item PASSED [41%] +tests/test_items.py::test_soft_delete PASSED [50%] +tests/test_items.py::test_item_not_found PASSED [58%] +tests/test_items.py::test_invalid_item_data PASSED [66%] +tests/test_items.py::test_database_transaction PASSED [75%] +tests/test_items.py::test_concurrent_operations PASSED [83%] +tests/test_items.py::test_item_count PASSED [91%] +tests/test_items.py::test_batch_operations PASSED [100%] + +======================== 12 passed in 2.34s ======================== +``` + +
+ +## Schritt 12: Überlegungen für ein Produktionsdeployment + +### Verbindungspool optimieren + +```python +# Add to src/core/config.py + +class Settings(BaseSettings): + # ... existing settings ... + + # Database connection pool settings + DB_POOL_SIZE: int = 20 + DB_MAX_OVERFLOW: int = 0 + DB_POOL_PRE_PING: bool = True + DB_POOL_RECYCLE: int = 300 # 5 minutes + + # Query timeout + DB_QUERY_TIMEOUT: int = 30 + + # Connection retry settings + DB_RETRY_ATTEMPTS: int = 3 + DB_RETRY_DELAY: int = 1 +``` + +### Datenbank-Monitoring + +```python +# Add to src/core/db.py + +import logging +from sqlalchemy import event +from sqlalchemy.engine import Engine + +logger = logging.getLogger(__name__) + +@event.listens_for(Engine, "before_cursor_execute") +def receive_before_cursor_execute(conn, cursor, statement, parameters, context, executemany): + """Log before query execution""" + context._query_start_time = time.time() + +@event.listens_for(Engine, "after_cursor_execute") +def receive_after_cursor_execute(conn, cursor, statement, parameters, context, executemany): + """Log after query execution""" + total = time.time() - context._query_start_time + if total > 1.0: # Log slow queries (1 second or more) + logger.warning(f"Slow query: {total:.2f}s - {statement[:100]}...") +``` + +## Nächste Schritte + +Sie haben die PostgreSQL-Datenbankintegration abgeschlossen! Nächste Schritte zum Ausprobieren: + +1. **[Docker-Containerisierung](docker-deployment.md)** — eine Produktionsumgebung aufbauen +2. **[Benutzerdefinierte Antwortbehandlung](custom-response-handling.md)** — erweiterte API-Antwortformate + + + + +## Zusammenfassung + +In diesem Tutorial haben wir mit PostgreSQL und SQLAlchemy: + +- ✅ Eine PostgreSQL-Datenbank integriert +- ✅ ORM mit SQLModel implementiert +- ✅ Das Alembic-Migrationssystem eingerichtet +- ✅ Fortgeschrittene CRUD-Operationen und Abfrageoptimierung umgesetzt +- ✅ Transaktionsverarbeitung und Datenintegrität behandelt +- ✅ Paginierung, Suche und Sortierung implementiert +- ✅ Integrationstests und Datenbank-Tests geschrieben +- ✅ Überlegungen zum Produktionsdeployment angestellt + +Jetzt können Sie robuste, datenbankgetriebene APIs für echte Produktionsumgebungen bauen! diff --git a/docs/de/tutorial/docker-deployment.md b/docs/de/tutorial/docker-deployment.md new file mode 100644 index 0000000..166cf4c --- /dev/null +++ b/docs/de/tutorial/docker-deployment.md @@ -0,0 +1,1178 @@ +# Docker-Containerisierung und Deployment + +Lernen Sie, wie Sie FastAPI-Anwendungen mit Docker containerisieren, um konsistente Entwicklungsumgebungen zu bauen und sich auf das Produktionsdeployment vorzubereiten. Wir richten eine vollständige Docker-basierte Deployment-Umgebung mit der Vorlage `fastapi-dockerized` ein. + +## Was Sie in diesem Tutorial lernen + +- FastAPI-Anwendungen mit Docker containerisieren +- Optimierte Docker-Images mit mehrstufigen Builds erstellen +- Entwicklungsumgebungen mit Docker Compose einrichten +- Docker-Konfiguration für das Produktionsdeployment +- Container-Monitoring und Log-Verwaltung +- CI/CD-Pipelines aufbauen + +## Voraussetzungen + +- Das [Tutorial Datenbankintegration](database-integration.md) abgeschlossen +- Docker und Docker Compose installiert +- Verständnis der grundlegenden Docker-Befehle +- Grundkenntnisse zu Container-Konzepten + +## Vorteile der Docker-Containerisierung + +### Klassischer Ansatz vs. Docker + +| Kategorie | Klassischer Ansatz | Docker-Ansatz | +|----------|---------------------|-----------------| +| **Umgebungskonsistenz** | Unterschiede zwischen Umgebungen | Überall gleiche Umgebung | +| **Abhängigkeitsverwaltung** | Manuelle Installation erforderlich | Alle Abhängigkeiten im Image | +| **Deployment-Geschwindigkeit** | Langsam | Schnelles Deployment möglich | +| **Skalierbarkeit** | Begrenzt | Einfache Skalierung | +| **Rollback** | Komplex | Sofortiger Rollback auf vorherige Version | +| **Ressourcennutzung** | Schwer | Leichtgewichtige Container | + +## Schritt 1: Ein Docker-basiertes Projekt erstellen + +Erstellen Sie ein Projekt mit der Vorlage `fastapi-dockerized`: + +
+ +```console +$ fastkit startdemo fastapi-dockerized +Enter the project name: dockerized-todo-api +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: Dockerized todo management API +Deploying FastAPI project using 'fastapi-dockerized' template + + Project Information +┌──────────────┬─────────────────────────────────────────────┐ +│ Project Name │ dockerized-todo-api │ +│ Author │ Developer Kim │ +│ Author Email │ developer@example.com │ +│ Description │ Dockerized todo management API │ +└──────────────┴─────────────────────────────────────────────┘ + + Template Dependencies +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ pydantic │ +│ Dependency 4 │ pydantic-settings │ +│ Dependency 5 │ python-dotenv │ +└──────────────┴───────────────────┘ + +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y + +✨ FastAPI project 'dockerized-todo-api' from 'fastapi-dockerized' has been created successfully! +``` + +
+ +## Schritt 2: Docker-Konfigurationsdateien analysieren + +Untersuchen wir die Docker-bezogenen Dateien im generierten Projekt: + +``` +dockerized-todo-api/ +├── Dockerfile # Docker image build configuration +├── docker-compose.yml # Development environment container setup +├── docker-compose.prod.yml # Production environment configuration +├── .dockerignore # Files to exclude during Docker build +├── scripts/ +│ ├── start.sh # Container startup script +│ ├── prestart.sh # Pre-start initialization script +│ └── gunicorn.conf.py # Gunicorn configuration +├── src/ +│ ├── main.py # FastAPI application +│ └── ... # Other source code +└── requirements.txt # Python dependencies +``` + +### Dockerfile-Analyse + +```dockerfile +# Optimized Dockerfile using multi-stage build + +# ============================================ +# Stage 1: Build stage +# ============================================ +FROM python:3.12-slim as builder + +# Install build tools +RUN apt-get update && apt-get install -y \ + build-essential \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Copy dependency file and install +COPY requirements.txt . +RUN pip install --user --no-cache-dir -r requirements.txt + +# ============================================ +# Stage 2: Runtime stage +# ============================================ +FROM python:3.12-slim + +# System update and essential package installation +RUN apt-get update && apt-get install -y \ + curl \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get clean + +# Create non-root user (security enhancement) +RUN groupadd -r appuser && useradd -r -g appuser appuser + +# Create application directory +WORKDIR /app + +# Copy Python packages from build stage +COPY --from=builder /root/.local /home/appuser/.local + +# Copy application code +COPY . . + +# Set file permissions +RUN chown -R appuser:appuser /app +RUN chmod +x scripts/start.sh scripts/prestart.sh + +# Add Python package path to PATH +ENV PATH=/home/appuser/.local/bin:$PATH + +# Switch to non-root user +USER appuser + +# Configure health check +HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8000/health || exit 1 + +# Expose port +EXPOSE 8000 + +# Execute startup script +CMD ["./scripts/start.sh"] +``` + +### Docker-Compose-Entwicklungsumgebung (`docker-compose.yml`) + +```yaml +version: '3.8' + +services: + app: + build: + context: . + dockerfile: Dockerfile + container_name: dockerized-todo-api + restart: unless-stopped + ports: + - "8000:8000" + environment: + - ENVIRONMENT=development + - DEBUG=true + - RELOAD=true + volumes: + # Mount volume for development (auto-reload on code changes) + - ./src:/app/src:ro + - ./scripts:/app/scripts:ro + networks: + - app-network + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # Redis (für Caching und Session-Speicher) + redis: + image: redis:7-alpine + container_name: dockerized-todo-redis + restart: unless-stopped + ports: + - "6379:6379" + volumes: + - redis_data:/data + networks: + - app-network + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 30s + timeout: 10s + retries: 3 + + # Nginx (reverse proxy) + nginx: + image: nginx:alpine + container_name: dockerized-todo-nginx + restart: unless-stopped + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./nginx/ssl:/etc/nginx/ssl:ro + depends_on: + - app + networks: + - app-network + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/health"] + interval: 30s + timeout: 10s + retries: 3 + +volumes: + redis_data: + +networks: + app-network: + driver: bridge +``` + +### Docker-Compose-Produktionsumgebung (`docker-compose.prod.yml`) + +```yaml +version: '3.8' + +services: + app: + build: + context: . + dockerfile: Dockerfile + restart: always + environment: + - ENVIRONMENT=production + - DEBUG=false + - WORKERS=4 + - MAX_WORKERS=8 + volumes: + - app_logs:/app/logs + networks: + - app-network + deploy: + replicas: 2 + resources: + limits: + cpus: '1.0' + memory: 1G + reservations: + cpus: '0.5' + memory: 512M + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + + redis: + image: redis:7-alpine + restart: always + command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD} + volumes: + - redis_data:/data + networks: + - app-network + deploy: + resources: + limits: + cpus: '0.5' + memory: 512M + + nginx: + image: nginx:alpine + restart: always + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/nginx.prod.conf:/etc/nginx/nginx.conf:ro + - ./nginx/ssl:/etc/nginx/ssl:ro + - nginx_logs:/var/log/nginx + depends_on: + - app + networks: + - app-network + deploy: + resources: + limits: + cpus: '0.5' + memory: 256M + +volumes: + redis_data: + app_logs: + nginx_logs: + +networks: + app-network: + driver: overlay + attachable: true +``` + +## Schritt 3: Startskripte konfigurieren + +### Haupt-Startskript (`scripts/start.sh`) + +```bash +#!/bin/bash + +set -e + +# Set environment variables +export PYTHONPATH=/app:$PYTHONPATH + +# Run pre-start script +echo "Running pre-start script..." +./scripts/prestart.sh + +# Determine execution mode based on environment +if [[ "$ENVIRONMENT" == "production" ]]; then + echo "Starting production server with Gunicorn..." + exec gunicorn src.main:app \ + --config scripts/gunicorn.conf.py \ + --bind 0.0.0.0:8000 \ + --workers ${WORKERS:-4} \ + --worker-class uvicorn.workers.UvicornWorker \ + --max-requests 1000 \ + --max-requests-jitter 100 \ + --preload \ + --access-logfile - \ + --error-logfile - +else + echo "Starting development server with Uvicorn..." + if [[ "$RELOAD" == "true" ]]; then + exec uvicorn src.main:app \ + --host 0.0.0.0 \ + --port 8000 \ + --reload \ + --reload-dir src \ + --log-level debug + else + exec uvicorn src.main:app \ + --host 0.0.0.0 \ + --port 8000 \ + --log-level info + fi +fi +``` + +### Pre-Start-Skript (`scripts/prestart.sh`) + +```bash +#!/bin/bash + +set -e + +echo "Running pre-start checks..." + +# Check Python modules and dependencies +echo "Checking Python dependencies..." +python -c "import fastapi, uvicorn, pydantic; print('✓ Core dependencies OK')" + +# Check environment variables +if [[ -z "$ENVIRONMENT" ]]; then + export ENVIRONMENT="development" + echo "ℹ ENVIRONMENT not set, defaulting to development" +fi + +# Create log directory +mkdir -p /app/logs +touch /app/logs/app.log + +# Check if health endpoint is present +echo "Checking health endpoint..." +python -c " +from src.main import app +routes = [route.path for route in app.routes] +if '/health' not in routes: + print('⚠ Warning: /health endpoint not found') +else: + print('✓ Health endpoint OK') +" + +echo "Pre-start checks completed successfully!" +``` + +### Gunicorn-Konfiguration (`scripts/gunicorn.conf.py`) + +```python +import multiprocessing +import os + +# Server socket +bind = "0.0.0.0:8000" +backlog = 2048 + +# Worker process +workers = int(os.getenv("WORKERS", multiprocessing.cpu_count() * 2 + 1)) +worker_class = "uvicorn.workers.UvicornWorker" +worker_connections = 1000 +max_requests = 1000 +max_requests_jitter = 100 + +# Worker restart settings +preload_app = True +timeout = 120 +keepalive = 2 + +# Logging +accesslog = "-" +errorlog = "-" +loglevel = "info" +access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s' + +# Process name +proc_name = "dockerized-todo-api" + +# Security +limit_request_line = 4094 +limit_request_fields = 100 +limit_request_field_size = 8190 + +# Performance tuning +def when_ready(server): + server.log.info("Server is ready. Spawning workers") + +def worker_int(worker): + worker.log.info("worker received INT or QUIT signal") + +def pre_fork(server, worker): + server.log.info("Worker spawned (pid: %s)", worker.pid) + +def post_fork(server, worker): + server.log.info("Worker spawned (pid: %s)", worker.pid) + +def worker_abort(worker): + worker.log.info("worker received SIGABRT signal") +``` + +## Schritt 4: Health-Check und Monitoring implementieren + +### Health-Check-Endpunkt hinzufügen (`src/main.py`) + +```python +from fastapi import FastAPI, status, Depends +from fastapi.responses import JSONResponse +import psutil +import time +from datetime import datetime + +app = FastAPI( + title="Dockerized Todo API", + description="Dockerized todo management API", + version="1.0.0" +) + +# Application start time +start_time = time.time() + +@app.get("/health", status_code=status.HTTP_200_OK) +async def health_check(): + """ + Container health check endpoint + """ + current_time = time.time() + uptime = current_time - start_time + + # System resource information + memory_info = psutil.virtual_memory() + cpu_percent = psutil.cpu_percent(interval=1) + + health_data = { + "status": "healthy", + "timestamp": datetime.utcnow().isoformat(), + "uptime_seconds": round(uptime, 2), + "version": app.version, + "system": { + "memory_usage_percent": memory_info.percent, + "memory_available_mb": round(memory_info.available / 1024 / 1024, 2), + "cpu_usage_percent": cpu_percent, + }, + "checks": { + "database": await check_database_connection(), + "redis": await check_redis_connection(), + "disk_space": check_disk_space(), + } + } + + # Check if all checks passed + all_checks_passed = all(health_data["checks"].values()) + + if not all_checks_passed: + return JSONResponse( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + content=health_data + ) + + return health_data + +async def check_database_connection() -> bool: + """Check database connection status""" + try: + # In der echten Implementierung sollte hier die Datenbankverbindung geprüft werden + return True + except Exception: + return False + +async def check_redis_connection() -> bool: + """Status der Redis-Verbindung prüfen""" + try: + # In einer echten Implementierung würde hier die Redis-Verbindung geprüft + return True + except Exception: + return False + +def check_disk_space() -> bool: + """Check disk space""" + disk_usage = psutil.disk_usage('/') + free_percentage = (disk_usage.free / disk_usage.total) * 100 + return free_percentage > 10 # 10% or more free space needed + +@app.get("/health/ready", status_code=status.HTTP_200_OK) +async def readiness_check(): + """ + Kubernetes readiness probe endpoint + """ + # Check if application is ready to receive traffic + return {"status": "ready", "timestamp": datetime.utcnow().isoformat()} + +@app.get("/health/live", status_code=status.HTTP_200_OK) +async def liveness_check(): + """ + Kubernetes liveness probe endpoint + """ + return {"status": "alive", "timestamp": datetime.utcnow().isoformat()} +``` + +## Schritt 5: Nginx-Reverse-Proxy konfigurieren + +### Nginx-Konfiguration für die Entwicklung (`nginx/nginx.conf`) + +```nginx +events { + worker_connections 1024; +} + +http { + upstream fastapi_backend { + # Specify backend by container name + server app:8000; + } + + # Define log format + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for" ' + 'rt=$request_time uct="$upstream_connect_time" ' + 'uht="$upstream_header_time" urt="$upstream_response_time"'; + + access_log /var/log/nginx/access.log main; + error_log /var/log/nginx/error.log warn; + + # Default settings + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + client_max_body_size 100M; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript + application/json application/javascript application/xml+rss + application/atom+xml image/svg+xml; + + server { + listen 80; + server_name localhost; + + # Security headers + add_header X-Content-Type-Options nosniff; + add_header X-Frame-Options DENY; + add_header X-XSS-Protection "1; mode=block"; + + # Health check endpoint + location /health { + proxy_pass http://fastapi_backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Health check should respond quickly + proxy_connect_timeout 5s; + proxy_send_timeout 5s; + proxy_read_timeout 5s; + } + + # API endpoint + location / { + proxy_pass http://fastapi_backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Timeout settings + proxy_connect_timeout 30s; + proxy_send_timeout 30s; + proxy_read_timeout 30s; + + # Buffering settings + proxy_buffering on; + proxy_buffer_size 4k; + proxy_buffers 8 4k; + } + + # Static file caching (future use) + location /static { + expires 1y; + add_header Cache-Control public; + add_header ETag ""; + } + } +} +``` + +### Nginx-Konfiguration für die Produktion (`nginx/nginx.prod.conf`) + +```nginx +events { + worker_connections 2048; +} + +http { + upstream fastapi_backend { + # Load balancing for multiple app instances + server app:8000 max_fails=3 fail_timeout=30s; + # server app2:8000 max_fails=3 fail_timeout=30s; # For scaling + + # Keep-alive + keepalive 32; + } + + # Security settings + server_tokens off; + + # Rate limiting + limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; + limit_req_zone $binary_remote_addr zone=health:10m rate=100r/s; + + # SSL settings + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384; + ssl_prefer_server_ciphers off; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + server { + listen 80; + server_name your-domain.com; + return 301 https://$server_name$request_uri; + } + + server { + listen 443 ssl http2; + server_name your-domain.com; + + ssl_certificate /etc/nginx/ssl/cert.pem; + ssl_certificate_key /etc/nginx/ssl/key.pem; + + # Security headers + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Content-Type-Options nosniff always; + add_header X-Frame-Options DENY always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # Health check (rate limit applied) + location /health { + limit_req zone=health burst=20 nodelay; + proxy_pass http://fastapi_backend; + include /etc/nginx/proxy_params; + } + + # API endpoint (rate limit applied) + location / { + limit_req zone=api burst=20 nodelay; + proxy_pass http://fastapi_backend; + include /etc/nginx/proxy_params; + } + } +} +``` + +## Schritt 6: Container bauen und starten + +### In der Entwicklungsumgebung ausführen + +
+ +```console +$ cd dockerized-todo-api + +# Build Docker image +$ docker-compose build +Building app +Step 1/15 : FROM python:3.12-slim as builder + ---> abc123def456 +Step 2/15 : RUN apt-get update && apt-get install -y build-essential curl + ---> Running in xyz789abc123 +... +Successfully built def456ghi789 +Successfully tagged dockerized-todo-api_app:latest + +# Run container (background) +$ docker-compose up -d +Creating network "dockerized-todo-api_app-network" with driver "bridge" +Creating volume "dockerized-todo-api_redis_data" with default driver +Creating dockerized-todo-redis ... done +Creating dockerized-todo-api ... done +Creating dockerized-todo-nginx ... done + +# Check container status +$ docker-compose ps + Name Command State Ports +------------------------------------------------------------------------------------------------ +dockerized-todo-api ./scripts/start.sh Up (healthy) 8000/tcp +dockerized-todo-nginx /docker-entrypoint.sh ngin ... Up 0.0.0.0:80->80/tcp, :::80->80/tcp +dockerized-todo-redis docker-entrypoint.sh redis ... Up (healthy) 0.0.0.0:6379->6379/tcp, :::6379->6379/tcp +``` + +
+ +### Logs prüfen + +
+ +```console +# Check all service logs +$ docker-compose logs + +# Check specific service logs +$ docker-compose logs app +$ docker-compose logs nginx +$ docker-compose logs redis + +# Check real-time logs +$ docker-compose logs -f app +``` + +
+ +### Health-Check-Test + +
+ +```console +# Basic health check +$ curl http://localhost/health +{ + "status": "healthy", + "timestamp": "2024-01-01T12:00:00.123456", + "uptime_seconds": 45.67, + "version": "1.0.0", + "system": { + "memory_usage_percent": 25.3, + "memory_available_mb": 3072.45, + "cpu_usage_percent": 5.2 + }, + "checks": { + "database": true, + "redis": true, + "disk_space": true + } +} + +# Kubernetes probe test +$ curl http://localhost/health/ready +$ curl http://localhost/health/live +``` + +
+ +## Schritt 7: Produktionsdeployment + +### Umgebungsvariablen setzen (`.env.prod`) + +```bash +# Application settings +ENVIRONMENT=production +DEBUG=false +SECRET_KEY=your-super-secret-key-here +WORKERS=4 + +# Database settings +DATABASE_URL=postgresql://user:password@db:5432/todoapp +REDIS_URL=redis://:password@redis:6379/0 +REDIS_PASSWORD=your-redis-password + +# Logging settings +LOG_LEVEL=info +LOG_FILE=/app/logs/app.log + +# Security settings +ALLOWED_HOSTS=["your-domain.com"] +CORS_ORIGINS=["https://your-frontend.com"] + +# Monitoring +SENTRY_DSN=https://your-sentry-dsn@sentry.io/project-id +``` + +### Befehle für das Produktionsdeployment + +
+ +```console +# Deploy in production environment +$ docker-compose -f docker-compose.prod.yml --env-file .env.prod up -d + +# Scaling (app instance scaling) +$ docker-compose -f docker-compose.prod.yml up -d --scale app=3 + +# Rolling update +$ docker-compose -f docker-compose.prod.yml build app +$ docker-compose -f docker-compose.prod.yml up -d --no-deps app + +# Safe shutdown before backup +$ docker-compose -f docker-compose.prod.yml down --timeout 30 +``` + +
+ +## Schritt 8: Monitoring und Logging + +### Ressourcen-Monitoring von Docker-Containern + +
+ +```console +# Check real-time resource usage +$ docker stats + +CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS +abc123def456 dockerized-todo-api 2.34% 128.5MiB / 1GiB 12.55% 1.23MB / 456kB 12.3MB / 4.56MB 15 +def456ghi789 dockerized-todo-nginx 0.12% 12.5MiB / 256MiB 4.88% 456kB / 1.23MB 1.23MB / 456kB 3 +ghi789jkl012 dockerized-todo-redis 1.45% 32.1MiB / 512MiB 6.27% 789kB / 2.34MB 4.56MB / 1.23MB 4 + +# Check specific container details +$ docker inspect dockerized-todo-api + +# Check container internal processes +$ docker-compose exec app ps aux +``` + +
+ +### Log-Aggregation und -Analyse + +```yaml +# docker-compose.logging.yml +version: '3.8' + +services: + # ELK Stack for log aggregation + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:8.6.0 + environment: + - discovery.type=single-node + - xpack.security.enabled=false + volumes: + - elasticsearch_data:/usr/share/elasticsearch/data + networks: + - logging + + logstash: + image: docker.elastic.co/logstash/logstash:8.6.0 + volumes: + - ./logstash/pipeline:/usr/share/logstash/pipeline:ro + - ./logstash/config:/usr/share/logstash/config:ro + networks: + - logging + depends_on: + - elasticsearch + + kibana: + image: docker.elastic.co/kibana/kibana:8.6.0 + ports: + - "5601:5601" + environment: + - ELASTICSEARCH_HOSTS=http://elasticsearch:9200 + networks: + - logging + depends_on: + - elasticsearch + + # Fluentd for log collection + fluentd: + image: fluent/fluentd:v1.16-debian-1 + volumes: + - ./fluentd/conf:/fluentd/etc:ro + - /var/log:/var/log:ro + networks: + - logging + depends_on: + - elasticsearch + +volumes: + elasticsearch_data: + +networks: + logging: + driver: bridge +``` + +### Prometheus-Metriken sammeln + +```python +# src/monitoring.py +from prometheus_client import Counter, Histogram, Gauge, generate_latest +from fastapi import Request, Response +import time + +# Define metrics +REQUEST_COUNT = Counter( + 'http_requests_total', + 'Total HTTP requests', + ['method', 'endpoint', 'status_code'] +) + +REQUEST_DURATION = Histogram( + 'http_request_duration_seconds', + 'HTTP request duration in seconds', + ['method', 'endpoint'] +) + +ACTIVE_CONNECTIONS = Gauge( + 'active_connections', + 'Number of active connections' +) + +async def metrics_middleware(request: Request, call_next): + """Prometheus metric collection middleware""" + start_time = time.time() + method = request.method + endpoint = request.url.path + + ACTIVE_CONNECTIONS.inc() + + try: + response = await call_next(request) + status_code = response.status_code + except Exception as e: + status_code = 500 + raise + finally: + duration = time.time() - start_time + REQUEST_DURATION.labels(method=method, endpoint=endpoint).observe(duration) + REQUEST_COUNT.labels(method=method, endpoint=endpoint, status_code=status_code).inc() + ACTIVE_CONNECTIONS.dec() + + return response + +@app.get("/metrics") +async def get_metrics(): + """Prometheus metric endpoint""" + return Response(generate_latest(), media_type="text/plain") +``` + +## Schritt 9: CI/CD-Pipeline aufbauen + +### GitHub-Actions-Workflow (`.github/workflows/deploy.yml`) + +```yaml +name: Deploy to Production + +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pytest pytest-asyncio httpx + + - name: Run tests + run: | + pytest tests/ -v --cov=src --cov-report=xml + + - name: Upload coverage reports + uses: codecov/codecov-action@v3 + with: + file: ./coverage.xml + + build: + needs: test + runs-on: ubuntu-latest + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + + steps: + - uses: actions/checkout@v4 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=sha + type=raw,value=latest + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + deploy: + needs: build + runs-on: ubuntu-latest + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + + steps: + - uses: actions/checkout@v4 + + - name: Deploy to production + uses: appleboy/ssh-action@v1.0.0 + with: + host: ${{ secrets.PROD_HOST }} + username: ${{ secrets.PROD_USERNAME }} + key: ${{ secrets.PROD_SSH_KEY }} + script: | + cd /opt/dockerized-todo-api + + # Pull new image + docker-compose -f docker-compose.prod.yml pull + + # Rolling update + docker-compose -f docker-compose.prod.yml up -d --no-deps app + + # Health check + sleep 30 + curl -f http://localhost/health || exit 1 + + # Clean up previous image + docker image prune -f +``` + +## Schritt 10: Sicherheit verbessern + +### Sicherheitsoptionen für Container + +```dockerfile +# Add security enhancement to Dockerfile + +# Run as non-root user +USER appuser + +# Read-only root filesystem +# docker run --read-only --tmpfs /tmp dockerized-todo-api + +# Limit permissions +# docker run --cap-drop=ALL dockerized-todo-api + +# Network isolation +# docker run --network=none dockerized-todo-api +``` + +### Sicherheitsoptionen für Docker Compose + +```yaml +# Add security settings to docker-compose.yml +services: + app: + # ... existing settings ... + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + cap_add: + - NET_BIND_SERVICE + read_only: true + tmpfs: + - /tmp + - /app/logs + user: "1000:1000" +``` + +### Secrets-Verwaltung + +```yaml +# Add secrets settings to docker-compose.yml +version: '3.8' + +services: + app: + secrets: + - db_password + - api_key + environment: + - DB_PASSWORD_FILE=/run/secrets/db_password + - API_KEY_FILE=/run/secrets/api_key + +secrets: + db_password: + file: ./secrets/db_password.txt + api_key: + external: true +``` + +## Nächste Schritte + +Sie haben die Docker-Containerisierung abgeschlossen! Nächste Schritte zum Ausprobieren: + +1. **[Benutzerdefinierte Antwortbehandlung](custom-response-handling.md)** — fortgeschrittene API-Antwortformate implementieren + + + + + +## Zusammenfassung + +In diesem Tutorial haben wir mit Docker: + +- ✅ Optimierte Container-Images mit mehrstufigen Builds erstellt +- ✅ Entwicklungs-/Produktionsumgebungen mit Docker Compose eingerichtet +- ✅ Nginx-Reverse-Proxy und Lastverteilung konfiguriert +- ✅ Health-Check- und Monitoring-Systeme aufgebaut +- ✅ Automatisiertes Deployment über CI/CD-Pipelines implementiert +- ✅ Produktionsreife Sicherheitsoptionen konfiguriert +- ✅ Systeme für Logging und Metrik-Sammlung implementiert + +Jetzt können Sie FastAPI-Anwendungen sicher und effizient in Produktionsumgebungen deployen! diff --git a/docs/de/tutorial/domain-starter.md b/docs/de/tutorial/domain-starter.md new file mode 100644 index 0000000..c2ef074 --- /dev/null +++ b/docs/de/tutorial/domain-starter.md @@ -0,0 +1,393 @@ +# Domänenorientiertes FastAPI mit `fastapi-domain-starter` + +Bauen Sie einen mittelgroßen FastAPI-Dienst mit dem empfohlenen modernen Layout — **ein Ordner pro Geschäftskonzept** unter `src/app/domains/`. Dieses Tutorial führt Sie Schritt für Schritt durch die Vorlage `fastapi-domain-starter`: wie Sie sie generieren, was jedes Top-Level-Paket übernimmt, wie das mitgelieferte `items`-Beispiel eingebunden ist und wie Sie Ihre nächste Domäne hinzufügen. + +## Was Sie lernen werden + +- Ein Projekt mit `fastkit startdemo fastapi-domain-starter` generieren +- Die Rolle von `core`, `db`, `domains` und `tests` im Layout +- Wie eine Domäne in router → service → repository → schemas → models aufgeteilt wird +- Den Vertrag zum Hinzufügen einer neuen Domäne (items-Ordner kopieren, Router registrieren) +- Wie der mitgelieferte `/health`-Endpunkt und das `/api/v1/items`-CRUD in die App eingebunden werden + +## Voraussetzungen + +- Python 3.12+ +- FastAPI-fastkit installiert (`pip install fastapi-fastkit`) +- Vertrautheit mit grundlegenden FastAPI-Konzepten (Path-Operations, Pydantic-Schemas, Dependencies) + +Wenn dies Ihr erstes FastAPI-Projekt ist, starten Sie stattdessen mit [Einen einfachen API-Server bauen](basic-api-server.md) — dieses Tutorial verwendet die einfachere Vorlage `fastapi-default`. + +## Schritt 1: Das Projekt generieren + +```console +$ fastkit startdemo fastapi-domain-starter +Enter the project name: orders-api +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: Domain-oriented orders service +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y +``` + +`fastkit` legt die Vorlage ab, füllt Platzhalter, erstellt eine virtuelle Umgebung und installiert Abhängigkeiten. Nach Abschluss: + +```console +$ cd orders-api +$ bash scripts/run-server.sh # or: uvicorn src.app.main:app --reload +``` + +Die API-Dokumentation wird dann unter bereitgestellt. + +## Schritt 2: Der generierte Baum + +``` +orders-api/ +├── README.md +├── pyproject.toml # PEP 621 metadata + [tool.fastapi-fastkit] +├── requirements.txt # pinned deps (template ships both files; you maintain them as you add packages) +├── .env # SECRET_KEY, ENVIRONMENT +├── .gitignore +├── scripts/ +│ ├── format.sh # black + isort +│ ├── lint.sh # black --check + isort --check + mypy +│ ├── run-server.sh # uvicorn src.app.main:app --reload +│ └── test.sh # pytest +├── src/ +│ ├── __init__.py +│ └── app/ # the application package +│ ├── __init__.py +│ ├── main.py # FastAPI() + middleware + api_router include +│ ├── core/ # cross-cutting configuration +│ │ ├── __init__.py +│ │ └── config.py # pydantic-settings (PROJECT_NAME, CORS, ...) +│ ├── db/ # persistence abstractions +│ │ ├── __init__.py +│ │ └── memory.py # InMemoryStore[T] generic key-value store +│ ├── api/ # transport-level routing +│ │ ├── __init__.py +│ │ ├── health.py # GET /health +│ │ └── router.py # aggregates health + every domain router +│ └── domains/ # business concepts (one folder each) +│ ├── __init__.py +│ └── items/ # the example domain +│ ├── __init__.py +│ ├── models.py # @dataclass Item (entity) +│ ├── schemas.py # ItemCreate, ItemRead (pydantic) +│ ├── repository.py # ItemRepository over InMemoryStore +│ ├── service.py # ItemService + ItemNotFoundError +│ └── router.py # APIRouter(prefix="/items") +└── tests/ + ├── __init__.py + ├── conftest.py # TestClient fixture, store reset + ├── test_health.py + └── test_items.py +``` + +Zwei zentrale Ideen zum Verinnerlichen: + +1. **`src/app/`** ist das **Anwendungspaket** — alles, was die Laufzeit importiert, liegt hier. Tests importieren daraus (`from src.app.main import app`). Das äußere `src/` existiert, damit das Projekt per `pip install` installierbar ist. +2. **`src/app/domains//`** ist die **Scheibe pro Konzept** — jedes Geschäftskonzept (items, orders, users, …) besitzt seinen eigenen Router / Service / Repository / Schemas / Models und nur diese. + +## Schritt 3: Was jedes Top-Level-Paket tut + +### `src/app/core/` — Konfiguration + +Enthält die übergreifende Anwendungskonfiguration. Die mitgelieferte `config.py` stellt eine pydantic-settings-Klasse `Settings` bereit, die aus `.env` / Umgebungsvariablen gelesen wird: + +```python +class Settings(BaseSettings): + PROJECT_NAME: str = "" + ENVIRONMENT: Literal["development", "staging", "production"] = "development" + SECRET_KEY: str = secrets.token_urlsafe(32) + API_V1_PREFIX: str = "/api/v1" + BACKEND_CORS_ORIGINS: ... = [] + ... + +settings = Settings() +``` + +`main.py` liest `settings.PROJECT_NAME`, `settings.API_V1_PREFIX` und `settings.all_cors_origins`, um die FastAPI-App zu verdrahten. + +**Wann etwas in `core/` ergänzen:** alles, was nicht spezifisch für eine einzelne Domäne ist — globale Einstellungen, strukturiertes Logging, eigene Middleware, Sicherheits-Helfer usw. + +### `src/app/db/` — Persistenzgrenze + +Enthält die Abstraktion über Ihren Datenspeicher. Der Starter liefert `memory.py` — einen prozesslokalen `InMemoryStore[T]`, generisch über den Entity-Typ. Das Repository jeder Domäne wickelt einen `InMemoryStore` ein, sodass ein späterer Wechsel zu SQLAlchemy / asynchronen Treibern eine eingegrenzte Änderung ist: nur die Repositories müssen neu geschrieben werden. + +```python +class InMemoryStore(Generic[T]): + def list(self) -> Iterable[T]: ... + def get(self, id_: int) -> Optional[T]: ... + def add(self, item: T) -> int: ... + def replace(self, id_: int, item: T) -> bool: ... + def delete(self, id_: int) -> bool: ... + def clear(self) -> None: ... +``` + +**Wann `db/` ausbauen:** Ergänzen Sie eine `session.py` mit Ihrer echten Datenbank-Session-Factory, sobald Sie weg von `InMemoryStore` migrieren. Behalten Sie die öffentliche Methodenform (`list` / `get` / `add` / …) bei, damit die Domain-Repositories ihren internen Vertrag nicht ändern müssen. + +### `src/app/api/` — Transport-Routing + +Zwei Teile: + +- `health.py` — ein kleiner `APIRouter`, der `GET /health` mit `{"status": "ok"}` ausliefert. Nebenwirkungsfrei, ideal für Liveness-Probes. +- `router.py` — der **Top-Level-Aggregator**. Er bindet den Health-Router und den Router jeder Domäne ein, und dieser eine kombinierte `api_router` wird unter `/api/v1` auf der FastAPI-App montiert: + +```python +# src/app/api/router.py +api_router = APIRouter() +api_router.include_router(health.router) +api_router.include_router(items_router.router) +``` + +```python +# src/app/main.py +app.include_router(api_router, prefix=settings.API_V1_PREFIX) +``` + +**Warum hier aggregieren:** Wenn Sie eine neue Domäne hinzufügen, bearbeiten Sie nur `src/app/api/router.py`, um deren Router zu registrieren. `main.py` ändert sich nie. + +### `src/app/domains//` — Geschäfts-Scheiben + +Hier lebt der Großteil Ihres Codes, wenn das Projekt wächst. Jede Domäne besitzt fünf Dateien: + +| Datei | Rolle | +|---|---| +| `models.py` | Domain-Entity (im Starter ein `@dataclass`; später ggf. SQLAlchemy / SQLModel). Die interne Form — nicht das Wire-Format. | +| `schemas.py` | API-Ein-/Ausgabeschemas (pydantic). Vom Entity getrennt, damit sich das Wire-Format ändern lässt, ohne die Domain-Logik anzufassen. | +| `repository.py` | Datenzugriff. Wickelt den Store mit item-typisierten Methoden ein. Die Naht, an der Persistenz ausgetauscht wird. | +| `service.py` | Geschäftslogik. Router rufen den `service` auf, niemals direkt das `repository`. Domain-spezifische Exceptions (z. B. `ItemNotFoundError`) leben hier. | +| `router.py` | HTTP-Transport. Übersetzt pydantic-Schemas ↔ Service-Aufrufe; wandelt Domain-Exceptions in `HTTPException`s. | + +Die **Abhängigkeitsrichtung** ist `router → service → repository → store`. Jede Schicht hängt nur von der darunterliegenden ab. Schemas werden von Router und Service referenziert; Models von Repository und Service. + +### `tests/` + +Spiegelt das Laufzeit-Layout — pro Oberfläche, deren Verhalten festgepinnt werden soll, ein Testmodul. Der Starter liefert: + +- `conftest.py` — autouse-Fixture, die den Items-Store zwischen Tests zurücksetzt, plus eine `client`-Fixture, die `TestClient(app)` umhüllt. +- `test_health.py` — prüft, dass `GET /api/v1/health` 200 + `{"status": "ok"}` liefert. +- `test_items.py` — vollständige CRUD-Abdeckung der Items-Endpunkte, inklusive eines 404 für unbekannte IDs und eines 422 für ungültige Payloads. + +Ausführen mit: + +```console +$ bash scripts/test.sh # or: pytest +``` + +## Schritt 4: Die mitgelieferte `items`-Domäne durchgehen + +Die Beispieldomäne ist ein CRUD über einer kleinen Entity: + +```python +# src/app/domains/items/models.py +@dataclass +class Item: + id: int + name: str + price: float + in_stock: bool = True +``` + +Die API-Schemas trennen die Eingabeform von der Ausgabeform, sodass wir serverseitig kontrollierte Felder (`id`) und Validierung (`price` ≥ 0) hinzufügen können: + +```python +# src/app/domains/items/schemas.py +class ItemCreate(BaseModel): + name: str = Field(min_length=1, max_length=120) + price: float = Field(ge=0) + in_stock: bool = True + +class ItemRead(BaseModel): + id: int + name: str + price: float + in_stock: bool + model_config = ConfigDict(from_attributes=True) +``` + +Das Repository umschließt den In-Memory-Store und vergibt IDs beim Einfügen: + +```python +# src/app/domains/items/repository.py +class ItemRepository: + def __init__(self, store: Optional[InMemoryStore[Item]] = None) -> None: + self._store = store if store is not None else _store + + def add(self, name: str, price: float, in_stock: bool = True) -> Item: + item = Item(id=0, name=name, price=price, in_stock=in_stock) + new_id = self._store.add(item) + item.id = new_id + return item + # list_all / get / replace / delete / reset elided +``` + +In der Service-Schicht sammeln sich Geschäftsregeln. Heute ist es ein dünner Durchreicher mit einer benutzerdefinierten Exception, aber hier wird zukünftig Policy leben („darf ein Item nicht löschen, das in einer offenen Bestellung ist" usw.): + +```python +# src/app/domains/items/service.py +class ItemNotFoundError(Exception): ... + +class ItemService: + def __init__(self, repository: Optional[ItemRepository] = None) -> None: + self._repository = repository if repository is not None else ItemRepository() + + def get_item(self, item_id: int) -> Item: + item = self._repository.get(item_id) + if item is None: + raise ItemNotFoundError(f"Item {item_id} does not exist") + return item + # list_items / create_item / replace_item / delete_item elided +``` + +Der Router ist das einzige Stück, das HTTP kennt. Beachten Sie, dass er den Service als FastAPI-`Depends(...)` entgegennimmt, sodass Tests ihn überschreiben können, und er bildet `ItemNotFoundError` → `HTTPException(404)` ab: + +```python +# src/app/domains/items/router.py +router = APIRouter(prefix="/items", tags=["items"]) + +def get_item_service() -> ItemService: + return ItemService() + +@router.get("/{item_id}", response_model=ItemRead) +def get_item(item_id: int, service: ItemService = Depends(get_item_service)) -> ItemRead: + try: + return ItemRead.model_validate(service.get_item(item_id)) + except ItemNotFoundError as exc: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) +``` + +Der vollständige Router stellt bereit: + +| Methode | Pfad | Wirkung | +|---|---|---| +| `GET` | `/api/v1/items` | Items auflisten | +| `GET` | `/api/v1/items/{item_id}` | Ein Item lesen | +| `POST` | `/api/v1/items` | Anlegen (gibt 201 zurück) | +| `PUT` | `/api/v1/items/{item_id}` | Ersetzen | +| `DELETE` | `/api/v1/items/{item_id}` | Löschen (gibt 204 zurück) | +| `GET` | `/api/v1/health` | Liveness-Probe | + +Probieren Sie es: + +```console +$ curl -X POST http://127.0.0.1:8000/api/v1/items \ + -H 'Content-Type: application/json' \ + -d '{"name":"Mug","price":9.5,"in_stock":true}' +{"id":1,"name":"Mug","price":9.5,"in_stock":true} + +$ curl http://127.0.0.1:8000/api/v1/items +[{"id":1,"name":"Mug","price":9.5,"in_stock":true}] + +$ curl http://127.0.0.1:8000/api/v1/items/999 +{"detail":"Item 999 does not exist"} +``` + +## Schritt 5: Ihre nächste Domäne hinzufügen + +Der Starter ist so gestaltet, dass das **Hinzufügen einer Domäne eine Kopier-und-Umbenenn-Operation** ist. Angenommen, Sie möchten eine `users`-Domäne neben `items`: + +### 1. Den `items/`-Ordner kopieren + +```console +$ cp -r src/app/domains/items src/app/domains/users +``` + +### 2. Entity, Schemas und Klassennamen pro Datei umschreiben + +```python +# src/app/domains/users/models.py +from dataclasses import dataclass + +@dataclass +class User: + id: int + email: str + is_active: bool = True +``` + +```python +# src/app/domains/users/schemas.py +from pydantic import BaseModel, ConfigDict, Field + +class UserCreate(BaseModel): + # Ein einfaches ``str`` sorgt dafür, dass das Beispiel direkt funktioniert. + # Wenn Sie stattdessen Pydantics eingebaute E-Mail-Validierung nutzen möchten, + # installieren Sie die optionale Abhängigkeit + # (``pip install 'pydantic[email]'`` — dadurch wird ``email-validator`` mitinstalliert) + # und ersetzen ``str`` durch ``EmailStr``. + email: str = Field(min_length=3, max_length=320) + is_active: bool = True + +class UserRead(BaseModel): + id: int + email: str + is_active: bool + model_config = ConfigDict(from_attributes=True) +``` + +Benennen Sie `Item → User`, `ItemNotFoundError → UserNotFoundError`, `ItemRepository → UserRepository`, `ItemService → UserService` in `models.py`, `schemas.py`, `repository.py`, `service.py` und `router.py` um. Vergessen Sie nicht `prefix="/items"` → `prefix="/users"` und `tags=["items"]` → `tags=["users"]` im Router. + +Das Repository kann dasselbe `InMemoryStore`-gestützte Muster behalten — es ist generisch über den Entity-Typ: + +```python +# src/app/domains/users/repository.py +_store: InMemoryStore[User] = InMemoryStore() + +class UserRepository: + def __init__(self, store: Optional[InMemoryStore[User]] = None) -> None: + self._store = store if store is not None else _store + # ... same shape as ItemRepository ... +``` + +### 3. Die `__init__.py` der Domäne aktualisieren + +Die items-Domäne re-exportiert ihre Module, sodass Aufrufer `from src.app.domains.items import service` schreiben können. Spiegeln Sie das für users: + +```python +# src/app/domains/users/__init__.py +from src.app.domains.users import ( # noqa: F401 + models, + repository, + router, + schemas, + service, +) +``` + +### 4. Den Router im Aggregator registrieren + +Dies ist die **einzige Datei außerhalb von `domains/users/`, die Sie anfassen müssen**: + +```python +# src/app/api/router.py +from src.app.api import health +from src.app.domains.items import router as items_router +from src.app.domains.users import router as users_router # ← add + +api_router = APIRouter() +api_router.include_router(health.router) +api_router.include_router(items_router.router) +api_router.include_router(users_router.router) # ← add +``` + +Nach einem Server-Neustart sehen Sie `/api/v1/users` in `/docs` eingebunden. + +### 5. Tests hinzufügen + +Spiegeln Sie `tests/test_items.py` als `tests/test_users.py` — dieselbe client-getriebene Form, einfach gegen die neuen Endpunkte. Die autouse-Fixture zur Store-Reset in `conftest.py` hält jeden Test bereits isoliert. + +Wenn Sie eine zweite Domäne hinzufügen, die ebenfalls `InMemoryStore` nutzt, erweitern Sie die Fixture, um auch deren Store zurückzusetzen, oder behalten Sie eine Fixture pro Domäne. + +## Schritt 6: Wie es weitergeht + +- Die [Architektur-Preset-Matrix](../reference/preset-feature-matrix.md) zeigt, was `fastkit init --interactive` für jedes Preset generiert, einschließlich der Funktionsauswahlen, die unter `domain-starter` manuelle Verdrahtung benötigen. +- Das [`fastapi-default`-Tutorial](basic-api-server.md) behandelt die geschichtete Alternative, falls Sie Layouts vergleichen möchten, bevor Sie sich festlegen. +- Für die Datenbankintegration zeigt das [Tutorial Datenbankintegration](database-integration.md) das Muster PostgreSQL + SQLAlchemy + Alembic. Dieselben Ideen passen in `src/app/db/` und die `repository.py`-Dateien je Domäne. + +## Rückblick + +- **Generierung**: `fastkit startdemo fastapi-domain-starter` → `bash scripts/run-server.sh` → Doku unter `/docs`. +- **Layout**: `core/` für Konfiguration, `db/` für Persistenz-Abstraktionen, `domains//` für Geschäfts-Scheiben, `api/router.py` als zentrale Aggregationsstelle, `tests/` als Spiegel der Laufzeitmodule. +- **Domäne hinzufügen**: `items/` kopieren, Entity / Schemas / Klassen umbenennen, Re-Exports in `__init__.py` aktualisieren, Router in `src/app/api/router.py` registrieren, Testmodul ergänzen. Keine Änderung an `main.py`. diff --git a/docs/de/tutorial/first-project.md b/docs/de/tutorial/first-project.md new file mode 100644 index 0000000..26cf657 --- /dev/null +++ b/docs/de/tutorial/first-project.md @@ -0,0 +1,1252 @@ +# Ihr erstes Projekt + +Bauen Sie eine vollständige Blog-API mit Nutzerverwaltung, Beitragserstellung und Kommentarsystem mit FastAPI-fastkit. + +## Projektüberblick + +In diesem Tutorial erstellen wir eine **Blog-API** mit folgenden Funktionen: + +- **Nutzerverwaltung**: Registrierung, Authentifizierung und Nutzerprofile +- **Beitragsverwaltung**: Blog-Beiträge erstellen, lesen, aktualisieren und löschen +- **Kommentarsystem**: Kommentare zu Blog-Beiträgen hinzufügen +- **Datenvalidierung**: robuste Eingabevalidierung und Fehlerbehandlung +- **API-Dokumentation**: automatische OpenAPI-Dokumentation +- **Testen**: vollständige Test-Suite + +### Was Sie lernen werden + +Am Ende dieses Tutorials werden Sie verstehen: + +- Erweiterte Struktur von FastAPI-fastkit-Projekten +- Datenbankintegration mit SQLAlchemy +- Authentifizierung und Autorisierung von Nutzern +- Komplexe Datenbeziehungen +- Fehlerbehandlung und Validierung +- Best Practices beim Testen + +## Voraussetzungen + +Bevor Sie starten, stellen Sie sicher, dass Sie: + +- das [Tutorial Erste Schritte](getting-started.md) abgeschlossen haben +- grundlegendes Verständnis von REST-APIs haben +- Python 3.12+ installiert haben +- einen Texteditor oder eine IDE bereit haben + +## Schritt 1: Das Projekt erstellen + +Beginnen wir mit der Erstellung eines neuen Projekts mit dem **STANDARD**-Stack für Datenbankunterstützung: + +
+ +```console +$ fastkit init +Enter the project name: blog-api +Enter the author name: Your Name +Enter the author email: your.email@example.com +Enter the project description: A complete blog API with users, posts, and comments + + Project Information +┌──────────────┬─────────────────────────────────────────┐ +│ Project Name │ blog-api │ +│ Author │ Your Name │ +│ Author Email │ your.email@example.com │ +│ Description │ A complete blog API with users, posts, │ +│ │ and comments │ +└──────────────┴─────────────────────────────────────────┘ + +Available Stacks and Dependencies: + MINIMAL Stack +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ pydantic │ +│ Dependency 4 │ pydantic-settings │ +└──────────────┴───────────────────┘ + + STANDARD Stack +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ sqlalchemy │ +│ Dependency 4 │ alembic │ +│ Dependency 5 │ pytest │ +│ Dependency 6 │ pydantic │ +│ Dependency 7 │ pydantic-settings │ +└──────────────┴───────────────────┘ + +Select stack (minimal, standard, full): standard + +Available Package Managers: + Package Managers +┌────────┬────────────────────────────────────────────┐ +│ PIP │ Standard Python package manager │ +│ UV │ Fast Python package manager │ +│ PDM │ Modern Python dependency management │ +│ POETRY │ Python dependency management and packaging │ +└────────┴────────────────────────────────────────────┘ + +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y + +✨ FastAPI project 'blog-api' has been created successfully! +``` + +
+ +## Schritt 2: Das Projekt einrichten + +Wechseln Sie in das Projekt und aktivieren Sie die virtuelle Umgebung: + +
+ +```console +$ cd blog-api +$ source .venv/bin/activate +``` + +
+ +## Schritt 3: Benötigte Routen hinzufügen + +Fügen wir die Hauptressourcen unserer Blog-API hinzu: + +
+ +```console +$ fastkit addroute users blog-api +✨ Successfully added new route 'users' to project 'blog-api' + +$ fastkit addroute posts blog-api +✨ Successfully added new route 'posts' to project 'blog-api' + +$ fastkit addroute comments blog-api +✨ Successfully added new route 'comments' to project 'blog-api' +``` + +
+ +## Schritt 4: Die Datenmodelle entwerfen + +Entwerfen wir unsere Datenschemata. Wir beginnen damit, das Nutzer-Schema realistischer zu gestalten. + +### Nutzer-Schema aktualisieren + +Bearbeiten Sie `src/schemas/users.py`: + +```python +from typing import Optional, List +from datetime import datetime +from pydantic import BaseModel, EmailStr, Field + +class UserBase(BaseModel): + email: EmailStr + username: str = Field(..., min_length=3, max_length=50) + full_name: Optional[str] = None + bio: Optional[str] = Field(None, max_length=500) + is_active: bool = True + +class UserCreate(UserBase): + password: str = Field(..., min_length=8) + +class UserUpdate(BaseModel): + email: Optional[EmailStr] = None + username: Optional[str] = Field(None, min_length=3, max_length=50) + full_name: Optional[str] = None + bio: Optional[str] = Field(None, max_length=500) + is_active: Optional[bool] = None + +class User(UserBase): + id: int + created_at: datetime + posts_count: int = 0 + + class Config: + from_attributes = True + +class UserInDB(User): + hashed_password: str +``` + +### Beitrags-Schema erstellen + +Bearbeiten Sie `src/schemas/posts.py`: + +```python +from typing import Optional, List +from datetime import datetime +from pydantic import BaseModel, Field + +class PostBase(BaseModel): + title: str = Field(..., min_length=1, max_length=200) + content: str = Field(..., min_length=1) + published: bool = True + +class PostCreate(PostBase): + pass + +class PostUpdate(BaseModel): + title: Optional[str] = Field(None, min_length=1, max_length=200) + content: Optional[str] = Field(None, min_length=1) + published: Optional[bool] = None + +class Post(PostBase): + id: int + author_id: int + created_at: datetime + updated_at: datetime + comments_count: int = 0 + + class Config: + from_attributes = True + +class PostWithAuthor(Post): + author: "User" + +class PostWithComments(Post): + comments: List["Comment"] = [] + +# Import to avoid circular imports +from src.schemas.users import User +from src.schemas.comments import Comment +PostWithAuthor.model_rebuild() +PostWithComments.model_rebuild() +``` + +### Kommentar-Schema erstellen + +Bearbeiten Sie `src/schemas/comments.py`: + +```python +from typing import Optional +from datetime import datetime +from pydantic import BaseModel, Field + +class CommentBase(BaseModel): + content: str = Field(..., min_length=1, max_length=1000) + +class CommentCreate(CommentBase): + post_id: int + +class CommentUpdate(BaseModel): + content: Optional[str] = Field(None, min_length=1, max_length=1000) + +class Comment(CommentBase): + id: int + post_id: int + author_id: int + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True + +class CommentWithAuthor(Comment): + author: "User" + +# Import to avoid circular imports +from src.schemas.users import User +CommentWithAuthor.model_rebuild() +``` + +## Schritt 5: Erweiterte CRUD-Operationen implementieren + +### Erweitertes Nutzer-CRUD + +Aktualisieren Sie `src/crud/users.py`: + +```python +from typing import List, Optional +from datetime import datetime +import hashlib +from src.schemas.users import UserCreate, UserUpdate, UserInDB + +class UsersCRUD: + def __init__(self): + self._users: List[UserInDB] = [] + self._next_id = 1 + + def _hash_password(self, password: str) -> str: + """Simple password hashing (use bcrypt in production)""" + return hashlib.sha256(password.encode()).hexdigest() + + def _verify_password(self, plain_password: str, hashed_password: str) -> bool: + """Verify password against hash""" + return self._hash_password(plain_password) == hashed_password + + def get_all(self) -> List[UserInDB]: + """Get all users""" + return [user for user in self._users if user.is_active] + + def get_by_id(self, user_id: int) -> Optional[UserInDB]: + """Get user by ID""" + return next((user for user in self._users if user.id == user_id), None) + + def get_by_email(self, email: str) -> Optional[UserInDB]: + """Get user by email""" + return next((user for user in self._users if user.email == email), None) + + def get_by_username(self, username: str) -> Optional[UserInDB]: + """Get user by username""" + return next((user for user in self._users if user.username == username), None) + + def create(self, user: UserCreate) -> UserInDB: + """Create a new user with validation""" + # Check for duplicates + if self.get_by_email(user.email): + raise ValueError("Email already registered") + if self.get_by_username(user.username): + raise ValueError("Username already taken") + + new_user = UserInDB( + id=self._next_id, + email=user.email, + username=user.username, + full_name=user.full_name, + bio=user.bio, + is_active=user.is_active, + created_at=datetime.now(), + posts_count=0, + hashed_password=self._hash_password(user.password) + ) + self._next_id += 1 + self._users.append(new_user) + return new_user + + def update(self, user_id: int, user_update: UserUpdate) -> Optional[UserInDB]: + """Update an existing user""" + user = self.get_by_id(user_id) + if not user: + return None + + # Check for duplicates on email/username changes + update_data = user_update.dict(exclude_unset=True) + if "email" in update_data and update_data["email"] != user.email: + if self.get_by_email(update_data["email"]): + raise ValueError("Email already registered") + + if "username" in update_data and update_data["username"] != user.username: + if self.get_by_username(update_data["username"]): + raise ValueError("Username already taken") + + for field, value in update_data.items(): + setattr(user, field, value) + + return user + + def delete(self, user_id: int) -> bool: + """Soft delete user (deactivate)""" + user = self.get_by_id(user_id) + if user: + user.is_active = False + return True + return False + + def authenticate(self, email: str, password: str) -> Optional[UserInDB]: + """Authenticate user by email and password""" + user = self.get_by_email(email) + if user and self._verify_password(password, user.hashed_password): + return user + return None + +users_crud = UsersCRUD() +``` + +### Beitrags-CRUD + +Aktualisieren Sie `src/crud/posts.py`: + +```python +from typing import List, Optional +from datetime import datetime +from src.schemas.posts import PostCreate, PostUpdate, Post + +class PostsCRUD: + def __init__(self): + self._posts: List[Post] = [] + self._next_id = 1 + + def get_all(self, skip: int = 0, limit: int = 100, published_only: bool = True) -> List[Post]: + """Get all posts with pagination""" + posts = self._posts + if published_only: + posts = [post for post in posts if post.published] + return posts[skip:skip + limit] + + def get_by_id(self, post_id: int) -> Optional[Post]: + """Get post by ID""" + return next((post for post in self._posts if post.id == post_id), None) + + def get_by_author(self, author_id: int, skip: int = 0, limit: int = 100) -> List[Post]: + """Get posts by author""" + author_posts = [post for post in self._posts if post.author_id == author_id] + return author_posts[skip:skip + limit] + + def create(self, post: PostCreate, author_id: int) -> Post: + """Create a new post""" + now = datetime.now() + new_post = Post( + id=self._next_id, + title=post.title, + content=post.content, + published=post.published, + author_id=author_id, + created_at=now, + updated_at=now, + comments_count=0 + ) + self._next_id += 1 + self._posts.append(new_post) + + # Update author's post count + from src.crud.users import users_crud + author = users_crud.get_by_id(author_id) + if author: + author.posts_count += 1 + + return new_post + + def update(self, post_id: int, post_update: PostUpdate, author_id: int) -> Optional[Post]: + """Update an existing post""" + post = self.get_by_id(post_id) + if not post or post.author_id != author_id: + return None + + update_data = post_update.dict(exclude_unset=True) + for field, value in update_data.items(): + setattr(post, field, value) + + post.updated_at = datetime.now() + return post + + def delete(self, post_id: int, author_id: int) -> bool: + """Delete a post""" + post = self.get_by_id(post_id) + if post and post.author_id == author_id: + self._posts.remove(post) + + # Update author's post count + from src.crud.users import users_crud + author = users_crud.get_by_id(author_id) + if author: + author.posts_count = max(0, author.posts_count - 1) + + return True + return False + + def search(self, query: str, skip: int = 0, limit: int = 100) -> List[Post]: + """Search posts by title or content""" + query_lower = query.lower() + matching_posts = [ + post for post in self._posts + if post.published and ( + query_lower in post.title.lower() or + query_lower in post.content.lower() + ) + ] + return matching_posts[skip:skip + limit] + +posts_crud = PostsCRUD() +``` + +### Kommentar-CRUD + +Aktualisieren Sie `src/crud/comments.py`: + +```python +from typing import List, Optional +from datetime import datetime +from src.schemas.comments import CommentCreate, CommentUpdate, Comment + +class CommentsCRUD: + def __init__(self): + self._comments: List[Comment] = [] + self._next_id = 1 + + def get_all(self) -> List[Comment]: + """Get all comments""" + return self._comments + + def get_by_id(self, comment_id: int) -> Optional[Comment]: + """Get comment by ID""" + return next((comment for comment in self._comments if comment.id == comment_id), None) + + def get_by_post(self, post_id: int, skip: int = 0, limit: int = 100) -> List[Comment]: + """Get comments for a specific post""" + post_comments = [comment for comment in self._comments if comment.post_id == post_id] + return post_comments[skip:skip + limit] + + def get_by_author(self, author_id: int, skip: int = 0, limit: int = 100) -> List[Comment]: + """Get comments by author""" + author_comments = [comment for comment in self._comments if comment.author_id == author_id] + return author_comments[skip:skip + limit] + + def create(self, comment: CommentCreate, author_id: int) -> Comment: + """Create a new comment""" + # Verify post exists + from src.crud.posts import posts_crud + post = posts_crud.get_by_id(comment.post_id) + if not post: + raise ValueError("Post not found") + + now = datetime.now() + new_comment = Comment( + id=self._next_id, + content=comment.content, + post_id=comment.post_id, + author_id=author_id, + created_at=now, + updated_at=now + ) + self._next_id += 1 + self._comments.append(new_comment) + + # Update post's comment count + post.comments_count += 1 + + return new_comment + + def update(self, comment_id: int, comment_update: CommentUpdate, author_id: int) -> Optional[Comment]: + """Update an existing comment""" + comment = self.get_by_id(comment_id) + if not comment or comment.author_id != author_id: + return None + + update_data = comment_update.dict(exclude_unset=True) + for field, value in update_data.items(): + setattr(comment, field, value) + + comment.updated_at = datetime.now() + return comment + + def delete(self, comment_id: int, author_id: int) -> bool: + """Delete a comment""" + comment = self.get_by_id(comment_id) + if comment and comment.author_id == author_id: + self._comments.remove(comment) + + # Update post's comment count + from src.crud.posts import posts_crud + post = posts_crud.get_by_id(comment.post_id) + if post: + post.comments_count = max(0, post.comments_count - 1) + + return True + return False + +comments_crud = CommentsCRUD() +``` + +## Schritt 6: Erweiterte API-Routen implementieren + +### Erweiterte Nutzer-Routen + +Aktualisieren Sie `src/api/routes/users.py`: + +```python +from typing import List +from fastapi import APIRouter, HTTPException, status, Depends, Query +from src.schemas.users import User, UserCreate, UserUpdate +from src.crud.users import users_crud + +router = APIRouter() + +# Helper function to get current user (simplified for tutorial) +def get_current_user_id() -> int: + # In a real app, this would verify JWT token and return user ID + return 1 # For tutorial purposes + +@router.get("/", response_model=List[User]) +def read_users( + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=100) +): + """Get all users with pagination""" + users = users_crud.get_all()[skip:skip + limit] + return [User(**user.dict()) for user in users] + +@router.post("/", response_model=User, status_code=status.HTTP_201_CREATED) +def create_user(user: UserCreate): + """Register a new user""" + try: + new_user = users_crud.create(user) + return User(**new_user.dict()) + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + +@router.get("/{user_id}", response_model=User) +def read_user(user_id: int): + """Get a specific user""" + user = users_crud.get_by_id(user_id) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"User with id {user_id} not found" + ) + return User(**user.dict()) + +@router.put("/{user_id}", response_model=User) +def update_user( + user_id: int, + user_update: UserUpdate, + current_user_id: int = Depends(get_current_user_id) +): + """Update user profile""" + if user_id != current_user_id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="You can only update your own profile" + ) + + try: + updated_user = users_crud.update(user_id, user_update) + if not updated_user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + return User(**updated_user.dict()) + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + +@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_user( + user_id: int, + current_user_id: int = Depends(get_current_user_id) +): + """Deactivate user account""" + if user_id != current_user_id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="You can only delete your own account" + ) + + success = users_crud.delete(user_id) + if not success: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + +@router.post("/login") +def login(email: str, password: str): + """Authenticate user""" + user = users_crud.authenticate(email, password) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid email or password" + ) + + # In a real app, return JWT token + return { + "message": "Login successful", + "user_id": user.id, + "username": user.username + } +``` + +### Erweiterte Beitrags-Routen + +Aktualisieren Sie `src/api/routes/posts.py`: + +```python +from typing import List, Optional +from fastapi import APIRouter, HTTPException, status, Depends, Query +from src.schemas.posts import Post, PostCreate, PostUpdate +from src.crud.posts import posts_crud + +router = APIRouter() + +def get_current_user_id() -> int: + return 1 # Simplified for tutorial + +@router.get("/", response_model=List[Post]) +def read_posts( + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=100), + search: Optional[str] = Query(None) +): + """Get all posts with optional search""" + if search: + posts = posts_crud.search(search, skip, limit) + else: + posts = posts_crud.get_all(skip, limit) + return posts + +@router.post("/", response_model=Post, status_code=status.HTTP_201_CREATED) +def create_post( + post: PostCreate, + current_user_id: int = Depends(get_current_user_id) +): + """Create a new blog post""" + new_post = posts_crud.create(post, current_user_id) + return new_post + +@router.get("/{post_id}", response_model=Post) +def read_post(post_id: int): + """Get a specific post""" + post = posts_crud.get_by_id(post_id) + if not post: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Post not found" + ) + return post + +@router.put("/{post_id}", response_model=Post) +def update_post( + post_id: int, + post_update: PostUpdate, + current_user_id: int = Depends(get_current_user_id) +): + """Update a blog post""" + updated_post = posts_crud.update(post_id, post_update, current_user_id) + if not updated_post: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Post not found or you don't have permission to edit it" + ) + return updated_post + +@router.delete("/{post_id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_post( + post_id: int, + current_user_id: int = Depends(get_current_user_id) +): + """Delete a blog post""" + success = posts_crud.delete(post_id, current_user_id) + if not success: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Post not found or you don't have permission to delete it" + ) + +@router.get("/author/{author_id}", response_model=List[Post]) +def read_posts_by_author( + author_id: int, + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=100) +): + """Get posts by a specific author""" + posts = posts_crud.get_by_author(author_id, skip, limit) + return posts +``` + +### Erweiterte Kommentar-Routen + +Aktualisieren Sie `src/api/routes/comments.py`: + +```python +from typing import List +from fastapi import APIRouter, HTTPException, status, Depends, Query +from src.schemas.comments import Comment, CommentCreate, CommentUpdate +from src.crud.comments import comments_crud + +router = APIRouter() + +def get_current_user_id() -> int: + return 1 # Simplified for tutorial + +@router.get("/", response_model=List[Comment]) +def read_comments( + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=100) +): + """Get all comments""" + comments = comments_crud.get_all()[skip:skip + limit] + return comments + +@router.post("/", response_model=Comment, status_code=status.HTTP_201_CREATED) +def create_comment( + comment: CommentCreate, + current_user_id: int = Depends(get_current_user_id) +): + """Create a new comment""" + try: + new_comment = comments_crud.create(comment, current_user_id) + return new_comment + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + +@router.get("/{comment_id}", response_model=Comment) +def read_comment(comment_id: int): + """Get a specific comment""" + comment = comments_crud.get_by_id(comment_id) + if not comment: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Comment not found" + ) + return comment + +@router.put("/{comment_id}", response_model=Comment) +def update_comment( + comment_id: int, + comment_update: CommentUpdate, + current_user_id: int = Depends(get_current_user_id) +): + """Update a comment""" + updated_comment = comments_crud.update(comment_id, comment_update, current_user_id) + if not updated_comment: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Comment not found or you don't have permission to edit it" + ) + return updated_comment + +@router.delete("/{comment_id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_comment( + comment_id: int, + current_user_id: int = Depends(get_current_user_id) +): + """Delete a comment""" + success = comments_crud.delete(comment_id, current_user_id) + if not success: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Comment not found or you don't have permission to delete it" + ) + +@router.get("/post/{post_id}", response_model=List[Comment]) +def read_comments_by_post( + post_id: int, + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=100) +): + """Get comments for a specific post""" + comments = comments_crud.get_by_post(post_id, skip, limit) + return comments + +@router.get("/author/{author_id}", response_model=List[Comment]) +def read_comments_by_author( + author_id: int, + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=100) +): + """Get comments by a specific author""" + comments = comments_crud.get_by_author(author_id, skip, limit) + return comments +``` + +## Schritt 7: Ihre Blog-API testen + +Starten wir den Server und testen unsere vollständige Blog-API: + +
+ +```console +$ fastkit runserver +INFO: Uvicorn running on http://127.0.0.1:8000 +``` + +
+ +### Nutzerregistrierung testen + +
+ +```console +$ curl -X POST "http://127.0.0.1:8000/api/v1/users/" \ + -H "Content-Type: application/json" \ + -d '{ + "email": "john@example.com", + "username": "john_doe", + "full_name": "John Doe", + "bio": "Software developer and blogger", + "password": "securepassword123" + }' + +{ + "id": 1, + "email": "john@example.com", + "username": "john_doe", + "full_name": "John Doe", + "bio": "Software developer and blogger", + "is_active": true, + "created_at": "2023-12-07T10:30:00", + "posts_count": 0 +} +``` + +
+ +### Nutzeranmeldung testen + +
+ +```console +$ curl -X POST "http://127.0.0.1:8000/api/v1/users/login" \ + -H "Content-Type: application/json" \ + -d '{ + "email": "john@example.com", + "password": "securepassword123" + }' + +{ + "message": "Login successful", + "user_id": 1, + "username": "john_doe" +} +``` + +
+ +### Beitragserstellung testen + +
+ +```console +$ curl -X POST "http://127.0.0.1:8000/api/v1/posts/" \ + -H "Content-Type: application/json" \ + -d '{ + "title": "My First Blog Post", + "content": "This is the content of my first blog post. It is about learning FastAPI with FastAPI-fastkit!", + "published": true + }' + +{ + "id": 1, + "title": "My First Blog Post", + "content": "This is the content of my first blog post. It is about learning FastAPI with FastAPI-fastkit!", + "published": true, + "author_id": 1, + "created_at": "2023-12-07T10:35:00", + "updated_at": "2023-12-07T10:35:00", + "comments_count": 0 +} +``` + +
+ +### Kommentarerstellung testen + +
+ +```console +$ curl -X POST "http://127.0.0.1:8000/api/v1/comments/" \ + -H "Content-Type: application/json" \ + -d '{ + "content": "Great post! I learned a lot from this.", + "post_id": 1 + }' + +{ + "id": 1, + "content": "Great post! I learned a lot from this.", + "post_id": 1, + "author_id": 1, + "created_at": "2023-12-07T10:40:00", + "updated_at": "2023-12-07T10:40:00" +} +``` + +
+ +### Suchfunktion testen + +
+ +```console +$ curl "http://127.0.0.1:8000/api/v1/posts/?search=FastAPI" + +[ + { + "id": 1, + "title": "My First Blog Post", + "content": "This is the content of my first blog post. It is about learning FastAPI with FastAPI-fastkit!", + "published": true, + "author_id": 1, + "created_at": "2023-12-07T10:35:00", + "updated_at": "2023-12-07T10:35:00", + "comments_count": 1 + } +] +``` + +
+ +## Schritt 8: API-Dokumentation + +Besuchen Sie [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs), um die vollständige Dokumentation Ihrer API zu sehen. Sie sollten nun sehen: + +- **Users**: Registrierung, Anmeldung, Profilverwaltung +- **Posts**: CRUD-Operationen, Suche, Filterung nach Autor +- **Comments**: CRUD-Operationen, Filterung nach Beitrag/Autor +- **Items**: ursprüngliche Beispiel-Endpunkte + +Die Dokumentation zeigt: + +- alle verfügbaren Endpunkte +- Anfrage-/Antwortschemas +- Datenvalidierungsregeln +- Fehlerantworten + +## Schritt 9: Tests schreiben + +Erstellen wir umfassende Tests für unsere Blog-API. Legen Sie `tests/test_blog_api.py` an: + +```python +from fastapi.testclient import TestClient +from src.main import app + +client = TestClient(app) + +class TestUserAPI: + def test_create_user(self): + user_data = { + "email": "test@example.com", + "username": "testuser", + "full_name": "Test User", + "bio": "Test bio", + "password": "testpassword123" + } + response = client.post("/api/v1/users/", json=user_data) + assert response.status_code == 201 + data = response.json() + assert data["email"] == user_data["email"] + assert data["username"] == user_data["username"] + assert "id" in data + assert "hashed_password" not in data # Should not expose password + + def test_duplicate_email(self): + # First user + user_data1 = { + "email": "duplicate@example.com", + "username": "user1", + "password": "password123" + } + response1 = client.post("/api/v1/users/", json=user_data1) + assert response1.status_code == 201 + + # Second user with same email + user_data2 = { + "email": "duplicate@example.com", + "username": "user2", + "password": "password123" + } + response2 = client.post("/api/v1/users/", json=user_data2) + assert response2.status_code == 400 + assert "Email already registered" in response2.json()["detail"] + + def test_login(self): + # Create user first + user_data = { + "email": "login@example.com", + "username": "loginuser", + "password": "loginpassword123" + } + client.post("/api/v1/users/", json=user_data) + + # Test login + login_data = { + "email": "login@example.com", + "password": "loginpassword123" + } + response = client.post("/api/v1/users/login", json=login_data) + assert response.status_code == 200 + data = response.json() + assert "user_id" in data + assert data["username"] == "loginuser" + +class TestPostAPI: + def test_create_post(self): + post_data = { + "title": "Test Post", + "content": "This is a test post content", + "published": True + } + response = client.post("/api/v1/posts/", json=post_data) + assert response.status_code == 201 + data = response.json() + assert data["title"] == post_data["title"] + assert data["content"] == post_data["content"] + assert "id" in data + assert "author_id" in data + + def test_read_posts(self): + response = client.get("/api/v1/posts/") + assert response.status_code == 200 + data = response.json() + assert isinstance(data, list) + + def test_search_posts(self): + # Create a post with specific content + post_data = { + "title": "FastAPI Tutorial", + "content": "Learn how to build APIs with FastAPI", + "published": True + } + client.post("/api/v1/posts/", json=post_data) + + # Search for the post + response = client.get("/api/v1/posts/?search=FastAPI") + assert response.status_code == 200 + data = response.json() + assert len(data) > 0 + assert any("FastAPI" in post["title"] or "FastAPI" in post["content"] for post in data) + +class TestCommentAPI: + def test_create_comment(self): + # Create a post first + post_data = { + "title": "Post for Comments", + "content": "This post will receive comments", + "published": True + } + post_response = client.post("/api/v1/posts/", json=post_data) + post_id = post_response.json()["id"] + + # Create comment + comment_data = { + "content": "This is a test comment", + "post_id": post_id + } + response = client.post("/api/v1/comments/", json=comment_data) + assert response.status_code == 201 + data = response.json() + assert data["content"] == comment_data["content"] + assert data["post_id"] == post_id + + def test_get_comments_by_post(self): + # Create post and comment first + post_data = { + "title": "Post with Comments", + "content": "This post has comments", + "published": True + } + post_response = client.post("/api/v1/posts/", json=post_data) + post_id = post_response.json()["id"] + + comment_data = { + "content": "Comment on post", + "post_id": post_id + } + client.post("/api/v1/comments/", json=comment_data) + + # Get comments for the post + response = client.get(f"/api/v1/comments/post/{post_id}") + assert response.status_code == 200 + data = response.json() + assert len(data) > 0 + assert all(comment["post_id"] == post_id for comment in data) + +# Run the tests +if __name__ == "__main__": + import pytest + pytest.main([__file__]) +``` + +### Tests ausführen + +
+ +```console +$ python -m pytest tests/test_blog_api.py -v +======================== test session starts ======================== +tests/test_blog_api.py::TestUserAPI::test_create_user PASSED +tests/test_blog_api.py::TestUserAPI::test_duplicate_email PASSED +tests/test_blog_api.py::TestUserAPI::test_login PASSED +tests/test_blog_api.py::TestPostAPI::test_create_post PASSED +tests/test_blog_api.py::TestPostAPI::test_read_posts PASSED +tests/test_blog_api.py::TestPostAPI::test_search_posts PASSED +tests/test_blog_api.py::TestCommentAPI::test_create_comment PASSED +tests/test_blog_api.py::TestCommentAPI::test_get_comments_by_post PASSED +======================== 8 passed in 1.23s ======================== +``` + +
+ +## Was Sie gebaut haben + +Glückwunsch! Sie haben erfolgreich eine vollständige Blog-API gebaut mit: + +### ✅ Implementierte Funktionen + +- **Nutzerverwaltung** + - Nutzerregistrierung mit Validierung + - Nutzerauthentifizierung (Login) + - Profilverwaltung + - Verhinderung von Duplikaten + +- **Blog-Beiträge** + - Beiträge erstellen, lesen, aktualisieren, löschen + - Filterung nach Autor + - Suchfunktionalität + - Veröffentlicht-/Entwurf-Status + +- **Kommentarsystem** + - Kommentare zu Beiträgen hinzufügen + - Kommentare nach Beitrag oder Autor anzeigen + - Kommentarverwaltung + +- **Datenvalidierung** + - E-Mail-Validierung + - Anforderungen an Passwörter + - Inhaltslängen-Begrenzungen + - Validierung von Pflichtfeldern + +- **Fehlerbehandlung** + - Passende HTTP-Statuscodes + - Aussagekräftige Fehlermeldungen + - Validierungsfehler bei Eingaben + +- **API-Dokumentation** + - automatische OpenAPI-Generierung + - interaktive Testoberfläche + - Anfrage-/Antwortschemas + +- **Tests** + - umfassende Testabdeckung + - Unit-Tests für alle Endpunkte + - Tests für Grenzfälle + +## Nächste Schritte + +### Mögliche Erweiterungen + +1. **Echte Authentifizierung** + - JWT-Tokens implementieren + - Passwort-Hashing mit bcrypt + - rollenbasierte Berechtigungen + +2. **Datenbankintegration** + - PostgreSQL oder MySQL verwenden + - echte Datenbankmodelle implementieren + - Datenbankmigrationen hinzufügen + +3. **Erweiterte Funktionen** + - Datei-Uploads für Bilder + - E-Mail-Benachrichtigungen + - Beitragskategorien/-tags + - Like/Dislike-System + +4. **Produktionsreife** + - Logging hinzufügen + - Caching implementieren + - Rate-Limiting ergänzen + - Umgebungskonfiguration + +### Weiter lernen + +1. **[Vorlagen verwenden](../user-guide/using-templates.md)**: erkunden Sie die Vorlage `fastapi-psql-orm` für Datenbankintegration +2. **[Routen hinzufügen](../user-guide/adding-routes.md)**: lernen Sie fortgeschrittenere Routenmuster +3. **[Mitwirken](../contributing/development-setup.md)**: tragen Sie zu FastAPI-fastkit bei + +!!! tip "Best Practices, die Sie gelernt haben" + - **Modulare Architektur**: Trennung der Belange in Schemas, CRUD und Routen + - **Datenvalidierung**: Pydantic für robuste Eingabevalidierung nutzen + - **Fehlerbehandlung**: passende HTTP-Statuscodes und Fehlermeldungen + - **Testen**: umfassende Testabdeckung für alle Funktionen + - **Dokumentation**: automatische Generierung der API-Dokumentation ausnutzen + +Sie haben nun die Fähigkeiten, produktionsreife APIs mit FastAPI-fastkit zu bauen! 🚀 diff --git a/docs/de/tutorial/getting-started.md b/docs/de/tutorial/getting-started.md new file mode 100644 index 0000000..4b468fe --- /dev/null +++ b/docs/de/tutorial/getting-started.md @@ -0,0 +1,564 @@ +# Erste Schritte + +Ein umfassendes Schritt-für-Schritt-Tutorial zum Einstieg in FastAPI-fastkit. Dieser Leitfaden führt Sie in etwa 15 Minuten von der Installation bis zum Ausführen Ihrer ersten API. + +## Voraussetzungen + +Bevor Sie starten, stellen Sie sicher, dass Sie haben: + +- **Python 3.12 oder höher** auf Ihrem System installiert +- **Grundkenntnisse in Python** (Variablen, Funktionen, Klassen) +- Zugang zum **Terminal / zur Kommandozeile** +- **Einen Texteditor oder eine IDE** (VS Code, PyCharm usw.) + +## Schritt 1: Installation + +Zuerst installieren wir FastAPI-fastkit. Wir empfehlen die Nutzung einer virtuellen Umgebung, um Ihre Projekte zu isolieren. + +### Option A: mit pip (klassisch) + +
+ +```console +$ pip install fastapi-fastkit +---> 100% +Successfully installed fastapi-fastkit +``` + +
+ +### Option B: mit UV (empfohlen — schneller) + +UV ist ein schneller Python-Paketmanager. Falls Sie UV nicht installiert haben: + +
+ +```console +# Install UV first +$ curl -LsSf https://astral.sh/uv/install.sh | sh + +# Then install FastAPI-fastkit +$ uv pip install fastapi-fastkit +---> 100% +Successfully installed fastapi-fastkit +``` + +
+ +### Option C: mit virtueller Umgebung + +
+ +```console +$ python -m venv fastapi-env +$ source fastapi-env/bin/activate # On Windows: fastapi-env\Scripts\activate +$ pip install fastapi-fastkit +``` + +
+ +### Installation überprüfen + +Überprüfen Sie, dass FastAPI-fastkit korrekt installiert ist: + +
+ +```console +$ fastkit --version +FastAPI-fastkit version 1.0.0 +``` + +
+ +## Schritt 2: Ihr erstes Projekt erstellen + +Erstellen wir nun Ihr erstes FastAPI-Projekt mit dem interaktiven Befehl `init`: + +
+ +```console +$ fastkit init +Enter the project name: my-first-api +Enter the author name: Your Name +Enter the author email: your.email@example.com +Enter the project description: My first FastAPI project + + Project Information +┌──────────────┬─────────────────────────┐ +│ Project Name │ my-first-api │ +│ Author │ Your Name │ +│ Author Email │ your.email@example.com │ +│ Description │ My first FastAPI project│ +└──────────────┴─────────────────────────┘ + +Available Stacks and Dependencies: + MINIMAL Stack +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ pydantic │ +│ Dependency 4 │ pydantic-settings │ +└──────────────┴───────────────────┘ + + STANDARD Stack +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ sqlalchemy │ +│ Dependency 4 │ alembic │ +│ Dependency 5 │ pytest │ +│ Dependency 6 │ pydantic │ +│ Dependency 7 │ pydantic-settings │ +└──────────────┴───────────────────┘ + +Select stack (minimal, standard, full): minimal + +Available Package Managers: + Package Managers +┌────────┬────────────────────────────────────────────┐ +│ PIP │ Standard Python package manager │ +│ UV │ Fast Python package manager │ +│ PDM │ Modern Python dependency management │ +│ POETRY │ Python dependency management and packaging │ +└────────┴────────────────────────────────────────────┘ + +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y + +Creating virtual environment... +Installing dependencies... +✨ FastAPI project 'my-first-api' has been created successfully! +``` + +
+ +!!! note "Stack-Auswahl" + Wir haben für dieses Tutorial **MINIMAL** gewählt, um es einfach zu halten. Für echte Projekte ziehen Sie **STANDARD** (mit Datenbankunterstützung) oder **FULL** (mit Hintergrundaufgaben) in Betracht. + +## Schritt 3: In Ihr Projekt wechseln + +Wechseln Sie in das neu erstellte Projektverzeichnis: + +
+ +```console +$ cd my-first-api +$ ls -la +total 32 +drwxr-xr-x 8 user user 256 Dec 7 10:30 . +drwxr-xr-x 3 user user 96 Dec 7 10:30 .. +drwxr-xr-x 5 user user 160 Dec 7 10:30 .venv +-rw-r--r-- 1 user user 156 Dec 7 10:30 README.md +-rw-r--r-- 1 user user 243 Dec 7 10:30 requirements.txt +drwxr-xr-x 3 user user 96 Dec 7 10:30 scripts +-rw-r--r-- 1 user user 1245 Dec 7 10:30 setup.py +drwxr-xr-x 8 user user 256 Dec 7 10:30 src +drwxr-xr-x 3 user user 96 Dec 7 10:30 tests +``` + +
+ +## Schritt 4: Virtuelle Umgebung aktivieren + +Ihr Projekt enthält eine vorkonfigurierte virtuelle Umgebung. Aktivieren Sie sie: + +
+ +```console +$ source .venv/bin/activate # On Windows: .venv\Scripts\activate +(my-first-api) $ +``` + +
+ +Beachten Sie, dass Ihre Terminal-Eingabeaufforderung jetzt `(my-first-api)` anzeigt, was darauf hinweist, dass die virtuelle Umgebung aktiv ist. + +## Schritt 5: Entwicklungsserver starten + +Nun kommt der spannende Teil — starten wir Ihren FastAPI-Server: + +
+ +```console +$ fastkit runserver +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] using StatReload +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +🎉 **Glückwunsch!** Ihr FastAPI-Server läuft jetzt. + +## Schritt 6: Ihre API testen + +Lassen Sie uns Ihre API auf mehrere Arten testen: + +### Methode 1: Browser + +Öffnen Sie Ihren Browser und besuchen Sie: + +- **Haupt-API-Endpunkt**: [http://127.0.0.1:8000](http://127.0.0.1:8000) + +Sie sollten sehen: +```json +{"message": "Hello World"} +``` + +### Methode 2: Interaktive API-Dokumentation + +Besuchen Sie die automatisch generierte API-Dokumentation: + +- **Swagger UI**: [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs) +- **ReDoc**: [http://127.0.0.1:8000/redoc](http://127.0.0.1:8000/redoc) + +Die Swagger UI ist besonders nützlich — Sie können: + +- alle verfügbaren Endpunkte sehen +- Endpunkte direkt im Browser testen +- Anfrage-/Antwortschemas einsehen +- OpenAPI-Spezifikationen herunterladen + +### Methode 3: Kommandozeile + +Öffnen Sie ein neues Terminal (lassen Sie den Server weiterlaufen) und testen Sie mit curl: + +
+ +```console +$ curl http://127.0.0.1:8000 +{"message":"Hello World"} + +$ curl http://127.0.0.1:8000/api/v1/items/ +[] + +$ curl -X POST "http://127.0.0.1:8000/api/v1/items/" \ + -H "Content-Type: application/json" \ + -d '{"title": "My First Item", "description": "This is a test item"}' +{ + "id": 1, + "title": "My First Item", + "description": "This is a test item" +} +``` + +
+ +## Schritt 7: Ihre Projektstruktur verstehen + +Erkunden wir, was FastAPI-fastkit für Sie generiert hat: + +
+ +```console +$ tree src +src/ +├── __init__.py +├── main.py # Einstiegspunkt der FastAPI-Anwendung +├── core/ +│ ├── __init__.py +│ └── config.py # Anwendungskonfiguration +├── api/ +│ ├── __init__.py +│ ├── api.py # Haupt-Router der API +│ └── routes/ +│ ├── __init__.py +│ └── items.py # API-Endpunkte für Items +├── crud/ +│ ├── __init__.py +│ └── items.py # Geschäftslogik für Items +├── schemas/ +│ ├── __init__.py +│ └── items.py # Schemas zur Datenvalidierung +└── mocks/ + ├── __init__.py + └── mock_items.json # Beispieldaten +``` + +
+ +### Wichtige Dateien erklärt + +**`src/main.py`** — das Herz Ihrer Anwendung: +```python +from fastapi import FastAPI +from src.api.api import api_router +from src.core.config import settings + +app = FastAPI( + title=settings.PROJECT_NAME, + version=settings.VERSION, + openapi_url=f"{settings.API_V1_STR}/openapi.json" +) + +app.include_router(api_router, prefix=settings.API_V1_STR) + +@app.get("/") +def read_root(): + return {"message": "Hello World"} +``` + +**`src/core/config.py`** — Anwendungseinstellungen: +```python +from pydantic_settings import BaseSettings + +class Settings(BaseSettings): + PROJECT_NAME: str = "my-first-api" + VERSION: str = "1.0.0" + API_V1_STR: str = "/api/v1" + + class Config: + env_file = ".env" + +settings = Settings() +``` + +**`src/api/routes/items.py`** — API-Endpunkte: +```python +from typing import List +from fastapi import APIRouter, HTTPException +from src.schemas.items import Item, ItemCreate, ItemUpdate +from src.crud.items import items_crud + +router = APIRouter() + +@router.get("/", response_model=List[Item]) +def read_items(): + """Get all items""" + return items_crud.get_all() + +@router.post("/", response_model=Item) +def create_item(item: ItemCreate): + """Create a new item""" + return items_crud.create(item) +``` + +## Schritt 8: Ihre erste benutzerdefinierte Route hinzufügen + +Fügen wir eine neue API-Route hinzu, um das Gelernte zu üben: + +
+ +```console +$ fastkit addroute users my-first-api + Adding New Route +┌──────────────────┬──────────────────────────────────────────┐ +│ Project │ my-first-api │ +│ Route Name │ users │ +│ Target Directory │ ~/my-first-api │ +└──────────────────┴──────────────────────────────────────────┘ + +Do you want to add route 'users' to project 'my-first-api'? [Y/n]: y + +✨ Successfully added new route 'users' to project 'my-first-api' +``` + +
+ +Der Server startet automatisch neu und Sie verfügen nun über neue Endpunkte: + +- `GET /api/v1/users/` — alle Nutzer abrufen +- `POST /api/v1/users/` — einen neuen Nutzer anlegen +- `GET /api/v1/users/{user_id}` — einen bestimmten Nutzer abrufen +- und mehr… + +### Ihre neue Route testen + +
+ +```console +$ curl -X POST "http://127.0.0.1:8000/api/v1/users/" \ + -H "Content-Type: application/json" \ + -d '{"title": "John Doe", "description": "Software Developer"}' +{ + "id": 1, + "title": "John Doe", + "description": "Software Developer" +} + +$ curl http://127.0.0.1:8000/api/v1/users/ +[ + { + "id": 1, + "title": "John Doe", + "description": "Software Developer" + } +] +``` + +
+ +## Schritt 9: Den Code erkunden und anpassen + +Nehmen wir eine kleine Anpassung vor, um zu verstehen, wie der Code funktioniert. + +### Die Willkommensnachricht anpassen + +Öffnen Sie `src/main.py` in Ihrem Texteditor und ändern Sie den Root-Endpunkt: + +```python +@app.get("/") +def read_root(): + return {"message": "Welcome to my first FastAPI application!"} +``` + +Speichern Sie die Datei. Dank automatischem Neuladen startet Ihr Server automatisch neu. + +### Die Änderung testen + +
+ +```console +$ curl http://127.0.0.1:8000 +{"message":"Welcome to my first FastAPI application!"} +``` + +
+ +### Einen neuen Endpunkt hinzufügen + +Fügen wir einen einfachen Endpunkt in `src/main.py` hinzu: + +```python +@app.get("/hello/{name}") +def say_hello(name: str): + return {"message": f"Hello, {name}!"} +``` + +### Den neuen Endpunkt testen + +
+ +```console +$ curl http://127.0.0.1:8000/hello/World +{"message":"Hello, World!"} + +$ curl http://127.0.0.1:8000/hello/FastAPI +{"message":"Hello, FastAPI!"} +``` + +
+ +## Schritt 10: Tests ausführen + +Ihr Projekt enthält vorkonfigurierte Tests. Lassen Sie sie laufen: + +
+ +```console +$ python -m pytest +======================== test session starts ======================== +collected 5 items + +tests/test_items.py::test_create_item PASSED +tests/test_items.py::test_read_items PASSED +tests/test_items.py::test_read_item PASSED +tests/test_items.py::test_update_item PASSED +tests/test_items.py::test_delete_item PASSED + +======================== 5 passed in 0.45s ======================== +``` + +
+ +## Kernkonzepte verstehen + +### 1. Struktur einer FastAPI-Anwendung + +FastAPI-fastkit folgt einer **modularen Architektur**: + +- **`main.py`**: Einstiegspunkt der Anwendung und globale Endpunkte +- **`api/`**: Organisation der API-Routen +- **`core/`**: Konfiguration und Einstellungen der Anwendung +- **`crud/`**: Geschäftslogik und Datenoperationen +- **`schemas/`**: Datenvalidierung und Serialisierung +- **`tests/`**: Automatisierte Tests + +### 2. Abhängigkeitsverwaltung + +Ihr Projekt nutzt moderne Python-Abhängigkeitsverwaltung: + +- **Virtuelle Umgebung**: isolierte Python-Umgebung +- **requirements.txt**: listet alle Abhängigkeiten +- **Automatische Installation**: Abhängigkeiten werden bei der Projekterstellung installiert + +### 3. Entwicklungsserver + +FastAPI-fastkit verwendet **Uvicorn** als ASGI-Server: + +- **Automatisches Neuladen**: startet automatisch neu, wenn sich Code ändert +- **Schneller Start**: schnelle Entwicklungsiteration +- **Produktionsreif**: derselbe Server wird in der Produktion verwendet + +### 4. API-Dokumentation + +FastAPI generiert automatisch: + +- **OpenAPI-Spezifikation**: branchenstandardisierte API-Dokumentation +- **Swagger UI**: interaktive Testoberfläche +- **ReDoc**: alternative Dokumentationsansicht + +## Nächste Schritte + +Glückwunsch! Sie haben erfolgreich: + +✅ FastAPI-fastkit installiert +✅ Ihr erstes Projekt erstellt +✅ den Entwicklungsserver gestartet +✅ Ihre API-Endpunkte getestet +✅ eine neue Route hinzugefügt +✅ vorhandenen Code angepasst +✅ Tests ausgeführt + +### Weiter lernen + +1. **[Ihr erstes Projekt](first-project.md)**: bauen Sie eine vollständige Blog-API mit fortgeschrittenen Funktionen +2. **[Routen hinzufügen](../user-guide/adding-routes.md)**: lernen Sie, komplexe API-Endpunkte zu erstellen +3. **[Vorlagen verwenden](../user-guide/using-templates.md)**: erkunden Sie vorgefertigte Projektvorlagen + +### Mehr experimentieren + +Probieren Sie diese Herausforderungen aus: + +1. **Validierung hinzufügen**: erweitern Sie Schemata um Validierungsregeln +2. **Benutzerdefinierte Antworten**: ändern Sie Antwortformate in Routen +3. **Umgebungsvariablen**: nutzen Sie `.env`-Dateien für Konfiguration +4. **Middleware hinzufügen**: implementieren Sie CORS oder Authentifizierung +5. **Datenbankintegration**: wechseln Sie auf den STANDARD-Stack für Datenbankunterstützung + +### Häufige Probleme und Lösungen + +**Server startet nicht:** + +- Prüfen Sie, dass Sie im Projektverzeichnis sind +- Stellen Sie sicher, dass die virtuelle Umgebung aktiviert ist +- Überprüfen Sie, dass keine Syntaxfehler im Code vorliegen + +**Import-Fehler:** + +- Stellen Sie sicher, dass alle `__init__.py`-Dateien existieren +- Prüfen Sie, dass Ihre Importpfade korrekt sind +- Vergewissern Sie sich, dass Sie die virtuelle Umgebung nutzen + +**Port bereits in Verwendung:** +```console +$ fastkit runserver --port 8080 +``` + +## Best Practices, die Sie gelernt haben + +1. **Virtuelle Umgebungen**: immer isolierte Umgebungen verwenden +2. **Projektstruktur**: organisierter, modularer Architektur folgen +3. **Automatisches Neuladen**: Entwicklungsserver für schnelle Iteration nutzen +4. **API-Dokumentation**: automatische Dokumentationsgenerierung ausnutzen +5. **Testen**: Tests während der Entwicklung regelmäßig ausführen + +!!! tip "Entwicklungstipps" + - Halten Sie den Entwicklungsserver während des Codens am Laufen + - Nutzen Sie die interaktive Doku (`/docs`), um Ihre APIs zu testen + - Beobachten Sie das Terminal auf hilfreiche Fehlermeldungen + - Committen Sie Ihren Code regelmäßig in die Versionsverwaltung + +Sie sind nun bereit, beeindruckende APIs mit FastAPI-fastkit zu bauen! 🚀 diff --git a/docs/de/tutorial/mcp-integration.md b/docs/de/tutorial/mcp-integration.md new file mode 100644 index 0000000..4189256 --- /dev/null +++ b/docs/de/tutorial/mcp-integration.md @@ -0,0 +1,1730 @@ +# MCP-Integration (Model Context Protocol) + +Lernen Sie, wie Sie das Model Context Protocol (MCP) mit FastAPI integrieren, um ein System aufzubauen, in dem KI-Modelle API-Endpunkte als Werkzeuge nutzen können. Wir implementieren eine vollständige KI-integrierte API mit Authentifizierung, Berechtigungsverwaltung und MCP-Server-Implementierung anhand der Vorlage `fastapi-mcp`. + +## Was Sie in diesem Tutorial lernen + +- Konzepte und Implementierung des Model Context Protocol (MCP) +- Ein JWT-basiertes Authentifizierungssystem aufbauen +- Rollenbasierte Zugriffskontrolle (RBAC) implementieren +- MCP-Werkzeuge bereitstellen und verwalten +- Sichere API-Kommunikation mit KI-Modellen +- Verwaltung von Nutzersitzungen und Kontext + +## Voraussetzungen + +- Das [Tutorial Benutzerdefinierte Antwortbehandlung](custom-response-handling.md) abgeschlossen +- Verständnis der Grundkonzepte von JWT und OAuth2 +- Konzepte der API-Kommunikation mit KI/LLM-Modellen +- Grundkenntnisse des MCP-Protokolls + +## Was ist das Model Context Protocol (MCP)? + +MCP ist ein standardisiertes Protokoll, das es KI-Modellen ermöglicht, mit externen Systemen zu interagieren. + +### Klassischer Ansatz vs. MCP + +**Klassischer Ansatz (direkte API-Aufrufe):** +``` +AI Model → HTTP Request → API Server → Response +``` + +**MCP-Ansatz:** +``` +AI Model → MCP Client → MCP Server (FastAPI) → Safe Tool Execution → Response +``` + +### Vorteile von MCP + +- **Sicherheit**: integrierte Authentifizierung und Berechtigungsverwaltung +- **Standardisierung**: konsistente Schnittstellenbereitstellung +- **Kontextverwaltung**: sitzungsbasierter Statuserhalt +- **Werkzeug-Abstraktion**: komplexe APIs als einfache Werkzeuge bereitstellen + +## Schritt 1: Ein MCP-Integrationsprojekt erstellen + +Erstellen Sie ein Projekt mit der Vorlage `fastapi-mcp`: + +
+ +```console +$ fastkit startdemo fastapi-mcp +Enter the project name: ai-integrated-api +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: MCP-based API server integrated with AI models +Deploying FastAPI project using 'fastapi-mcp' template + + Project Information +┌──────────────┬─────────────────────────────────────────────┐ +│ Project Name │ ai-integrated-api │ +│ Author │ Developer Kim │ +│ Author Email │ developer@example.com │ +│ Description │ MCP-based API server integrated with AI models │ +└──────────────┴─────────────────────────────────────────────┘ + + Template Dependencies +┌──────────────┬────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ pydantic │ +│ Dependency 4 │ python-jose │ +│ Dependency 5 │ passlib │ +│ Dependency 6 │ python-multipart│ +│ Dependency 7 │ mcp │ +└──────────────┴────────────────┘ + +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y + +✨ FastAPI project 'ai-integrated-api' from 'fastapi-mcp' has been created successfully! +``` + +
+ +## Schritt 2: Projektstruktur analysieren + +Sehen wir uns die Struktur des generierten Projekts an: + +``` +ai-integrated-api/ +├── src/ +│ ├── main.py # FastAPI application +│ ├── auth/ +│ │ ├── __init__.py +│ │ ├── models.py # Authentication-related data models +│ │ ├── jwt_handler.py # JWT token processing +│ │ ├── dependencies.py # Authentication dependencies +│ │ └── routes.py # Authentication router +│ ├── mcp/ +│ │ ├── __init__.py +│ │ ├── server.py # MCP server implementation +│ │ ├── tools.py # MCP tool definitions +│ │ └── client.py # MCP client (for testing) +│ ├── api/ +│ │ ├── __init__.py +│ │ ├── api.py # Sammlung der API-Router +│ │ └── routes/ +│ │ ├── items.py # API zur Item-Verwaltung +│ │ ├── users.py # API zur Benutzerverwaltung +│ │ └── admin.py # Admin-API +│ ├── schemas/ +│ │ ├── __init__.py +│ │ ├── auth.py # Authentication schemas +│ │ ├── users.py # User schemas +│ │ └── items.py # Item schemas +│ └── core/ +│ ├── __init__.py +│ ├── config.py # Configuration +│ ├── database.py # Database (in-memory) +│ └── security.py # Security configuration +└── tests/ + ├── test_auth.py # Authentication tests + ├── test_mcp.py # MCP tests + └── test_integration.py # Integration tests +``` + +## Schritt 3: Authentifizierungssystem implementieren + +### Verarbeitung von JWT-Tokens (`src/auth/jwt_handler.py`) + +```python +from datetime import datetime, timedelta +from typing import Optional, Dict, Any +from jose import JWTError, jwt +from passlib.context import CryptContext + +from src.core.config import settings + +# Password hashing +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +def verify_password(plain_password: str, hashed_password: str) -> bool: + """Password verification""" + return pwd_context.verify(plain_password, hashed_password) + +def get_password_hash(password: str) -> str: + """Password hashing""" + return pwd_context.hash(password) + +def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str: + """Access token generation""" + to_encode = data.copy() + + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) + + to_encode.update({"exp": expire, "iat": datetime.utcnow()}) + + encoded_jwt = jwt.encode( + to_encode, + settings.SECRET_KEY, + algorithm=settings.ALGORITHM + ) + + return encoded_jwt + +def create_refresh_token(user_id: str) -> str: + """Refresh token generation""" + data = {"sub": user_id, "type": "refresh"} + expire = datetime.utcnow() + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS) + + to_encode = data.copy() + to_encode.update({"exp": expire, "iat": datetime.utcnow()}) + + return jwt.encode( + to_encode, + settings.SECRET_KEY, + algorithm=settings.ALGORITHM + ) + +def decode_token(token: str) -> Optional[Dict[str, Any]]: + """Token decoding""" + try: + payload = jwt.decode( + token, + settings.SECRET_KEY, + algorithms=[settings.ALGORITHM] + ) + return payload + except JWTError: + return None + +def verify_token(token: str, token_type: str = "access") -> Optional[str]: + """Token verification and user ID return""" + payload = decode_token(token) + + if not payload: + return None + + # Token type verification + if token_type == "refresh" and payload.get("type") != "refresh": + return None + + user_id = payload.get("sub") + if not user_id: + return None + + return user_id + +class TokenManager: + """Token management class""" + + def __init__(self): + self.blacklisted_tokens = set() + + def blacklist_token(self, token: str): + """Add token to blacklist""" + self.blacklisted_tokens.add(token) + + def is_blacklisted(self, token: str) -> bool: + """Check if token is blacklisted""" + return token in self.blacklisted_tokens + + def create_token_pair(self, user_id: str, user_role: str) -> Dict[str, str]: + """Create access/refresh token pair""" + access_token_data = { + "sub": user_id, + "role": user_role, + "type": "access" + } + + access_token = create_access_token(access_token_data) + refresh_token = create_refresh_token(user_id) + + return { + "access_token": access_token, + "refresh_token": refresh_token, + "token_type": "bearer" + } + +# Global token manager +token_manager = TokenManager() +``` + +### Nutzermodelle und Datenbank (`src/auth/models.py`) + +```python +from typing import List, Optional, Dict, Any +from pydantic import BaseModel, EmailStr +from enum import Enum +from datetime import datetime + +class UserRole(str, Enum): + """User roles""" + ADMIN = "admin" + USER = "user" + AI_AGENT = "ai_agent" + READONLY = "readonly" + +class Permission(str, Enum): + """Permissions""" + READ_ITEMS = "read:items" + WRITE_ITEMS = "write:items" + DELETE_ITEMS = "delete:items" + MANAGE_USERS = "manage:users" + USE_MCP_TOOLS = "use:mcp_tools" + ADMIN_MCP = "admin:mcp" + +class User(BaseModel): + """User model""" + id: str + email: EmailStr + username: str + full_name: Optional[str] = None + role: UserRole + permissions: List[Permission] + is_active: bool = True + created_at: datetime + last_login: Optional[datetime] = None + api_key: Optional[str] = None # For MCP client + +class UserInDB(User): + """User model for database storage""" + hashed_password: str + +class UserCreate(BaseModel): + """User creation schema""" + email: EmailStr + username: str + password: str + full_name: Optional[str] = None + role: UserRole = UserRole.USER + +class UserUpdate(BaseModel): + """User update schema""" + email: Optional[EmailStr] = None + username: Optional[str] = None + full_name: Optional[str] = None + role: Optional[UserRole] = None + is_active: Optional[bool] = None + +class LoginRequest(BaseModel): + """Login request schema""" + username: str + password: str + +class TokenResponse(BaseModel): + """Token response schema""" + access_token: str + refresh_token: str + token_type: str = "bearer" + expires_in: int + user: User + +# Default permission mapping by role +ROLE_PERMISSIONS = { + UserRole.ADMIN: [ + Permission.READ_ITEMS, + Permission.WRITE_ITEMS, + Permission.DELETE_ITEMS, + Permission.MANAGE_USERS, + Permission.USE_MCP_TOOLS, + Permission.ADMIN_MCP + ], + UserRole.USER: [ + Permission.READ_ITEMS, + Permission.WRITE_ITEMS, + Permission.USE_MCP_TOOLS + ], + UserRole.AI_AGENT: [ + Permission.READ_ITEMS, + Permission.WRITE_ITEMS, + Permission.USE_MCP_TOOLS + ], + UserRole.READONLY: [ + Permission.READ_ITEMS + ] +} + +class UserDatabase: + """Memory-based user database""" + + def __init__(self): + self.users: Dict[str, UserInDB] = {} + self._init_default_users() + + def _init_default_users(self): + """Create default users""" + from src.auth.jwt_handler import get_password_hash + import uuid + + # Admin account + admin_id = str(uuid.uuid4()) + self.users[admin_id] = UserInDB( + id=admin_id, + email="admin@example.com", + username="admin", + full_name="System Administrator", + role=UserRole.ADMIN, + permissions=ROLE_PERMISSIONS[UserRole.ADMIN], + hashed_password=get_password_hash("admin123"), + created_at=datetime.utcnow(), + api_key=str(uuid.uuid4()) + ) + + # AI agent account + ai_id = str(uuid.uuid4()) + self.users[ai_id] = UserInDB( + id=ai_id, + email="ai@example.com", + username="ai_agent", + full_name="AI Assistant", + role=UserRole.AI_AGENT, + permissions=ROLE_PERMISSIONS[UserRole.AI_AGENT], + hashed_password=get_password_hash("ai123"), + created_at=datetime.utcnow(), + api_key=str(uuid.uuid4()) + ) + + def get_user_by_username(self, username: str) -> Optional[UserInDB]: + """Get user by username""" + return next( + (user for user in self.users.values() if user.username == username), + None + ) + + def get_user_by_id(self, user_id: str) -> Optional[UserInDB]: + """Get user by ID""" + return self.users.get(user_id) + + def get_user_by_api_key(self, api_key: str) -> Optional[UserInDB]: + """Get user by API key""" + return next( + (user for user in self.users.values() if user.api_key == api_key), + None + ) + + def create_user(self, user_create: UserCreate) -> UserInDB: + """Create user""" + import uuid + from src.auth.jwt_handler import get_password_hash + + user_id = str(uuid.uuid4()) + user = UserInDB( + id=user_id, + email=user_create.email, + username=user_create.username, + full_name=user_create.full_name, + role=user_create.role, + permissions=ROLE_PERMISSIONS[user_create.role], + hashed_password=get_password_hash(user_create.password), + created_at=datetime.utcnow(), + api_key=str(uuid.uuid4()) + ) + + self.users[user_id] = user + return user + + def update_user(self, user_id: str, user_update: UserUpdate) -> Optional[UserInDB]: + """Update user""" + if user_id not in self.users: + return None + + user = self.users[user_id] + update_data = user_update.dict(exclude_unset=True) + + for field, value in update_data.items(): + setattr(user, field, value) + + # Update permissions if role changed + if "role" in update_data: + user.permissions = ROLE_PERMISSIONS[user.role] + + return user + + def update_last_login(self, user_id: str): + """Update last login time""" + if user_id in self.users: + self.users[user_id].last_login = datetime.utcnow() + +# Global database instance +user_db = UserDatabase() +``` + +## Schritt 4: Authentifizierungs-Dependencies implementieren + +### Authentifizierungs-Dependencies (`src/auth/dependencies.py`) + +```python +from typing import Optional, List +from fastapi import Depends, HTTPException, status, Security +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials, APIKeyHeader +from jose import JWTError + +from src.auth.jwt_handler import decode_token, token_manager +from src.auth.models import User, UserInDB, Permission, user_db + +# Security schema +security = HTTPBearer() +api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False) + +async def get_current_user( + credentials: HTTPAuthorizationCredentials = Security(security) +) -> User: + """Get current authenticated user""" + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + + try: + token = credentials.credentials + + # Check blacklist + if token_manager.is_blacklisted(token): + raise credentials_exception + + payload = decode_token(token) + if payload is None: + raise credentials_exception + + user_id: str = payload.get("sub") + if user_id is None: + raise credentials_exception + + except JWTError: + raise credentials_exception + + user = user_db.get_user_by_id(user_id) + if user is None: + raise credentials_exception + + if not user.is_active: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Inactive user" + ) + + return User(**user.dict()) + +async def get_current_user_by_api_key( + api_key: Optional[str] = Security(api_key_header) +) -> Optional[User]: + """Authenticate user by API key""" + if not api_key: + return None + + user = user_db.get_user_by_api_key(api_key) + if not user or not user.is_active: + return None + + return User(**user.dict()) + +async def get_current_user_flexible( + token_user: Optional[User] = Depends(get_current_user), + api_key_user: Optional[User] = Depends(get_current_user_by_api_key) +) -> User: + """Authenticate user by token or API key (flexible authentication)""" + user = token_user or api_key_user + + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Authentication required" + ) + + return user + +def require_permissions(*required_permissions: Permission): + """Dependency requiring specific permissions""" + def permission_checker(current_user: User = Depends(get_current_user_flexible)) -> User: + for permission in required_permissions: + if permission not in current_user.permissions: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"Permission '{permission}' required" + ) + return current_user + + return permission_checker + +def require_roles(*required_roles): + """Dependency requiring specific roles""" + def role_checker(current_user: User = Depends(get_current_user_flexible)) -> User: + if current_user.role not in required_roles: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"Role must be one of: {', '.join(required_roles)}" + ) + return current_user + + return role_checker + +# Common permission dependencies +RequireAdmin = require_roles("admin") +RequireReadItems = require_permissions(Permission.READ_ITEMS) +RequireWriteItems = require_permissions(Permission.WRITE_ITEMS) +RequireDeleteItems = require_permissions(Permission.DELETE_ITEMS) +RequireMCPTools = require_permissions(Permission.USE_MCP_TOOLS) +RequireAdminMCP = require_permissions(Permission.ADMIN_MCP) +``` + +### Authentifizierungs-Router (`src/auth/routes.py`) + +```python +from datetime import timedelta +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.security import OAuth2PasswordRequestForm + +from src.auth.models import ( + User, UserCreate, UserUpdate, LoginRequest, TokenResponse, + user_db, UserRole +) +from src.auth.jwt_handler import ( + verify_password, token_manager, verify_token, create_access_token +) +from src.auth.dependencies import get_current_user, RequireAdmin +from src.core.config import settings + +router = APIRouter(prefix="/auth", tags=["authentication"]) + +@router.post("/register", response_model=User) +async def register_user(user_create: UserCreate): + """Register user""" + # Check duplicate username + if user_db.get_user_by_username(user_create.username): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Username already registered" + ) + + # First user is automatically set as admin + if not user_db.users: + user_create.role = UserRole.ADMIN + + user = user_db.create_user(user_create) + return User(**user.dict()) + +@router.post("/login", response_model=TokenResponse) +async def login_user(form_data: OAuth2PasswordRequestForm = Depends()): + """User login""" + user = user_db.get_user_by_username(form_data.username) + + if not user or not verify_password(form_data.password, user.hashed_password): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + + if not user.is_active: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Inactive user" + ) + + # Create token + tokens = token_manager.create_token_pair(user.id, user.role) + + # Update last login time + user_db.update_last_login(user.id) + + return TokenResponse( + access_token=tokens["access_token"], + refresh_token=tokens["refresh_token"], + token_type=tokens["token_type"], + expires_in=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60, + user=User(**user.dict()) + ) + +@router.post("/refresh", response_model=dict) +async def refresh_token(refresh_token: str): + """Refresh token""" + user_id = verify_token(refresh_token, "refresh") + + if not user_id: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid refresh token" + ) + + user = user_db.get_user_by_id(user_id) + if not user or not user.is_active: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="User not found or inactive" + ) + + # Create new token pair + tokens = token_manager.create_token_pair(user.id, user.role) + + return { + "access_token": tokens["access_token"], + "refresh_token": tokens["refresh_token"], + "token_type": tokens["token_type"], + "expires_in": settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60 + } + +@router.post("/logout") +async def logout_user(current_user: User = Depends(get_current_user)): + """Benutzer abmelden""" + # In einer echten Implementierung würde das Token hier auf eine Sperrliste gesetzt + return {"message": "Successfully logged out"} + +@router.get("/me", response_model=User) +async def get_current_user_info(current_user: User = Depends(get_current_user)): + """Get current user information""" + return current_user + +@router.put("/me", response_model=User) +async def update_current_user( + user_update: UserUpdate, + current_user: User = Depends(get_current_user) +): + """Update current user information""" + # Normal users cannot change role + if user_update.role and current_user.role != UserRole.ADMIN: + user_update.role = None + + updated_user = user_db.update_user(current_user.id, user_update) + if not updated_user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + return User(**updated_user.dict()) + +@router.get("/users", response_model=list[User]) +async def list_users(admin_user: User = Depends(RequireAdmin)): + """Get user list (admin only)""" + return [User(**user.dict()) for user in user_db.users.values()] + +@router.post("/users/{user_id}/generate-api-key") +async def generate_api_key( + user_id: str, + admin_user: User = Depends(RequireAdmin) +): + """Create user API key (admin only)""" + import uuid + + user = user_db.get_user_by_id(user_id) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + # Create new API key + new_api_key = str(uuid.uuid4()) + user.api_key = new_api_key + + return { + "api_key": new_api_key, + "message": "API key generated successfully" + } +``` + +## Schritt 5: MCP-Server implementieren + +### Werkzeugdefinitionen (`src/mcp/tools.py`) + +```python +from typing import Dict, Any, List, Optional +from pydantic import BaseModel, Field +from enum import Enum + +class ToolCategory(str, Enum): + """Tool category""" + DATA_MANAGEMENT = "data_management" + SEARCH = "search" + ANALYSIS = "analysis" + ADMIN = "admin" + +class MCPTool(BaseModel): + """MCP tool definition""" + name: str = Field(..., description="Tool name") + description: str = Field(..., description="Tool description") + category: ToolCategory = Field(..., description="Tool category") + parameters: Dict[str, Any] = Field(default_factory=dict, description="Parameter schema") + required_permissions: List[str] = Field(default_factory=list, description="Required permissions") + examples: List[Dict[str, Any]] = Field(default_factory=list, description="Usage examples") + +class ToolRegistry: + """Tool registry""" + + def __init__(self): + self.tools: Dict[str, MCPTool] = {} + self._register_default_tools() + + def _register_default_tools(self): + """Register default tools""" + + # Create item tool + self.register_tool(MCPTool( + name="create_item", + description="Create a new item", + category=ToolCategory.DATA_MANAGEMENT, + parameters={ + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Item name" + }, + "description": { + "type": "string", + "description": "Item description" + }, + "price": { + "type": "number", + "description": "Item price", + "minimum": 0 + }, + "category": { + "type": "string", + "description": "Item category" + } + }, + "required": ["name", "price"] + }, + required_permissions=["write:items"], + examples=[ + { + "name": "Notebook", + "description": "High-performance gaming notebook", + "price": 1500000, + "category": "electronics" + } + ] + )) + + # Search item tool + self.register_tool(MCPTool( + name="search_items", + description="Search for items", + category=ToolCategory.SEARCH, + parameters={ + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Search query" + }, + "category": { + "type": "string", + "description": "Category filter" + }, + "min_price": { + "type": "number", + "description": "Minimum price" + }, + "max_price": { + "type": "number", + "description": "Maximum price" + }, + "limit": { + "type": "integer", + "description": "Result count limit", + "default": 10, + "maximum": 100 + } + }, + "required": ["query"] + }, + required_permissions=["read:items"], + examples=[ + { + "query": "Notebook", + "category": "electronics", + "max_price": 2000000, + "limit": 5 + } + ] + )) + + # Analyze item tool + self.register_tool(MCPTool( + name="analyze_items", + description="Analyze item data", + category=ToolCategory.ANALYSIS, + parameters={ + "type": "object", + "properties": { + "analysis_type": { + "type": "string", + "enum": ["price_distribution", "category_breakdown", "trend_analysis"], + "description": "Analysis type" + }, + "date_range": { + "type": "object", + "properties": { + "start_date": {"type": "string", "format": "date"}, + "end_date": {"type": "string", "format": "date"} + }, + "description": "Analysis period" + } + }, + "required": ["analysis_type"] + }, + required_permissions=["read:items"], + examples=[ + { + "analysis_type": "price_distribution", + "date_range": { + "start_date": "2024-01-01", + "end_date": "2024-12-31" + } + } + ] + )) + + # Manage user tool (admin only) + self.register_tool(MCPTool( + name="manage_users", + description="Manage users", + category=ToolCategory.ADMIN, + parameters={ + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": ["list", "create", "update", "deactivate"], + "description": "Action to perform" + }, + "user_data": { + "type": "object", + "description": "User data (create/update)" + }, + "user_id": { + "type": "string", + "description": "User ID (update/deactivate)" + } + }, + "required": ["action"] + }, + required_permissions=["manage:users"], + examples=[ + { + "action": "list" + }, + { + "action": "create", + "user_data": { + "username": "newuser", + "email": "newuser@example.com", + "role": "user" + } + } + ] + )) + + def register_tool(self, tool: MCPTool): + """Register tool""" + self.tools[tool.name] = tool + + def get_tool(self, tool_name: str) -> Optional[MCPTool]: + """Get tool""" + return self.tools.get(tool_name) + + def list_tools(self, user_permissions: List[str] = None) -> List[MCPTool]: + """List tools by user permissions""" + if user_permissions is None: + return list(self.tools.values()) + + available_tools = [] + for tool in self.tools.values(): + # Check permissions + if all(perm in user_permissions for perm in tool.required_permissions): + available_tools.append(tool) + + return available_tools + + def get_tools_by_category(self, category: ToolCategory, user_permissions: List[str] = None) -> List[MCPTool]: + """List tools by category""" + tools = self.list_tools(user_permissions) + return [tool for tool in tools if tool.category == category] + +# Global tool registry +tool_registry = ToolRegistry() +``` + +### MCP-Server-Implementierung (`src/mcp/server.py`) + +```python +from typing import Dict, Any, List, Optional +from fastapi import HTTPException, status +import asyncio +import json + +from src.mcp.tools import tool_registry, ToolCategory +from src.auth.models import User, Permission +from src.api.routes.items import ItemCRUD +from src.auth.models import user_db + +class MCPServer: + """Model Context Protocol server""" + + def __init__(self): + self.item_crud = ItemCRUD() + self.active_sessions: Dict[str, Dict[str, Any]] = {} + + async def create_session(self, user: User) -> str: + """Create MCP session""" + import uuid + + session_id = str(uuid.uuid4()) + self.active_sessions[session_id] = { + "user_id": user.id, + "user": user, + "created_at": datetime.utcnow(), + "context": {}, + "tool_usage_count": 0, + "last_activity": datetime.utcnow() + } + + return session_id + + async def get_session(self, session_id: str) -> Optional[Dict[str, Any]]: + """Get session""" + session = self.active_sessions.get(session_id) + if session: + session["last_activity"] = datetime.utcnow() + return session + + async def close_session(self, session_id: str): + """Close session""" + if session_id in self.active_sessions: + del self.active_sessions[session_id] + + async def list_tools(self, user: User) -> List[Dict[str, Any]]: + """List tools available to user""" + user_permissions = [perm.value for perm in user.permissions] + tools = tool_registry.list_tools(user_permissions) + + return [ + { + "name": tool.name, + "description": tool.description, + "category": tool.category, + "parameters": tool.parameters, + "examples": tool.examples + } + for tool in tools + ] + + async def execute_tool( + self, + tool_name: str, + parameters: Dict[str, Any], + user: User, + session_id: Optional[str] = None + ) -> Dict[str, Any]: + """Execute tool""" + + # Check if tool exists + tool = tool_registry.get_tool(tool_name) + if not tool: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Tool '{tool_name}' not found" + ) + + # Check permissions + user_permissions = [perm.value for perm in user.permissions] + for required_perm in tool.required_permissions: + if required_perm not in user_permissions: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"Permission '{required_perm}' required for tool '{tool_name}'" + ) + + # Update session + if session_id: + session = await self.get_session(session_id) + if session: + session["tool_usage_count"] += 1 + + # Execute tool + try: + result = await self._execute_tool_logic(tool_name, parameters, user) + + return { + "success": True, + "tool": tool_name, + "result": result, + "timestamp": datetime.utcnow().isoformat() + } + + except Exception as e: + return { + "success": False, + "tool": tool_name, + "error": str(e), + "timestamp": datetime.utcnow().isoformat() + } + + async def _execute_tool_logic( + self, + tool_name: str, + parameters: Dict[str, Any], + user: User + ) -> Any: + """Execute tool logic""" + + if tool_name == "create_item": + return await self._create_item(parameters) + + elif tool_name == "search_items": + return await self._search_items(parameters) + + elif tool_name == "analyze_items": + return await self._analyze_items(parameters) + + elif tool_name == "manage_users": + return await self._manage_users(parameters, user) + + else: + raise ValueError(f"Tool '{tool_name}' implementation not found") + + async def _create_item(self, parameters: Dict[str, Any]) -> Dict[str, Any]: + """Create item tool implementation""" + from src.schemas.items import ItemCreate + + try: + item_create = ItemCreate(**parameters) + created_item = await self.item_crud.create(item_create) + + return { + "action": "create_item", + "item": created_item.dict(), + "message": f"Item '{created_item.name}' created successfully" + } + except Exception as e: + raise ValueError(f"Failed to create item: {str(e)}") + + async def _search_items(self, parameters: Dict[str, Any]) -> Dict[str, Any]: + """Search item tool implementation""" + query = parameters.get("query", "") + category = parameters.get("category") + min_price = parameters.get("min_price") + max_price = parameters.get("max_price") + limit = parameters.get("limit", 10) + + # Search logic implementation + all_items = await self.item_crud.get_all() + filtered_items = [] + + for item in all_items: + # Text search + if query.lower() not in item.name.lower() and query.lower() not in (item.description or "").lower(): + continue + + # Category filter + if category and getattr(item, 'category', None) != category: + continue + + # Price filter + if min_price is not None and item.price < min_price: + continue + if max_price is not None and item.price > max_price: + continue + + filtered_items.append(item) + + # Result limit + result_items = filtered_items[:limit] + + return { + "action": "search_items", + "query": query, + "total_found": len(filtered_items), + "returned_count": len(result_items), + "items": [item.dict() for item in result_items] + } + + async def _analyze_items(self, parameters: Dict[str, Any]) -> Dict[str, Any]: + """Analyze item tool implementation""" + analysis_type = parameters.get("analysis_type") + date_range = parameters.get("date_range", {}) + + all_items = await self.item_crud.get_all() + + if analysis_type == "price_distribution": + prices = [item.price for item in all_items] + if not prices: + return {"analysis": "price_distribution", "result": "No items found"} + + return { + "analysis": "price_distribution", + "result": { + "total_items": len(prices), + "min_price": min(prices), + "max_price": max(prices), + "average_price": sum(prices) / len(prices), + "price_ranges": { + "under_100k": len([p for p in prices if p < 100000]), + "100k_to_500k": len([p for p in prices if 100000 <= p < 500000]), + "500k_to_1m": len([p for p in prices if 500000 <= p < 1000000]), + "over_1m": len([p for p in prices if p >= 1000000]) + } + } + } + + elif analysis_type == "category_breakdown": + categories = {} + for item in all_items: + category = getattr(item, 'category', 'uncategorized') + categories[category] = categories.get(category, 0) + 1 + + return { + "analysis": "category_breakdown", + "result": { + "total_categories": len(categories), + "categories": categories + } + } + + else: + raise ValueError(f"Unknown analysis type: {analysis_type}") + + async def _manage_users(self, parameters: Dict[str, Any], requesting_user: User) -> Dict[str, Any]: + """Manage user tool implementation""" + action = parameters.get("action") + + # Check admin permissions + if Permission.MANAGE_USERS not in requesting_user.permissions: + raise ValueError("Insufficient permissions for user management") + + if action == "list": + users = [User(**user.dict()) for user in user_db.users.values()] + return { + "action": "list_users", + "total_users": len(users), + "users": [user.dict() for user in users] + } + + elif action == "create": + user_data = parameters.get("user_data", {}) + from src.auth.models import UserCreate + + user_create = UserCreate(**user_data) + created_user = user_db.create_user(user_create) + + return { + "action": "create_user", + "user": User(**created_user.dict()).dict(), + "message": f"User '{created_user.username}' created successfully" + } + + else: + raise ValueError(f"Unknown user management action: {action}") + +# Global MCP server instance +mcp_server = MCPServer() +``` + +## Schritt 6: MCP-API-Endpunkte implementieren + +### MCP-API-Router (`src/api/routes/mcp.py`) + +```python +from typing import Dict, Any, Optional +from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks +from pydantic import BaseModel + +from src.auth.dependencies import get_current_user_flexible, RequireMCPTools +from src.auth.models import User +from src.mcp.server import mcp_server +from src.mcp.tools import ToolCategory + +router = APIRouter(prefix="/mcp", tags=["MCP"]) + +class ToolExecuteRequest(BaseModel): + """Tool execution request""" + tool_name: str + parameters: Dict[str, Any] + session_id: Optional[str] = None + +class SessionCreateResponse(BaseModel): + """Session creation response""" + session_id: str + message: str + +@router.post("/session", response_model=SessionCreateResponse) +async def create_mcp_session( + current_user: User = Depends(RequireMCPTools) +): + """Create MCP session""" + session_id = await mcp_server.create_session(current_user) + + return SessionCreateResponse( + session_id=session_id, + message=f"MCP session created (User: {current_user.username})" + ) + +@router.delete("/session/{session_id}") +async def close_mcp_session( + session_id: str, + current_user: User = Depends(RequireMCPTools) +): + """Close MCP session""" + session = await mcp_server.get_session(session_id) + + if not session: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Session not found" + ) + + # Check session owner + if session["user_id"] != current_user.id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Cannot close another user's session" + ) + + await mcp_server.close_session(session_id) + + return {"message": "Session closed successfully"} + +@router.get("/tools") +async def list_mcp_tools( + category: Optional[ToolCategory] = None, + current_user: User = Depends(RequireMCPTools) +): + """List available MCP tools""" + tools = await mcp_server.list_tools(current_user) + + if category: + tools = [tool for tool in tools if tool["category"] == category] + + return { + "user": current_user.username, + "total_tools": len(tools), + "tools": tools + } + +@router.post("/execute") +async def execute_mcp_tool( + request: ToolExecuteRequest, + background_tasks: BackgroundTasks, + current_user: User = Depends(RequireMCPTools) +): + """Execute MCP tool""" + + # Check session (optional) + if request.session_id: + session = await mcp_server.get_session(request.session_id) + if not session: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Session not found" + ) + + if session["user_id"] != current_user.id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Cannot use another user's session" + ) + + # Execute tool + result = await mcp_server.execute_tool( + tool_name=request.tool_name, + parameters=request.parameters, + user=current_user, + session_id=request.session_id + ) + + # Log tool usage in background + background_tasks.add_task( + log_tool_usage, + current_user.id, + request.tool_name, + result["success"] + ) + + return result + +@router.get("/sessions") +async def list_user_sessions( + current_user: User = Depends(RequireMCPTools) +): + """List active user sessions""" + user_sessions = [] + + for session_id, session_data in mcp_server.active_sessions.items(): + if session_data["user_id"] == current_user.id: + user_sessions.append({ + "session_id": session_id, + "created_at": session_data["created_at"], + "tool_usage_count": session_data["tool_usage_count"], + "last_activity": session_data["last_activity"] + }) + + return { + "user": current_user.username, + "active_sessions": len(user_sessions), + "sessions": user_sessions + } + +@router.get("/stats") +async def get_mcp_stats( + current_user: User = Depends(RequireMCPTools) +): + """MCP usage statistics""" + total_sessions = len(mcp_server.active_sessions) + user_sessions = len([ + s for s in mcp_server.active_sessions.values() + if s["user_id"] == current_user.id + ]) + + return { + "user_stats": { + "username": current_user.username, + "active_sessions": user_sessions, + "permissions": [perm.value for perm in current_user.permissions] + }, + "server_stats": { + "total_active_sessions": total_sessions, + "available_tools": len(await mcp_server.list_tools(current_user)) + } + } + +async def log_tool_usage(user_id: str, tool_name: str, success: bool): + """Log tool usage (background job)""" + import logging + + logger = logging.getLogger("mcp.usage") + logger.info( + f"Tool usage - User: {user_id}, Tool: {tool_name}, Success: {success}" + ) +``` + +## Schritt 7: Anwendungsintegration und Tests + +### Hauptanwendung (`src/main.py`) + +```python +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from src.auth.routes import router as auth_router +from src.api.routes.items import router as items_router +from src.api.routes.mcp import router as mcp_router +from src.core.config import settings + +app = FastAPI( + title="AI Integrated API", + description="AI model integrated MCP-based API server", + version="1.0.0" +) + +# CORS settings +app.add_middleware( + CORSMiddleware, + allow_origins=settings.ALLOWED_HOSTS, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Include routers +app.include_router(auth_router) +app.include_router(items_router, prefix="/api/v1") +app.include_router(mcp_router, prefix="/api/v1") + +@app.get("/") +async def root(): + return { + "message": "AI Integrated API with MCP Support", + "version": "1.0.0", + "endpoints": { + "authentication": "/auth", + "items": "/api/v1/items", + "mcp": "/api/v1/mcp", + "docs": "/docs" + } + } + +@app.get("/health") +async def health_check(): + """Health check endpoint""" + return { + "status": "healthy", + "version": "1.0.0", + "services": { + "auth": "operational", + "mcp": "operational", + "database": "operational" + } + } +``` + +### Server starten und testen + +
+ +```console +$ cd ai-integrated-api +$ fastkit runserver +Starting FastAPI server at 127.0.0.1:8000... + +# User login +$ curl -X POST "http://localhost:8000/auth/login" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "username=admin&password=admin123" + +{ + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "token_type": "bearer", + "expires_in": 1800, + "user": { + "id": "123e4567-e89b-12d3-a456-426614174000", + "email": "admin@example.com", + "username": "admin", + "role": "admin", + "permissions": ["read:items", "write:items", ...] + } +} + +# Create MCP session +$ curl -X POST "http://localhost:8000/api/v1/mcp/session" \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" + +{ + "session_id": "abc123-def456-ghi789", + "message": "MCP session created (User: admin)" +} + +# List available tools +$ curl "http://localhost:8000/api/v1/mcp/tools" \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" + +{ + "user": "admin", + "total_tools": 4, + "tools": [ + { + "name": "create_item", + "description": "Create a new item", + "category": "data_management", + "parameters": {...}, + "examples": [...] + }, + ... + ] +} + +# Execute MCP tool (create item) +$ curl -X POST "http://localhost:8000/api/v1/mcp/execute" \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "tool_name": "create_item", + "parameters": { + "name": "AI generated item", + "description": "MCP through AI generated item", + "price": 500000, + "category": "ai_generated" + }, + "session_id": "abc123-def456-ghi789" + }' + +{ + "success": true, + "tool": "create_item", + "result": { + "action": "create_item", + "item": { + "id": 1, + "name": "AI generated item", + "description": "MCP through AI generated item", + "price": 500000, + "category": "ai_generated", + "created_at": "2024-01-01T12:00:00Z" + }, + "message": "Item 'AI generated item' created successfully" + }, + "timestamp": "2024-01-01T12:00:00.123456Z" +} + +# Execute MCP tool (search item) +$ curl -X POST "http://localhost:8000/api/v1/mcp/execute" \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "tool_name": "search_items", + "parameters": { + "query": "AI", + "limit": 5 + } + }' +``` + +
+ +## Schritt 8: Beispiel eines KI-Clients + +### Python-MCP-Client-Beispiel + +```python +# client_example.py +import asyncio +import aiohttp +from typing import Dict, Any, List + +class MCPClient: + """MCP client example""" + + def __init__(self, base_url: str, api_key: str): + self.base_url = base_url + self.api_key = api_key + self.session_id = None + self.session = None + + async def __aenter__(self): + self.session = aiohttp.ClientSession( + headers={"X-API-Key": self.api_key} + ) + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + if self.session_id: + await self.close_session() + if self.session: + await self.session.close() + + async def create_session(self) -> str: + """Create MCP session""" + async with self.session.post(f"{self.base_url}/api/v1/mcp/session") as resp: + data = await resp.json() + self.session_id = data["session_id"] + return self.session_id + + async def close_session(self): + """Close MCP session""" + if self.session_id: + async with self.session.delete(f"{self.base_url}/api/v1/mcp/session/{self.session_id}"): + pass + self.session_id = None + + async def list_tools(self) -> List[Dict[str, Any]]: + """List available tools""" + async with self.session.get(f"{self.base_url}/api/v1/mcp/tools") as resp: + data = await resp.json() + return data["tools"] + + async def execute_tool(self, tool_name: str, parameters: Dict[str, Any]) -> Dict[str, Any]: + """Execute tool""" + payload = { + "tool_name": tool_name, + "parameters": parameters, + "session_id": self.session_id + } + + async with self.session.post( + f"{self.base_url}/api/v1/mcp/execute", + json=payload + ) as resp: + return await resp.json() + + async def ai_assistant_workflow(self, user_request: str) -> str: + """AI assistant workflow simulation""" + + # 1. Create session + await self.create_session() + print(f"Session created: {self.session_id}") + + # 2. Analyze user request and select appropriate tool + if "Create item" in user_request or "Create" in user_request: + # Create item request + result = await self.execute_tool("create_item", { + "name": "AI recommended item", + "description": "AI generated item based on user request", + "price": 100000, + "category": "ai_recommended" + }) + + if result["success"]: + item_name = result["result"]["item"]["name"] + return f"✅ '{item_name}' item created successfully!" + else: + return f"❌ Item creation failed: {result.get('error', 'Unknown error')}" + + elif "Search" in user_request or "Find" in user_request: + # Search request + search_query = "Item" # Actually extracted from NLP + result = await self.execute_tool("search_items", { + "query": search_query, + "limit": 5 + }) + + if result["success"]: + items = result["result"]["items"] + item_list = "\n".join([f"- {item['name']} (₩{item['price']:,})" for item in items]) + return f"🔍 Search results ({len(items)} items):\n{item_list}" + else: + return f"❌ Search failed: {result.get('error', 'Unknown error')}" + + elif "Analyze" in user_request: + # Analyze request + result = await self.execute_tool("analyze_items", { + "analysis_type": "price_distribution" + }) + + if result["success"]: + analysis = result["result"]["result"] + return f"📊 Price analysis:\nAverage price: ₩{analysis['average_price']:,.0f}\nMinimum: ₩{analysis['min_price']:,} - Maximum: ₩{analysis['max_price']:,}" + else: + return f"❌ Analysis failed: {result.get('error', 'Unknown error')}" + + else: + return "Sorry, I couldn't find a tool to handle that request." + +async def main(): + """Client test""" + async with MCPClient("http://localhost:8000", "your-api-key-here") as client: + + # List available tools + tools = await client.list_tools() + print(f"Available tools: {len(tools)}") + for tool in tools: + print(f"- {tool['name']}: {tool['description']}") + + print("\n" + "="*50 + "\n") + + # AI assistant simulation + test_requests = [ + "Create a new item", + "Search for items", + "Analyze price distribution" + ] + + for request in test_requests: + print(f"User request: {request}") + response = await client.ai_assistant_workflow(request) + print(f"AI response: {response}") + print("-" * 30) + +if __name__ == "__main__": + asyncio.run(main()) +``` + + + + + +## Zusammenfassung + +In diesem Tutorial haben wir die MCP-Integration (Model Context Protocol) umgesetzt mit: + +- ✅ Aufbau eines JWT-basierten Authentifizierungssystems +- ✅ Implementierung rollenbasierter Zugriffskontrolle (RBAC) +- ✅ Implementierung eines MCP-Servers und Werkzeugsystems +- ✅ Sitzungsbasierter Kontextverwaltung +- ✅ Sicherer API-Kommunikation mit KI-Modellen +- ✅ Berechtigungsverwaltung und Nutzungsverfolgung für Werkzeuge +- ✅ Implementierung eines konkreten KI-Client-Beispiels + +Jetzt können Sie ein vollständiges MCP-basiertes System aufbauen, in dem KI-Modelle Ihre API-Funktionen sicher und effizient nutzen können! diff --git a/docs/de/user-guide/adding-routes.md b/docs/de/user-guide/adding-routes.md new file mode 100644 index 0000000..c8c2c7c --- /dev/null +++ b/docs/de/user-guide/adding-routes.md @@ -0,0 +1,581 @@ +# Routen hinzufügen + +Lernen Sie, wie Sie neue API-Routen zu Ihrem bestehenden FastAPI-Projekt hinzufügen. + +## Grundlegendes Hinzufügen einer Route + +### Den Befehl `addroute` verwenden + +Der Befehl `addroute` von FastAPI-fastkit erleichtert das Hinzufügen neuer Routen: + +
+ +```console +$ fastkit addroute users my-awesome-api + Adding New Route +┌──────────────────┬──────────────────────────────────────────┐ +│ Project │ my-awesome-api │ +│ Route Name │ users │ +│ Target Directory │ ~/my-awesome-api │ +└──────────────────┴──────────────────────────────────────────┘ + +Do you want to add route 'users' to project 'my-awesome-api'? [Y/n]: y + +╭──────────────────────── Info ────────────────────────╮ +│ ℹ Updated main.py to include the API router │ +╰──────────────────────────────────────────────────────╯ +╭─────────────────────── Success ───────────────────────╮ +│ ✨ Successfully added new route 'users' to project │ +│ `my-awesome-api` │ +╰───────────────────────────────────────────────────────╯ +``` + +
+ +## Was wird erstellt + +Wenn Sie eine Route hinzufügen, erzeugt FastAPI-fastkit automatisch: + +### 1. Routen-Datei: `src/api/routes/users.py` + +```python +from typing import List +from fastapi import APIRouter, HTTPException, status +from src.schemas.users import User, UserCreate, UserUpdate +from src.crud.users import users_crud + +router = APIRouter() + +@router.get("/", response_model=List[User]) +def read_users(): + """Get all users""" + return users_crud.get_all() + +@router.post("/", response_model=User, status_code=status.HTTP_201_CREATED) +def create_user(user: UserCreate): + """Create a new user""" + return users_crud.create(user) + +@router.get("/{user_id}", response_model=User) +def read_user(user_id: int): + """Get a specific user""" + user = users_crud.get_by_id(user_id) + if user is None: + raise HTTPException(status_code=404, detail="User not found") + return user + +@router.put("/{user_id}", response_model=User) +def update_user(user_id: int, user: UserUpdate): + """Update a user""" + updated_user = users_crud.update(user_id, user) + if updated_user is None: + raise HTTPException(status_code=404, detail="User not found") + return updated_user + +@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_user(user_id: int): + """Delete a user""" + success = users_crud.delete(user_id) + if not success: + raise HTTPException(status_code=404, detail="User not found") +``` + +### 2. CRUD-Operationen: `src/crud/users.py` + +```python +from typing import List, Optional +from src.schemas.users import User, UserCreate, UserUpdate + +class UsersCRUD: + def __init__(self): + self._users: List[User] = [] + self._next_id = 1 + + def get_all(self) -> List[User]: + """Get all users""" + return self._users + + def get_by_id(self, user_id: int) -> Optional[User]: + """Get user by ID""" + return next((user for user in self._users if user.id == user_id), None) + + def create(self, user: UserCreate) -> User: + """Create a new user""" + new_user = User( + id=self._next_id, + title=user.title, + description=user.description + ) + self._next_id += 1 + self._users.append(new_user) + return new_user + + def update(self, user_id: int, user: UserUpdate) -> Optional[User]: + """Update an existing user""" + existing_user = self.get_by_id(user_id) + if existing_user: + update_data = user.dict(exclude_unset=True) + for field, value in update_data.items(): + setattr(existing_user, field, value) + return existing_user + return None + + def delete(self, user_id: int) -> bool: + """Delete a user""" + user = self.get_by_id(user_id) + if user: + self._users.remove(user) + return True + return False + +users_crud = UsersCRUD() +``` + +### 3. Pydantic-Schemas: `src/schemas/users.py` + +```python +from typing import Optional +from pydantic import BaseModel + +class UserBase(BaseModel): + title: str + description: Optional[str] = None + +class UserCreate(UserBase): + pass + +class UserUpdate(BaseModel): + title: Optional[str] = None + description: Optional[str] = None + +class User(UserBase): + id: int + + class Config: + from_attributes = True +``` + +### 4. Router-Registrierung + +Der Befehl aktualisiert `src/api/api.py` automatisch, um den neuen Router einzubinden: + +```python +from fastapi import APIRouter +from src.api.routes import items, users + +api_router = APIRouter() + +api_router.include_router(items.router, prefix="/items", tags=["items"]) +api_router.include_router(users.router, prefix="/users", tags=["users"]) +``` + +## Generierte API-Endpunkte + +Nach dem Hinzufügen der Route `users` stehen Ihnen folgende Endpunkte zur Verfügung: + +| Methode | Endpunkt | Beschreibung | +|--------|----------|-------------| +| `GET` | `/api/v1/users/` | Alle Nutzer abrufen | +| `POST` | `/api/v1/users/` | Einen neuen Nutzer anlegen | +| `GET` | `/api/v1/users/{user_id}` | Einen bestimmten Nutzer abrufen | +| `PUT` | `/api/v1/users/{user_id}` | Einen Nutzer aktualisieren | +| `DELETE` | `/api/v1/users/{user_id}` | Einen Nutzer löschen | + +## Ihre neuen Routen testen + +### 1. Server starten + +
+ +```console +$ fastkit runserver +INFO: Uvicorn running on http://127.0.0.1:8000 +``` + +
+ +### 2. API-Dokumentation prüfen + +Besuchen Sie [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs), um Ihre neuen Endpunkte in der interaktiven Dokumentation zu sehen. + +### 3. Mit curl testen + +**Einen Nutzer anlegen:** +
+ +```console +$ curl -X POST "http://127.0.0.1:8000/api/v1/users/" \ + -H "Content-Type: application/json" \ + -d '{"title": "John Doe", "description": "Software Developer"}' + +{ + "id": 1, + "title": "John Doe", + "description": "Software Developer" +} +``` + +
+ +**Alle Nutzer abrufen:** +
+ +```console +$ curl http://127.0.0.1:8000/api/v1/users/ + +[ + { + "id": 1, + "title": "John Doe", + "description": "Software Developer" + } +] +``` + +
+ +**Einen bestimmten Nutzer abrufen:** +
+ +```console +$ curl http://127.0.0.1:8000/api/v1/users/1 + +{ + "id": 1, + "title": "John Doe", + "description": "Software Developer" +} +``` + +
+ +## Generierten Code anpassen + +Der generierte Code ist vollständig anpassbar. Hier sind häufige Anpassungen: + +### 1. Erweitertes Nutzer-Schema + +Bearbeiten Sie `src/schemas/users.py` für realistischere Nutzerdaten: + +```python +from typing import Optional +from datetime import datetime +from pydantic import BaseModel, EmailStr, Field + +class UserBase(BaseModel): + email: EmailStr + username: str = Field(..., min_length=3, max_length=50) + full_name: Optional[str] = None + is_active: bool = True + +class UserCreate(UserBase): + password: str = Field(..., min_length=8) + +class UserUpdate(BaseModel): + email: Optional[EmailStr] = None + username: Optional[str] = Field(None, min_length=3, max_length=50) + full_name: Optional[str] = None + is_active: Optional[bool] = None + +class User(UserBase): + id: int + created_at: datetime + + class Config: + from_attributes = True + +class UserInDB(User): + hashed_password: str +``` + +### 2. CRUD mit Validierung erweitern + +Aktualisieren Sie `src/crud/users.py` um bessere Validierung: + +```python +from typing import List, Optional +from datetime import datetime +import hashlib +from src.schemas.users import UserCreate, UserUpdate, UserInDB + +class UsersCRUD: + def __init__(self): + self._users: List[UserInDB] = [] + self._next_id = 1 + + def _hash_password(self, password: str) -> str: + """Simple password hashing (use bcrypt in production)""" + return hashlib.sha256(password.encode()).hexdigest() + + def get_by_email(self, email: str) -> Optional[UserInDB]: + """Get user by email""" + return next((user for user in self._users if user.email == email), None) + + def get_by_username(self, username: str) -> Optional[UserInDB]: + """Get user by username""" + return next((user for user in self._users if user.username == username), None) + + def create(self, user: UserCreate) -> UserInDB: + """Create a new user with validation""" + # Check for duplicates + if self.get_by_email(user.email): + raise ValueError("Email already registered") + if self.get_by_username(user.username): + raise ValueError("Username already taken") + + new_user = UserInDB( + id=self._next_id, + email=user.email, + username=user.username, + full_name=user.full_name, + is_active=user.is_active, + created_at=datetime.now(), + hashed_password=self._hash_password(user.password) + ) + self._next_id += 1 + self._users.append(new_user) + return new_user + +users_crud = UsersCRUD() +``` + +### 3. Route mit Fehlerbehandlung erweitern + +Aktualisieren Sie `src/api/routes/users.py` um eine bessere Fehlerbehandlung: + +```python +from typing import List +from fastapi import APIRouter, HTTPException, status +from src.schemas.users import User, UserCreate, UserUpdate +from src.crud.users import users_crud + +router = APIRouter() + +@router.post("/", response_model=User, status_code=status.HTTP_201_CREATED) +def create_user(user: UserCreate): + """Create a new user""" + try: + new_user = users_crud.create(user) + # Return user without password hash + return User(**new_user.dict()) + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + +@router.get("/{user_id}", response_model=User) +def read_user(user_id: int): + """Get a specific user""" + user = users_crud.get_by_id(user_id) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"User with id {user_id} not found" + ) + return User(**user.dict()) +``` + +## Mehrere Routen hinzufügen + +Sie können mehrere Routen hinzufügen, um eine vollständige API aufzubauen: + +
+ +```console +# Weitere Ressourcenrouten hinzufügen (zuerst der Routenname, dann das Projektverzeichnis) +$ fastkit addroute products my-awesome-api +$ fastkit addroute orders my-awesome-api +$ fastkit addroute categories my-awesome-api + +# Jede davon erzeugt die vollständige CRUD-Struktur +``` + +
+ +Dies erzeugt eine umfassende API mit: + +- `/api/v1/users/` — Nutzerverwaltung +- `/api/v1/products/` — Produktkatalog +- `/api/v1/orders/` — Bestellabwicklung +- `/api/v1/categories/` — Kategorienverwaltung + +## Routenorganisation + +### Verwandte Endpunkte gruppieren + +Sie können Routen nach Domäne organisieren: + +```python +# src/api/api.py +from fastapi import APIRouter +from src.api.routes import users, products, orders, categories + +api_router = APIRouter() + +# User management +api_router.include_router( + users.router, + prefix="/users", + tags=["User Management"] +) + +# E-commerce +api_router.include_router( + products.router, + prefix="/products", + tags=["E-commerce"] +) +api_router.include_router( + orders.router, + prefix="/orders", + tags=["E-commerce"] +) +api_router.include_router( + categories.router, + prefix="/categories", + tags=["E-commerce"] +) +``` + +### Routen-Abhängigkeiten hinzufügen + +Fügen Sie Authentifizierung oder andere Abhängigkeiten hinzu: + +```python +from fastapi import APIRouter, Depends +from src.core.auth import get_current_user + +router = APIRouter() + +@router.get("/profile", response_model=User) +def get_user_profile(current_user: User = Depends(get_current_user)): + """Get current user's profile""" + return current_user + +@router.post("/", response_model=User) +def create_user( + user: UserCreate, + current_user: User = Depends(get_current_user) +): + """Create a new user (admin only)""" + if not current_user.is_admin: + raise HTTPException(status_code=403, detail="Admin access required") + return users_crud.create(user) +``` + +## Best Practices + +### 1. Einheitliche Benennung + +Halten Sie sich an einheitliche Benennungskonventionen: + +- **Routennamen**: Substantive im Plural verwenden (`users`, `products`, `orders`) +- **Schema-Namen**: Singular verwenden (`User`, `Product`, `Order`) +- **CRUD-Klassen**: mit `CRUD` enden (`UsersCRUD`, `ProductsCRUD`) + +### 2. Fehlerbehandlung + +Behandeln Sie Fehler immer sauber: + +```python +@router.post("/", response_model=User) +def create_user(user: UserCreate): + try: + return users_crud.create(user) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + raise HTTPException(status_code=500, detail="Internal server error") +``` + +### 3. Dokumentation + +Schreiben Sie ausführliche Docstrings: + +```python +@router.get("/{user_id}", response_model=User) +def read_user(user_id: int): + """ + Get a specific user by ID. + + Args: + user_id: The unique identifier for the user + + Returns: + User: The user object with all details + + Raises: + HTTPException: 404 if user not found + """ + user = users_crud.get_by_id(user_id) + if not user: + raise HTTPException(status_code=404, detail="User not found") + return user +``` + +### 4. Testen + +Testen Sie Ihre neuen Routen immer: + +```python +# tests/test_users.py +from fastapi.testclient import TestClient +from src.main import app + +client = TestClient(app) + +def test_create_user(): + user_data = { + "email": "test@example.com", + "username": "testuser", + "password": "securepassword123" + } + response = client.post("/api/v1/users/", json=user_data) + assert response.status_code == 201 + assert response.json()["email"] == user_data["email"] + +def test_get_user(): + response = client.get("/api/v1/users/1") + assert response.status_code == 200 +``` + +## Fehlerbehebung + +### Route erscheint nicht + +Wenn Ihre Route nicht in der API-Dokumentation auftaucht: + +1. **Router-Registrierung prüfen** in `src/api/api.py` +2. **Server neu starten** nach dem Hinzufügen von Routen +3. **Auf Import-Fehler** in der Routen-Datei prüfen + +### Import-Fehler + +Wenn Sie Import-Fehler erhalten: + +1. **Dateistruktur prüfen** — sie sollte dem erwarteten Layout entsprechen +2. **Schema-Imports** in Routen- und CRUD-Dateien überprüfen +3. **Sicherstellen, dass alle `__init__.py`-Dateien existieren** + +### Server startet nicht + +Wenn der Server nach dem Hinzufügen von Routen nicht startet: + +1. **Auf Syntaxfehler** in den generierten Dateien prüfen +2. **Schema-Kompatibilität** zwischen Dateien überprüfen +3. **Logs prüfen** auf konkrete Fehlermeldungen + +## Nächste Schritte + +Jetzt, da Sie Routen hinzufügen können: + +1. **[Ihr erstes Projekt](../tutorial/first-project.md)**: bauen Sie eine vollständige Blog-API +2. **[CLI-Referenz](cli-reference.md)**: lernen Sie alle verfügbaren Befehle +3. **[Vorlagen verwenden](using-templates.md)**: erkunden Sie vorgefertigte Projektvorlagen + +!!! tip "Tipps zur Routenentwicklung" + - Testen Sie neue Routen immer in der interaktiven Doku (`/docs`) + - Verwenden Sie aussagekräftige HTTP-Statuscodes + - Implementieren Sie eine ordentliche Fehlerbehandlung für alle Endpunkte + - Halten Sie Routen-Handler schlank und delegieren Sie Geschäftslogik an CRUD-Klassen diff --git a/docs/de/user-guide/choosing-a-starter.md b/docs/de/user-guide/choosing-a-starter.md new file mode 100644 index 0000000..c89694d --- /dev/null +++ b/docs/de/user-guide/choosing-a-starter.md @@ -0,0 +1,145 @@ +# Welchen Starter soll ich wählen? + +FastAPI-fastkit bietet mehrere Wege, ein Projekt zu starten. Diese Seite ist eine **Entscheidungshilfe** für Einsteiger: Wählen Sie hier einen Pfad und springen Sie dann zum [Schnellstart](quick-start.md), um das Projekt tatsächlich zu erstellen. + +Wenn Sie unsicher sind, lautet die kurze Antwort: + +> **Beginnen Sie mit `fastkit init --interactive` und wählen Sie das Preset `domain-starter`.** Es ist der empfohlene Standard für moderne API-Projekte. + +Der Rest dieser Seite erklärt, warum das so ist und wann eine andere Wahl sinnvoller sein kann. + +## TL;DR — Wahl nach Benutzertyp + +| Sie sind … | Beginnen Sie mit | +|---|---| +| Neu bei FastAPI und möchten eine geführte Tour | `fastkit init --interactive` (Preset: **`domain-starter`**) | +| Möchten eine funktionierende CRUD-Demo zum Lesen und Anpassen | `fastkit startdemo fastapi-default` | +| Möchten das kleinstmögliche Gerüst | `fastkit init --interactive` (Preset: **`minimal`**) | +| Schreiben einen schnellen Prototyp / ein Single-File-Skript | `fastkit init --interactive` (Preset: **`single-module`**) | +| Brauchen eine echte Datenbank (PostgreSQL + SQLAlchemy + Alembic) | `fastkit startdemo fastapi-psql-orm` | +| Möchten ein produktionsnahes domänenorientiertes Layout für eine mittelgroße API | `fastkit init --interactive` (Preset: **`domain-starter`**) | + +## `startdemo` vs. `init --interactive` — wo ist der Unterschied? + +Dies sind die beiden Haupteinstiegspunkte. Sie decken unterschiedliche Bedürfnisse ab. + +### `fastkit startdemo