docker.dateiverwaltung/backend/app/routes/api.py

851 lines
26 KiB
Python

"""
API Routes - Getrennte Bereiche: Mail-Abruf und Datei-Sortierung
"""
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from typing import List, Optional
from pydantic import BaseModel
from datetime import datetime
from pathlib import Path
import json
import asyncio
from ..models.database import get_db, Postfach, QuellOrdner, SortierRegel, VerarbeiteteDatei, VerarbeiteteMail
from ..modules.mail_fetcher import MailFetcher
from ..modules.pdf_processor import PDFProcessor
from ..modules.sorter import Sorter
router = APIRouter(prefix="/api", tags=["api"])
# ============ Pydantic Models ============
class PostfachCreate(BaseModel):
name: str
imap_server: str
imap_port: int = 993
email: str
passwort: str
ordner: str = "INBOX"
alle_ordner: bool = False # Alle IMAP-Ordner durchsuchen
nur_ungelesen: bool = False # Nur ungelesene Mails (False = alle)
ziel_ordner: str
erlaubte_typen: List[str] = [".pdf"]
max_groesse_mb: int = 25
class PostfachResponse(BaseModel):
id: int
name: str
imap_server: str
email: str
ordner: str
alle_ordner: bool
nur_ungelesen: bool
ziel_ordner: str
erlaubte_typen: List[str]
max_groesse_mb: int
letzter_abruf: Optional[datetime]
letzte_anzahl: int
class Config:
from_attributes = True
class OrdnerCreate(BaseModel):
name: str
pfad: str
ziel_ordner: str
rekursiv: bool = True
dateitypen: List[str] = [".pdf", ".jpg", ".jpeg", ".png", ".tiff"]
class OrdnerResponse(BaseModel):
id: int
name: str
pfad: str
ziel_ordner: str
rekursiv: bool
dateitypen: List[str]
aktiv: bool
class Config:
from_attributes = True
class RegelCreate(BaseModel):
name: str
prioritaet: int = 100
muster: dict = {}
extraktion: dict = {}
schema: str = "{datum} - Dokument.pdf"
unterordner: Optional[str] = None
class RegelResponse(BaseModel):
id: int
name: str
prioritaet: int
aktiv: bool
muster: dict
extraktion: dict
schema: str
unterordner: Optional[str]
class Config:
from_attributes = True
class RegelTestRequest(BaseModel):
regel: dict
text: str
# ============ Verzeichnis-Browser ============
@router.get("/browse")
def browse_directory(path: str = "/"):
"""Listet Verzeichnisse für File-Browser"""
import os
# Sicherheit: Nur bestimmte Basispfade erlauben
allowed_bases = ["/srv", "/home", "/mnt", "/media", "/data", "/tmp"]
path = os.path.abspath(path)
# Prüfen ob Pfad erlaubt
is_allowed = any(path.startswith(base) for base in allowed_bases) or path == "/"
if not is_allowed:
return {"error": "Pfad nicht erlaubt", "entries": []}
if not os.path.exists(path):
return {"error": "Pfad existiert nicht", "entries": []}
if not os.path.isdir(path):
return {"error": "Kein Verzeichnis", "entries": []}
try:
entries = []
for entry in sorted(os.listdir(path)):
full_path = os.path.join(path, entry)
if os.path.isdir(full_path):
entries.append({
"name": entry,
"path": full_path,
"type": "directory"
})
return {
"current": path,
"parent": os.path.dirname(path) if path != "/" else None,
"entries": entries
}
except PermissionError:
return {"error": "Zugriff verweigert", "entries": []}
# ============ BEREICH 1: Postfächer ============
@router.get("/postfaecher", response_model=List[PostfachResponse])
def liste_postfaecher(db: Session = Depends(get_db)):
return db.query(Postfach).all()
@router.post("/postfaecher", response_model=PostfachResponse)
def erstelle_postfach(data: PostfachCreate, db: Session = Depends(get_db)):
postfach = Postfach(**data.dict())
db.add(postfach)
db.commit()
db.refresh(postfach)
return postfach
@router.put("/postfaecher/{id}", response_model=PostfachResponse)
def aktualisiere_postfach(id: int, data: PostfachCreate, db: Session = Depends(get_db)):
postfach = db.query(Postfach).filter(Postfach.id == id).first()
if not postfach:
raise HTTPException(status_code=404, detail="Nicht gefunden")
update_data = data.dict()
# Passwort nur aktualisieren wenn nicht leer
if not update_data.get("passwort"):
del update_data["passwort"]
for key, value in update_data.items():
setattr(postfach, key, value)
db.commit()
db.refresh(postfach)
return postfach
@router.delete("/postfaecher/{id}")
def loesche_postfach(id: int, db: Session = Depends(get_db)):
postfach = db.query(Postfach).filter(Postfach.id == id).first()
if not postfach:
raise HTTPException(status_code=404, detail="Nicht gefunden")
db.delete(postfach)
db.commit()
return {"message": "Gelöscht"}
@router.post("/postfaecher/{id}/test")
def teste_postfach(id: int, db: Session = Depends(get_db)):
postfach = db.query(Postfach).filter(Postfach.id == id).first()
if not postfach:
raise HTTPException(status_code=404, detail="Nicht gefunden")
fetcher = MailFetcher({
"imap_server": postfach.imap_server,
"imap_port": postfach.imap_port,
"email": postfach.email,
"passwort": postfach.passwort,
"ordner": postfach.ordner
})
return fetcher.test_connection()
@router.get("/postfaecher/{id}/abrufen/stream")
def rufe_postfach_ab_stream(id: int, db: Session = Depends(get_db)):
"""Streaming-Endpoint für Mail-Abruf mit Live-Updates"""
postfach = db.query(Postfach).filter(Postfach.id == id).first()
if not postfach:
raise HTTPException(status_code=404, detail="Nicht gefunden")
# Daten kopieren für Generator (Session ist nach return nicht mehr verfügbar)
pf_data = {
"id": postfach.id,
"name": postfach.name,
"imap_server": postfach.imap_server,
"imap_port": postfach.imap_port,
"email": postfach.email,
"passwort": postfach.passwort,
"ordner": postfach.ordner,
"alle_ordner": postfach.alle_ordner,
"erlaubte_typen": postfach.erlaubte_typen,
"max_groesse_mb": postfach.max_groesse_mb,
"ziel_ordner": postfach.ziel_ordner
}
# Bereits verarbeitete Message-IDs laden
bereits_verarbeitet = set(
row.message_id for row in
db.query(VerarbeiteteMail.message_id)
.filter(VerarbeiteteMail.postfach_id == id)
.all()
)
def event_generator():
from ..models.database import SessionLocal
def send_event(data):
return f"data: {json.dumps(data)}\n\n"
yield send_event({"type": "start", "postfach": pf_data["name"], "bereits_verarbeitet": len(bereits_verarbeitet)})
# Zielordner erstellen
ziel = Path(pf_data["ziel_ordner"])
ziel.mkdir(parents=True, exist_ok=True)
fetcher = MailFetcher({
"imap_server": pf_data["imap_server"],
"imap_port": pf_data["imap_port"],
"email": pf_data["email"],
"passwort": pf_data["passwort"],
"ordner": pf_data["ordner"],
"erlaubte_typen": pf_data["erlaubte_typen"],
"max_groesse_mb": pf_data["max_groesse_mb"]
})
attachments = []
try:
# Generator für streaming
for event in fetcher.fetch_attachments_generator(
ziel,
nur_ungelesen=False,
alle_ordner=pf_data["alle_ordner"],
bereits_verarbeitet=bereits_verarbeitet
):
yield send_event(event)
if event.get("type") == "datei":
attachments.append(event)
# DB-Session für Speicherung
session = SessionLocal()
try:
verarbeitete_msg_ids = set()
for att in attachments:
msg_id = att.get("message_id")
if msg_id and msg_id not in verarbeitete_msg_ids:
verarbeitete_msg_ids.add(msg_id)
session.add(VerarbeiteteMail(
postfach_id=pf_data["id"],
message_id=msg_id,
ordner=att.get("ordner", ""),
betreff=att.get("betreff", "")[:500] if att.get("betreff") else None,
absender=att.get("absender", "")[:255] if att.get("absender") else None,
anzahl_attachments=1
))
# Postfach aktualisieren
pf = session.query(Postfach).filter(Postfach.id == pf_data["id"]).first()
if pf:
pf.letzter_abruf = datetime.utcnow()
pf.letzte_anzahl = len(attachments)
session.commit()
finally:
session.close()
yield send_event({"type": "fertig", "anzahl": len(attachments)})
except Exception as e:
yield send_event({"type": "fehler", "nachricht": str(e)})
finally:
fetcher.disconnect()
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no"
}
)
@router.post("/postfaecher/{id}/abrufen")
def rufe_postfach_ab(id: int, db: Session = Depends(get_db)):
postfach = db.query(Postfach).filter(Postfach.id == id).first()
if not postfach:
raise HTTPException(status_code=404, detail="Nicht gefunden")
# Bereits verarbeitete Message-IDs laden
bereits_verarbeitet = set(
row.message_id for row in
db.query(VerarbeiteteMail.message_id)
.filter(VerarbeiteteMail.postfach_id == id)
.all()
)
# Zielordner erstellen
ziel = Path(postfach.ziel_ordner)
ziel.mkdir(parents=True, exist_ok=True)
fetcher = MailFetcher({
"imap_server": postfach.imap_server,
"imap_port": postfach.imap_port,
"email": postfach.email,
"passwort": postfach.passwort,
"ordner": postfach.ordner,
"erlaubte_typen": postfach.erlaubte_typen,
"max_groesse_mb": postfach.max_groesse_mb
})
try:
attachments = fetcher.fetch_attachments(
ziel,
nur_ungelesen=False, # Alle Mails durchsuchen
alle_ordner=postfach.alle_ordner,
bereits_verarbeitet=bereits_verarbeitet
)
# Verarbeitete Mails in DB speichern
verarbeitete_msg_ids = set()
for att in attachments:
msg_id = att.get("message_id")
if msg_id and msg_id not in verarbeitete_msg_ids:
verarbeitete_msg_ids.add(msg_id)
db.add(VerarbeiteteMail(
postfach_id=id,
message_id=msg_id,
ordner=att.get("ordner", ""),
betreff=att.get("betreff", "")[:500] if att.get("betreff") else None,
absender=att.get("absender", "")[:255] if att.get("absender") else None,
anzahl_attachments=1
))
postfach.letzter_abruf = datetime.utcnow()
postfach.letzte_anzahl = len(attachments)
db.commit()
return {
"ergebnisse": [{
"postfach": postfach.name,
"anzahl": len(attachments),
"dateien": [a["original_name"] for a in attachments],
"bereits_verarbeitet": len(bereits_verarbeitet)
}]
}
except Exception as e:
return {
"ergebnisse": [{
"postfach": postfach.name,
"fehler": str(e)
}]
}
finally:
fetcher.disconnect()
@router.post("/postfaecher/abrufen-alle")
def rufe_alle_postfaecher_ab(db: Session = Depends(get_db)):
postfaecher = db.query(Postfach).filter(Postfach.aktiv == True).all()
ergebnisse = []
for postfach in postfaecher:
ziel = Path(postfach.ziel_ordner)
ziel.mkdir(parents=True, exist_ok=True)
fetcher = MailFetcher({
"imap_server": postfach.imap_server,
"imap_port": postfach.imap_port,
"email": postfach.email,
"passwort": postfach.passwort,
"ordner": postfach.ordner,
"erlaubte_typen": postfach.erlaubte_typen,
"max_groesse_mb": postfach.max_groesse_mb
})
try:
attachments = fetcher.fetch_attachments(ziel)
postfach.letzter_abruf = datetime.utcnow()
postfach.letzte_anzahl = len(attachments)
ergebnisse.append({
"postfach": postfach.name,
"anzahl": len(attachments),
"dateien": [a["original_name"] for a in attachments]
})
except Exception as e:
ergebnisse.append({
"postfach": postfach.name,
"fehler": str(e)
})
finally:
fetcher.disconnect()
db.commit()
return {"ergebnisse": ergebnisse}
# ============ BEREICH 2: Quell-Ordner ============
@router.get("/ordner", response_model=List[OrdnerResponse])
def liste_ordner(db: Session = Depends(get_db)):
return db.query(QuellOrdner).all()
@router.post("/ordner", response_model=OrdnerResponse)
def erstelle_ordner(data: OrdnerCreate, db: Session = Depends(get_db)):
ordner = QuellOrdner(**data.dict())
db.add(ordner)
db.commit()
db.refresh(ordner)
return ordner
@router.delete("/ordner/{id}")
def loesche_ordner(id: int, db: Session = Depends(get_db)):
ordner = db.query(QuellOrdner).filter(QuellOrdner.id == id).first()
if not ordner:
raise HTTPException(status_code=404, detail="Nicht gefunden")
db.delete(ordner)
db.commit()
return {"message": "Gelöscht"}
@router.get("/ordner/{id}/scannen")
def scanne_ordner(id: int, db: Session = Depends(get_db)):
ordner = db.query(QuellOrdner).filter(QuellOrdner.id == id).first()
if not ordner:
raise HTTPException(status_code=404, detail="Nicht gefunden")
pfad = Path(ordner.pfad)
if not pfad.exists():
return {"anzahl": 0, "fehler": "Ordner existiert nicht"}
# Dateien sammeln (rekursiv oder nicht)
dateien = []
pattern = "**/*" if ordner.rekursiv else "*"
for f in pfad.glob(pattern):
if f.is_file() and f.suffix.lower() in [t.lower() for t in ordner.dateitypen]:
dateien.append(f)
return {"anzahl": len(dateien), "dateien": [str(f.relative_to(pfad)) for f in dateien[:30]]}
# ============ Regeln ============
@router.get("/regeln", response_model=List[RegelResponse])
def liste_regeln(db: Session = Depends(get_db)):
return db.query(SortierRegel).order_by(SortierRegel.prioritaet).all()
@router.post("/regeln", response_model=RegelResponse)
def erstelle_regel(data: RegelCreate, db: Session = Depends(get_db)):
regel = SortierRegel(**data.dict())
db.add(regel)
db.commit()
db.refresh(regel)
return regel
@router.put("/regeln/{id}", response_model=RegelResponse)
def aktualisiere_regel(id: int, data: RegelCreate, db: Session = Depends(get_db)):
regel = db.query(SortierRegel).filter(SortierRegel.id == id).first()
if not regel:
raise HTTPException(status_code=404, detail="Nicht gefunden")
for key, value in data.dict().items():
setattr(regel, key, value)
db.commit()
db.refresh(regel)
return regel
@router.delete("/regeln/{id}")
def loesche_regel(id: int, db: Session = Depends(get_db)):
regel = db.query(SortierRegel).filter(SortierRegel.id == id).first()
if not regel:
raise HTTPException(status_code=404, detail="Nicht gefunden")
db.delete(regel)
db.commit()
return {"message": "Gelöscht"}
@router.post("/regeln/test")
def teste_regel(data: RegelTestRequest):
regel = data.regel
regel["aktiv"] = True
regel["prioritaet"] = 1
sorter = Sorter([regel])
doc_info = {"text": data.text, "original_name": "test.pdf", "absender": ""}
passend = sorter.finde_passende_regel(doc_info)
if passend:
extrahiert = sorter.extrahiere_felder(passend, doc_info)
dateiname = sorter.generiere_dateinamen(passend, extrahiert)
return {"passt": True, "extrahiert": extrahiert, "dateiname": dateiname}
return {"passt": False}
# ============ Sortierung ============
def sammle_dateien(ordner: QuellOrdner) -> list:
"""Sammelt alle Dateien aus einem Ordner (rekursiv oder nicht)"""
pfad = Path(ordner.pfad)
if not pfad.exists():
return []
dateien = []
pattern = "**/*" if ordner.rekursiv else "*"
erlaubte = [t.lower() for t in (ordner.dateitypen or [".pdf"])]
for f in pfad.glob(pattern):
if f.is_file() and f.suffix.lower() in erlaubte:
dateien.append(f)
return dateien
@router.post("/sortierung/starten")
def starte_sortierung(db: Session = Depends(get_db)):
ordner_liste = db.query(QuellOrdner).filter(QuellOrdner.aktiv == True).all()
regeln = db.query(SortierRegel).filter(SortierRegel.aktiv == True).order_by(SortierRegel.prioritaet).all()
if not ordner_liste:
return {"fehler": "Keine Quell-Ordner konfiguriert", "verarbeitet": []}
if not regeln:
return {"fehler": "Keine Regeln definiert", "verarbeitet": []}
# Regeln in Dict-Format
regeln_dicts = []
for r in regeln:
regeln_dicts.append({
"id": r.id,
"name": r.name,
"prioritaet": r.prioritaet,
"muster": r.muster,
"extraktion": r.extraktion,
"schema": r.schema,
"unterordner": r.unterordner
})
sorter = Sorter(regeln_dicts)
pdf_processor = PDFProcessor()
ergebnis = {
"gesamt": 0,
"sortiert": 0,
"zugferd": 0,
"fehler": 0,
"verarbeitet": []
}
for quell_ordner in ordner_liste:
pfad = Path(quell_ordner.pfad)
if not pfad.exists():
continue
ziel_basis = Path(quell_ordner.ziel_ordner)
dateien = sammle_dateien(quell_ordner)
for datei in dateien:
ergebnis["gesamt"] += 1
# Relativer Pfad für Anzeige
try:
rel_pfad = str(datei.relative_to(pfad))
except:
rel_pfad = datei.name
datei_info = {"original": rel_pfad}
try:
ist_pdf = datei.suffix.lower() == ".pdf"
text = ""
ist_zugferd = False
ocr_gemacht = False
# Nur PDFs durch den PDF-Processor
if ist_pdf:
pdf_result = pdf_processor.verarbeite(str(datei))
if pdf_result.get("fehler"):
raise Exception(pdf_result["fehler"])
text = pdf_result.get("text", "")
ist_zugferd = pdf_result.get("ist_zugferd", False)
ocr_gemacht = pdf_result.get("ocr_durchgefuehrt", False)
# ZUGFeRD separat behandeln
if ist_zugferd:
zugferd_ziel = ziel_basis / "zugferd"
zugferd_ziel.mkdir(parents=True, exist_ok=True)
neuer_pfad = zugferd_ziel / datei.name
counter = 1
while neuer_pfad.exists():
neuer_pfad = zugferd_ziel / f"{datei.stem}_{counter}{datei.suffix}"
counter += 1
datei.rename(neuer_pfad)
ergebnis["zugferd"] += 1
datei_info["zugferd"] = True
datei_info["neuer_name"] = neuer_pfad.name
db.add(VerarbeiteteDatei(
original_pfad=str(datei),
original_name=datei.name,
neuer_pfad=str(neuer_pfad),
neuer_name=neuer_pfad.name,
ist_zugferd=True,
status="zugferd"
))
ergebnis["verarbeitet"].append(datei_info)
continue
# Regel finden (für PDFs mit Text, für andere nur Dateiname)
doc_info = {
"text": text,
"original_name": datei.name,
"absender": "",
"dateityp": datei.suffix.lower()
}
regel = sorter.finde_passende_regel(doc_info)
if not regel:
datei_info["fehler"] = "Keine passende Regel"
ergebnis["fehler"] += 1
ergebnis["verarbeitet"].append(datei_info)
continue
# Felder extrahieren
extrahiert = sorter.extrahiere_felder(regel, doc_info)
# Dateiendung beibehalten
schema = regel.get("schema", "{datum} - Dokument.pdf")
# Endung aus Schema entfernen und Original-Endung anhängen
if schema.endswith(".pdf"):
schema = schema[:-4] + datei.suffix
neuer_name = sorter.generiere_dateinamen({"schema": schema, **regel}, extrahiert)
# Zielordner
ziel = ziel_basis
if regel.get("unterordner"):
ziel = ziel / regel["unterordner"]
ziel.mkdir(parents=True, exist_ok=True)
# Verschieben
neuer_pfad = sorter.verschiebe_datei(str(datei), str(ziel), neuer_name)
ergebnis["sortiert"] += 1
datei_info["neuer_name"] = neuer_name
db.add(VerarbeiteteDatei(
original_pfad=str(datei),
original_name=datei.name,
neuer_pfad=neuer_pfad,
neuer_name=neuer_name,
ist_zugferd=False,
ocr_durchgefuehrt=ocr_gemacht,
status="sortiert",
extrahierte_daten=extrahiert
))
except Exception as e:
ergebnis["fehler"] += 1
datei_info["fehler"] = str(e)
ergebnis["verarbeitet"].append(datei_info)
db.commit()
return ergebnis
@router.get("/health")
def health():
return {"status": "ok"}
# ============ Einfache Regeln (UI-freundlich) ============
@router.get("/dokumenttypen")
def liste_dokumenttypen():
"""Gibt alle verfügbaren Dokumenttypen für das UI zurück"""
from ..modules.sorter import DOKUMENTTYPEN
return [
{"id": key, "name": config["name"], "schema": config["schema"], "unterordner": config["unterordner"]}
for key, config in DOKUMENTTYPEN.items()
]
class EinfacheRegelCreate(BaseModel):
name: str
dokumenttyp: str # z.B. "rechnung", "vertrag"
keywords: str # Komma-getrennt
firma: Optional[str] = None # Fester Firmenwert
unterordner: Optional[str] = None
prioritaet: int = 50
@router.post("/regeln/einfach")
def erstelle_einfache_regel_api(data: EinfacheRegelCreate, db: Session = Depends(get_db)):
"""Erstellt eine Regel basierend auf Dokumenttyp - für einfaches UI"""
from ..modules.sorter import DOKUMENTTYPEN
typ_config = DOKUMENTTYPEN.get(data.dokumenttyp, DOKUMENTTYPEN["sonstiges"])
# Muster als Dict (keywords werden vom Sorter geparst)
muster = {"keywords": data.keywords}
# Extraktion (nur Firma wenn angegeben)
extraktion = {}
if data.firma:
extraktion["firma"] = {"wert": data.firma}
regel = SortierRegel(
name=data.name,
prioritaet=data.prioritaet,
aktiv=True,
muster=muster,
extraktion=extraktion,
schema=typ_config["schema"],
unterordner=data.unterordner or typ_config["unterordner"]
)
db.add(regel)
db.commit()
db.refresh(regel)
return {
"id": regel.id,
"name": regel.name,
"dokumenttyp": data.dokumenttyp,
"keywords": data.keywords,
"schema": regel.schema
}
class ExtraktionTestRequest(BaseModel):
text: str
dateiname: Optional[str] = "test.pdf"
@router.post("/extraktion/test")
def teste_extraktion(data: ExtraktionTestRequest):
"""Testet die automatische Extraktion auf einem Text"""
from ..modules.extraktoren import extrahiere_alle_felder, baue_dateiname
dokument_info = {
"original_name": data.dateiname,
"absender": ""
}
# Felder extrahieren
felder = extrahiere_alle_felder(data.text, dokument_info)
# Beispiel-Dateinamen für verschiedene Typen generieren
beispiele = {}
from ..modules.sorter import DOKUMENTTYPEN
for typ_id, typ_config in DOKUMENTTYPEN.items():
beispiele[typ_id] = baue_dateiname(typ_config["schema"], felder, ".pdf")
return {
"extrahiert": felder,
"beispiel_dateinamen": beispiele
}
@router.post("/regeln/{id}/vorschau")
def regel_vorschau(id: int, data: ExtraktionTestRequest, db: Session = Depends(get_db)):
"""Zeigt Vorschau wie eine Regel auf einen Text angewendet würde"""
regel = db.query(SortierRegel).filter(SortierRegel.id == id).first()
if not regel:
raise HTTPException(status_code=404, detail="Regel nicht gefunden")
from ..modules.sorter import Sorter
sorter = Sorter([{
"id": regel.id,
"name": regel.name,
"prioritaet": regel.prioritaet,
"aktiv": True,
"muster": regel.muster,
"extraktion": regel.extraktion,
"schema": regel.schema,
"unterordner": regel.unterordner
}])
dokument_info = {
"text": data.text,
"original_name": data.dateiname or "test.pdf",
"absender": ""
}
# Prüfen ob Regel matched
passende_regel = sorter.finde_passende_regel(dokument_info)
if not passende_regel:
return {
"matched": False,
"grund": "Keywords nicht gefunden"
}
# Felder extrahieren
felder = sorter.extrahiere_felder(passende_regel, dokument_info)
# Dateiname generieren
dateiname = sorter.generiere_dateinamen(passende_regel, felder)
return {
"matched": True,
"extrahiert": felder,
"dateiname": dateiname,
"unterordner": passende_regel.get("unterordner")
}