217 lines
7.4 KiB
Python
217 lines
7.4 KiB
Python
import logging
|
||
import os
|
||
from typing import Any
|
||
|
||
from fastapi import Body, Depends, FastAPI, Header, HTTPException, Query, Request
|
||
|
||
from app.models import CompanyData, CreateCompanyResponse
|
||
from app.raynet_client import (
|
||
RaynetAuthError,
|
||
RaynetClient,
|
||
RaynetError,
|
||
RaynetValidationError,
|
||
)
|
||
|
||
logging.basicConfig(level=os.getenv("LOG_LEVEL", "INFO"))
|
||
logger = logging.getLogger(__name__)
|
||
|
||
APP_NAME = os.getenv("APP_NAME", "raynet")
|
||
APP_VERSION = os.getenv("APP_VERSION", "1.0.0")
|
||
ROOT_PATH = os.getenv("ROOT_PATH", "")
|
||
|
||
# Popis se zobrazí nahoře ve Swaggeru (/docs) – přihlašovací údaje jsou
|
||
# společné pro celé připojení a posílají se v hlavičkách u KAŽDÉHO requestu.
|
||
API_DESCRIPTION = """
|
||
Stateless proxy nad **RAYNET CRM API v2**.
|
||
|
||
## Přihlášení (společné pro celé připojení)
|
||
|
||
Každý endpoint vyžaduje tři hlavičky. Jsou stejné pro všechna volání:
|
||
|
||
| Hlavička | Co to je | Kde to vzít |
|
||
|-------------------|--------------------|-------------|
|
||
| **`X-Api-Key`** | API klíč (secret) | V RAYNET CRM: **Nastavení → Klíč k API** (vygeneruj / zkopíruj). |
|
||
| **`X-Raynet-Email`** | Email uživatele | Přihlašovací email do RAYNET (tvoří dvojici s API klíčem pro Basic Auth). |
|
||
| **`X-Instance-Name`** | Název instance | Identifikátor tvé RAYNET instance (subdoména účtu). |
|
||
|
||
`X-Api-Key` je **secret** – nikdy se neloguje ani nevrací z endpointů.
|
||
|
||
## Endpointy
|
||
|
||
- Typované zkratky pro firmy: `POST/GET/PUT/DELETE /company...`
|
||
- Generický průchod na celé API: `/api/{resource}` – podporuje libovolnou
|
||
RAYNET entitu (`company`, `person`, `lead`, `businessCase`, `activity`,
|
||
`product`, `offer`, `order`, `project`, ...).
|
||
|
||
Plná dokumentace RAYNET: <https://app.raynetcrm.com/api/doc/index-en.html>
|
||
"""
|
||
|
||
app = FastAPI(
|
||
title=APP_NAME,
|
||
version=APP_VERSION,
|
||
root_path=ROOT_PATH,
|
||
description=API_DESCRIPTION,
|
||
)
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Sdílená credentials dependency – 3 hlavičky pro celé připojení.
|
||
# --------------------------------------------------------------------------- #
|
||
def get_client(
|
||
x_api_key: str = Header(
|
||
..., alias="X-Api-Key", description="RAYNET API klíč (secret)."
|
||
),
|
||
x_raynet_email: str = Header(
|
||
..., alias="X-Raynet-Email", description="Email uživatele RAYNET."
|
||
),
|
||
x_instance_name: str = Header(
|
||
..., alias="X-Instance-Name", description="Název RAYNET instance."
|
||
),
|
||
):
|
||
"""Vytvoří RaynetClient z hlaviček a po dokončení requestu uvolní session."""
|
||
client = RaynetClient(
|
||
api_key=x_api_key,
|
||
email=x_raynet_email,
|
||
instance_name=x_instance_name,
|
||
)
|
||
try:
|
||
yield client
|
||
finally:
|
||
client.close()
|
||
|
||
|
||
def _run(fn, *args, **kwargs) -> Any:
|
||
"""Spustí volání klienta a převede výjimky konektoru na HTTP odpovědi."""
|
||
try:
|
||
return fn(*args, **kwargs)
|
||
except ValueError as exc:
|
||
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
||
except RaynetAuthError as exc:
|
||
raise HTTPException(status_code=401, detail=exc.message) from exc
|
||
except RaynetValidationError as exc:
|
||
raise HTTPException(status_code=400, detail=exc.message) from exc
|
||
except RaynetError as exc:
|
||
raise HTTPException(status_code=502, detail=exc.message) from exc
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Servisní endpointy
|
||
# --------------------------------------------------------------------------- #
|
||
@app.get("/health", tags=["service"])
|
||
def health():
|
||
return {"status": "ok"}
|
||
|
||
|
||
@app.get("/version", tags=["service"])
|
||
def version():
|
||
return {
|
||
"app": APP_NAME,
|
||
"version": APP_VERSION,
|
||
"language": "python",
|
||
"root_path": ROOT_PATH,
|
||
}
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Typované zkratky pro firmy (company)
|
||
# --------------------------------------------------------------------------- #
|
||
@app.post("/company", response_model=CreateCompanyResponse, tags=["company"])
|
||
def create_company(
|
||
company: CompanyData,
|
||
client: RaynetClient = Depends(get_client),
|
||
):
|
||
"""Vytvoří firmu v RAYNET CRM (validovaná data dle CompanyInsertDto)."""
|
||
data = company.model_dump(exclude_none=True)
|
||
result = _run(client.create_company, data)
|
||
return CreateCompanyResponse(id=result.get("id"), success=True, raw=result)
|
||
|
||
|
||
@app.get("/company", tags=["company"])
|
||
def list_companies(
|
||
client: RaynetClient = Depends(get_client),
|
||
offset: int = Query(0, ge=0),
|
||
limit: int = Query(20, ge=1, le=1000),
|
||
fulltext: str | None = Query(None, description="Fulltextové hledání"),
|
||
):
|
||
"""Vrátí seznam firem s podporou stránkování a fulltextu."""
|
||
params = {"offset": offset, "limit": limit}
|
||
if fulltext:
|
||
params["fulltext"] = fulltext
|
||
return _run(client.list_companies, **params)
|
||
|
||
|
||
@app.get("/company/{company_id}", tags=["company"])
|
||
def get_company(company_id: int, client: RaynetClient = Depends(get_client)):
|
||
"""Vrátí detail firmy."""
|
||
return _run(client.get_company, company_id)
|
||
|
||
|
||
@app.put("/company/{company_id}", tags=["company"])
|
||
def update_company(
|
||
company_id: int,
|
||
data: dict = Body(..., description="Pole firmy ke změně"),
|
||
client: RaynetClient = Depends(get_client),
|
||
):
|
||
"""Upraví firmu."""
|
||
return _run(client.update_company, company_id, data)
|
||
|
||
|
||
@app.delete("/company/{company_id}", tags=["company"])
|
||
def delete_company(company_id: int, client: RaynetClient = Depends(get_client)):
|
||
"""Smaže firmu."""
|
||
return _run(client.delete_company, company_id)
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Generický průchod na CELÉ RAYNET API
|
||
# resource = libovolná entita: company, person, lead, businessCase, activity,
|
||
# product, offer, order, project, ...
|
||
# --------------------------------------------------------------------------- #
|
||
@app.get("/api/{resource}", tags=["generic"])
|
||
def api_list(
|
||
resource: str,
|
||
request: Request,
|
||
client: RaynetClient = Depends(get_client),
|
||
):
|
||
"""Seznam záznamů entity. Všechny query parametry se předávají do RAYNET
|
||
(např. `offset`, `limit`, `fulltext`, `name`, ...)."""
|
||
params = dict(request.query_params)
|
||
return _run(client.list_records, resource, params or None)
|
||
|
||
|
||
@app.get("/api/{resource}/{record_id}", tags=["generic"])
|
||
def api_detail(
|
||
resource: str, record_id: str, client: RaynetClient = Depends(get_client)
|
||
):
|
||
"""Detail jednoho záznamu."""
|
||
return _run(client.get_record, resource, record_id)
|
||
|
||
|
||
@app.post("/api/{resource}", tags=["generic"])
|
||
def api_create(
|
||
resource: str,
|
||
data: dict = Body(..., description="Data nového záznamu"),
|
||
client: RaynetClient = Depends(get_client),
|
||
):
|
||
"""Vytvoří záznam (mapuje se na RAYNET `PUT /{resource}/`)."""
|
||
return _run(client.create_record, resource, data)
|
||
|
||
|
||
@app.put("/api/{resource}/{record_id}", tags=["generic"])
|
||
def api_update(
|
||
resource: str,
|
||
record_id: str,
|
||
data: dict = Body(..., description="Pole ke změně"),
|
||
client: RaynetClient = Depends(get_client),
|
||
):
|
||
"""Upraví záznam (mapuje se na RAYNET `POST /{resource}/{id}/`)."""
|
||
return _run(client.update_record, resource, record_id, data)
|
||
|
||
|
||
@app.delete("/api/{resource}/{record_id}", tags=["generic"])
|
||
def api_delete(
|
||
resource: str, record_id: str, client: RaynetClient = Depends(get_client)
|
||
):
|
||
"""Smaže záznam."""
|
||
return _run(client.delete_record, resource, record_id)
|