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: """ 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)