feat: scaffold FastAPI intelligence-api with ClickHouse client, company/contact search endpoints
This commit is contained in:
parent
c6c6de4787
commit
0b8a10481b
14 changed files with 152 additions and 0 deletions
12
.env.example
Normal file
12
.env.example
Normal file
|
|
@ -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=
|
||||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.venv/
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
7
Dockerfile
Normal file
7
Dockerfile
Normal file
|
|
@ -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"]
|
||||||
15
README.md
15
README.md
|
|
@ -1,2 +1,17 @@
|
||||||
# intelligence-api
|
# 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).
|
||||||
|
|
|
||||||
0
app/__init__.py
Normal file
0
app/__init__.py
Normal file
12
app/clickhouse.py
Normal file
12
app/clickhouse.py
Normal file
|
|
@ -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,
|
||||||
|
)
|
||||||
15
app/config.py
Normal file
15
app/config.py
Normal file
|
|
@ -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()
|
||||||
10
app/main.py
Normal file
10
app/main.py
Normal file
|
|
@ -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)
|
||||||
0
app/routers/__init__.py
Normal file
0
app/routers/__init__.py
Normal file
16
app/routers/companies.py
Normal file
16
app/routers/companies.py
Normal file
|
|
@ -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}
|
||||||
25
app/routers/contacts.py
Normal file
25
app/routers/contacts.py
Normal file
|
|
@ -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)
|
||||||
10
app/routers/health.py
Normal file
10
app/routers/health.py
Normal file
|
|
@ -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()}
|
||||||
11
app/schemas.py
Normal file
11
app/schemas.py
Normal file
|
|
@ -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
|
||||||
14
requirements.txt
Normal file
14
requirements.txt
Normal file
|
|
@ -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
|
||||||
Loading…
Reference in a new issue