From 0b8a10481b9a64a1d13de567c20feaf7b54a3b3a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 22:45:02 +0200 Subject: [PATCH] feat: scaffold FastAPI intelligence-api with ClickHouse client, company/contact search endpoints --- .env.example | 12 ++++++++++++ .gitignore | 5 +++++ Dockerfile | 7 +++++++ README.md | 15 +++++++++++++++ app/__init__.py | 0 app/clickhouse.py | 12 ++++++++++++ app/config.py | 15 +++++++++++++++ app/main.py | 10 ++++++++++ app/routers/__init__.py | 0 app/routers/companies.py | 16 ++++++++++++++++ app/routers/contacts.py | 25 +++++++++++++++++++++++++ app/routers/health.py | 10 ++++++++++ app/schemas.py | 11 +++++++++++ requirements.txt | 14 ++++++++++++++ 14 files changed, 152 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 app/__init__.py create mode 100644 app/clickhouse.py create mode 100644 app/config.py create mode 100644 app/main.py create mode 100644 app/routers/__init__.py create mode 100644 app/routers/companies.py create mode 100644 app/routers/contacts.py create mode 100644 app/routers/health.py create mode 100644 app/schemas.py create mode 100644 requirements.txt diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..3387baa --- /dev/null +++ b/.env.example @@ -0,0 +1,12 @@ +CLICKHOUSE_URL= +CLICKHOUSE_USER=default +CLICKHOUSE_PASSWORD= +CLICKHOUSE_DATABASE=intelligence + +REDIS_URL= + +RAW_DATA_PATH=/data/raw +PROCESSED_DATA_PATH=/data/processed + +QDRANT_URL= +QDRANT_API_KEY= diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7616915 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +__pycache__/ +*.pyc +.venv/ +.env +*.log diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..87086d5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM python:3.12-slim +WORKDIR /app +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt +COPY . . +EXPOSE 8000 +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/README.md b/README.md index cf435e4..b675718 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,17 @@ # intelligence-api +FastAPI pentru Data-Intelligence: cautare companii/persoane, matching contacte, scoring, statistici — peste ClickHouse. + +## Stack + +FastAPI, Uvicorn, Pydantic, clickhouse-connect, Polars, DuckDB, PyArrow, psycopg, Redis, rapidfuzz, phonenumbers, email-validator, orjson. + +## Dezvoltare + +```bash +pip install -r requirements.txt +cp .env.example .env +uvicorn app.main:app --reload +``` + +`GET /health` returneaza statusul serviciului. `GET /docs` expune Swagger UI (automat din FastAPI). diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/clickhouse.py b/app/clickhouse.py new file mode 100644 index 0000000..9dbcf90 --- /dev/null +++ b/app/clickhouse.py @@ -0,0 +1,12 @@ +import clickhouse_connect + +from app.config import settings + + +def get_client(): + return clickhouse_connect.get_client( + dsn=settings.clickhouse_url, + username=settings.clickhouse_user, + password=settings.clickhouse_password, + database=settings.clickhouse_database, + ) diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..78ffb6c --- /dev/null +++ b/app/config.py @@ -0,0 +1,15 @@ +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + clickhouse_url: str = "" + clickhouse_user: str = "default" + clickhouse_password: str = "" + clickhouse_database: str = "intelligence" + redis_url: str = "" + + class Config: + env_file = ".env" + + +settings = Settings() diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..507f27c --- /dev/null +++ b/app/main.py @@ -0,0 +1,10 @@ +from fastapi import FastAPI +from fastapi.responses import ORJSONResponse + +from app.routers import companies, contacts, health + +app = FastAPI(title="Intelligence API", version="0.1.0", default_response_class=ORJSONResponse) + +app.include_router(health.router) +app.include_router(companies.router) +app.include_router(contacts.router) diff --git a/app/routers/__init__.py b/app/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/routers/companies.py b/app/routers/companies.py new file mode 100644 index 0000000..0881e0d --- /dev/null +++ b/app/routers/companies.py @@ -0,0 +1,16 @@ +from fastapi import APIRouter + +from app.clickhouse import get_client +from app.schemas import CompanySearchQuery + +router = APIRouter(prefix="/companies", tags=["companies"]) + + +@router.post("/search") +def search_companies(payload: CompanySearchQuery): + client = get_client() + result = client.query( + "SELECT name, domain, country FROM companies WHERE name ILIKE {q:String} LIMIT {limit:UInt32}", + parameters={"q": f"%{payload.query}%", "limit": payload.limit}, + ) + return {"results": result.result_rows} diff --git a/app/routers/contacts.py b/app/routers/contacts.py new file mode 100644 index 0000000..c1bf793 --- /dev/null +++ b/app/routers/contacts.py @@ -0,0 +1,25 @@ +from fastapi import APIRouter +from rapidfuzz import fuzz + +from app.clickhouse import get_client +from app.schemas import ContactMatchQuery + +router = APIRouter(prefix="/contacts", tags=["contacts"]) + + +@router.post("/match") +def match_contact(payload: ContactMatchQuery): + """Placeholder pentru entity resolution: cauta dupa email/telefon normalizat, + apoi scoreaza candidatii cu rapidfuzz pentru potriviri apropiate.""" + client = get_client() + if payload.email: + result = client.query( + "SELECT * FROM contacts WHERE email = {email:String} LIMIT 5", + parameters={"email": payload.email}, + ) + return {"matches": result.result_rows} + return {"matches": []} + + +def score_name_similarity(a: str, b: str) -> float: + return fuzz.token_sort_ratio(a, b) diff --git a/app/routers/health.py b/app/routers/health.py new file mode 100644 index 0000000..18a9c8f --- /dev/null +++ b/app/routers/health.py @@ -0,0 +1,10 @@ +from datetime import datetime, timezone + +from fastapi import APIRouter + +router = APIRouter() + + +@router.get("/health") +def health(): + return {"status": "ok", "service": "intelligence-api", "timestamp": datetime.now(timezone.utc).isoformat()} diff --git a/app/schemas.py b/app/schemas.py new file mode 100644 index 0000000..9d34cab --- /dev/null +++ b/app/schemas.py @@ -0,0 +1,11 @@ +from pydantic import BaseModel, EmailStr + + +class CompanySearchQuery(BaseModel): + query: str + limit: int = 20 + + +class ContactMatchQuery(BaseModel): + email: EmailStr | None = None + phone: str | None = None diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..547573e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,14 @@ +fastapi==0.115.5 +uvicorn[standard]==0.32.1 +pydantic==2.10.2 +pydantic-settings==2.6.1 +clickhouse-connect==0.8.7 +polars==1.15.0 +duckdb==1.1.3 +pyarrow==18.0.0 +psycopg[binary]==3.2.3 +redis==5.2.0 +rapidfuzz==3.10.1 +phonenumbers==8.13.50 +email-validator==2.2.0 +orjson==3.10.12