Files
raynet/app/main.py
T
JiriUhlir a5795fe344 2
2026-06-18 15:11:49 +02:00

217 lines
7.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)