feat(C.5): Modularer Import/Export — Shared Helpers, Preview/Mapping, Background Jobs, Partial-Failure

C5-BASE: app/services/import_export_helpers.py (NEU, 352 Zeilen)
- parse_csv/json/xlsx, write_csv/json/xlsx, map_fields, suggest_mapping
- validate_row, build_error_report, build_import_result, detect_format

C5-PREVIEW: POST /import/preview + POST /import/validate
- Preview gibt erste 10 Zeilen + Spalten + Mapping-Vorschlag
- Validate gibt Fehler-Report ohne Import

C5-JOB: app/services/import_export_jobs.py (NEU, 165 Zeilen)
- ARQ Background Job für Files >1000 Zeilen
- Job-Status: pending/processing/completed/partial_success/failed
- GET /import/status/{job_id} — Status + Progress + Fehler-Report
- Partial-Failure: try/except pro Zeile, fehlerhafte gesammelt, erfolgreiche committet

C5-CONTACT+C5-COMPANY: Handler auf Shared Helpers umgestellt
C5-UI: ImportWizard.tsx (5 Steps: Upload→Preview→Validation→Review→Result)
C5-TEST: 45 Tests in test_import_export.py — alle grün
C5-DOC: Plugin-Dev-Guide Kapitel 31 (Import/Export Handler)
This commit is contained in:
Agent Zero
2026-08-13 22:43:33 +02:00
parent fd14e0076b
commit e7ae0ad5ce
10 changed files with 2630 additions and 439 deletions
+109
View File
@@ -2092,4 +2092,113 @@ Plugins deklarieren ihre API-Prefixe im Manifest (`routes.prefix`). Plugin-API-
---
## 31. Import/Export Handler auf Shared Helpers
LeoCRM stellt wiederverwendbare Bausteine für Import/Export-Funktionalität bereit. Plugins können eigene Import/Export-Handler auf dieser Basis aufsetzen.
### 31.1 Shared Helpers (`app/services/import_export_helpers.py`)
Die folgenden Funktionen sind verfügbar und können von jedem Plugin importiert werden:
```python
from app.services.import_export_helpers import (
parse_csv, # CSV → list[dict] mit Encoding-Detection
parse_json, # JSON → list[dict]
parse_xlsx, # XLSX → list[dict] (openpyxl)
write_csv, # list[dict] → CSV bytes
write_json, # list[dict] → JSON bytes
write_xlsx, # list[dict] → XLSX bytes
map_fields, # Source-Column → Target-Field Mapping
suggest_mapping, # Auto-Mapping-Vorschlag
validate_row, # Zeilen-Validierung mit required + validators
build_error_report, # Strukturierter Fehler-Report
build_import_result, # Standardisiertes Import-Ergebnis
detect_format, # Format-Erkennung (csv/json/xlsx)
parse_file, # Auto-Detect + Parse
)
```
### 31.2 Eigener Import-Handler
Ein Plugin kann einen eigenen Import-Handler erstellen:
```python
from app.services.import_export_helpers import parse_file, map_fields, validate_row, build_import_result
async def import_my_entity(db, tenant_id, user_id, content: bytes, filename: str, field_mapping: dict | None = None):
rows = parse_file(filename, content)
total = len(rows)
errors = []
valid_rows = []
for idx, row in enumerate(rows, start=1):
if field_mapping:
row = map_fields(row, field_mapping)
row_errors = validate_row(row, required=["name"], validators={"email": {"type": "email"}})
if row_errors:
for e in row_errors:
errors.append({"row": idx, "field": "", "message": e})
else:
valid_rows.append(row)
# ... DB-Insert mit Partial-Failure ...
for idx, row in enumerate(valid_rows, start=1):
try:
# Insert entity
pass
except Exception as exc:
errors.append({"row": idx, "field": "", "message": str(exc)})
failed_row_count = len({e["row"] for e in errors})
return build_import_result(
total=total,
succeeded=len(valid_rows),
failed=failed_row_count,
errors=errors,
)
```
### 31.3 Partial-Failure-Semantik
Import-Handler müssen Partial-Failure implementieren:
1. **Validierung pro Zeile**: `validate_row()` prüft required fields und validators
2. **Fehlerhafte Zeilen sammeln**: Fehler werden in `errors`-Liste mit `{row, field, message}` gesammelt
3. **Erfolgreiche Zeilen committen**: Gültige Zeilen werden in DB geschrieben, pro Zeile try/except
4. **Status-Klassifizierung**: `build_import_result()` setzt Status auf `success`, `partial_success`, oder `failed`
5. **Fehler-Report**: `build_error_report()` erstellt strukturierten Report mit `total_errors` und `errors`-Liste
### 31.4 Background Processing für große Imports
Für Dateien > 1000 Zeilen soll der Import als ARQ-Job laufen:
```python
from app.services.import_export_jobs import create_import_job, get_import_job_status
# In Route:
if len(rows) > 1000:
job_id = await create_import_job(
entity_type="my_entity",
csv_content=content.decode("utf-8"),
tenant_id=tenant_id,
user_id=user_id,
field_mapping=mapping,
)
return {"status": "pending", "job_id": job_id}
```
Job-Status wird in Redis gespeichert (`leocrm:import_job:{job_id}`) mit TTL 1h.
### 31.5 Frontend-Integration
Das Frontend nutzt die API-Client-Funktionen aus `importExport.ts`:
- `previewImport(file, entityType)` — Vorschau mit Mapping-Vorschlag
- `validateImport(file, entityType, fieldMapping)` — Validierung ohne Import
- `importCsv(file, entityType, dryRun, fieldMapping)` — Import mit optionalem Mapping
- `getImportJobStatus(jobId)` — Polling für Background-Jobs
- `exportData(entityType, format)` — Export als CSV/XLSX/JSON
---
*This document is authoritative for all plugin development at LeoCRM.*