This commit is contained in:
LORENZO\pacio 2025-09-26 15:29:31 +02:00
parent 6e34ffe2fe
commit 8c0e892fc5
6 changed files with 177 additions and 0 deletions

27
Dockerfile Normal file
View File

@ -0,0 +1,27 @@
FROM python:3.12-slim-bookworm
# Base deps
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
curl gnupg ca-certificates \
unixodbc unixodbc-dev \
&& rm -rf /var/lib/apt/lists/*
# MS ODBC Driver 17 for SQL Server
RUN mkdir -p /etc/apt/keyrings \
&& curl -fsSL https://packages.microsoft.com/keys/microsoft.asc \
| gpg --dearmor -o /etc/apt/keyrings/microsoft-prod.gpg \
&& echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/microsoft-prod.gpg] https://packages.microsoft.com/debian/12/prod bookworm main" \
> /etc/apt/sources.list.d/microsoft-prod.list \
&& apt-get update \
&& ACCEPT_EULA=Y apt-get install -y msodbcsql17 unixodbc unixodbc-dev \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app ./app
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

78
app/db.py Normal file
View File

@ -0,0 +1,78 @@
from sqlalchemy import create_engine, text
from sqlalchemy.engine import Engine
from urllib.parse import quote_plus
def build_odbc_url(
server: str,
database: str,
username: str | None,
password: str | None,
encrypt: bool = False,
trust_server_certificate: bool = True,
timeout: int = 15,
) -> str:
"""
Build SQLAlchemy ODBC URL for SQL Server using ODBC Driver 18.
Default Encrypt=no to support servers with encryption disabled.
If encrypt=True, we set Encrypt=yes and TrustServerCertificate accordingly.
"""
driver = "ODBC Driver 17 for SQL Server"
odbc_params = {
"DRIVER": driver,
"SERVER": server,
"DATABASE": database,
"Connection Timeout": str(timeout),
}
if encrypt:
odbc_params["Encrypt"] = "yes"
odbc_params["TrustServerCertificate"] = "yes" if trust_server_certificate else "no"
else:
# critical for servers with encryption disabled
odbc_params["Encrypt"] = "no"
if username and password:
odbc_params["UID"] = username
odbc_params["PWD"] = password
else:
# Trusted_Connection can work on Windows/domain; usually not in Linux containers.
odbc_params["Trusted_Connection"] = "yes"
conn_str = ";".join([f"{k}={v}" for k, v in odbc_params.items()])
return f"mssql+pyodbc:///?odbc_connect={quote_plus(conn_str)}"
def get_engine(payload) -> Engine:
url = build_odbc_url(
server=payload.server,
database=payload.database,
username=getattr(payload, "username", None),
password=getattr(payload, "password", None),
encrypt=getattr(payload, "encrypt", False),
trust_server_certificate=getattr(payload, "trust_server_certificate", True),
timeout=getattr(payload, "timeout", 15),
)
return create_engine(
url,
pool_size=5,
max_overflow=10,
pool_pre_ping=True,
fast_executemany=True,
)
async def run_query(engine: Engine, sql: str, params: dict, max_rows: int | None = None):
with engine.connect() as conn:
result = conn.execute(text(sql), params)
if result.returns_rows:
rows = result.fetchmany(max_rows) if max_rows else result.fetchall()
cols = result.keys()
data = [dict(zip(cols, row)) for row in rows]
return {"columns": list(cols), "rows": data}
return {"columns": [], "rows": []}
async def run_execute(engine: Engine, sql: str, params: dict, autocommit: bool):
with (engine.begin() if not autocommit else engine.connect()) as conn:
result = conn.execute(text(sql), params)
if autocommit:
conn.commit()
return {"rowcount": result.rowcount}

36
app/main.py Normal file
View File

@ -0,0 +1,36 @@
from fastapi import FastAPI, HTTPException
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder
from app.schemas import QueryPayload, ExecutePayload
from app.db import get_engine, run_query, run_execute
app = FastAPI(title="MSSQL ODBC Service", version="1.1.0")
def _trim_strings(obj):
if isinstance(obj, str):
return obj.strip()
if isinstance(obj, list):
return [_trim_strings(x) for x in obj]
if isinstance(obj, dict):
return {k: _trim_strings(v) for k, v in obj.items()}
return obj
@app.post("/query")
async def query(payload: QueryPayload):
try:
engine = get_engine(payload)
result = await run_query(engine, payload.sql, payload.params, payload.max_rows)
result = _trim_strings(result)
return JSONResponse(content=jsonable_encoder(result))
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@app.post("/execute")
async def execute(payload: ExecutePayload):
try:
engine = get_engine(payload)
result = await run_execute(engine, payload.sql, payload.params, payload.autocommit)
result = _trim_strings(result)
return JSONResponse(content=jsonable_encoder(result))
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))

25
app/schemas.py Normal file
View File

@ -0,0 +1,25 @@
from typing import Any, Dict, Optional
from pydantic import BaseModel, Field
class ConnectionPayload(BaseModel):
server: str = Field(..., example="mssql.example.local,1433")
database: str = Field(..., example="MyDb")
username: Optional[str] = Field(None, example="sa")
password: Optional[str] = Field(None, example="S3cret!")
# For servers with encryption disabled, set encrypt=False (default below)
encrypt: bool = Field(default=False, description="ODBC Encrypt setting: yes/no")
trust_server_certificate: bool = Field(
default=True,
description="If encrypt=True and you dont have CA chain, set True"
)
timeout: int = Field(default=15, ge=1, le=120)
class QueryPayload(ConnectionPayload):
sql: str = Field(..., example="SELECT TOP 5 id, name FROM dbo.Customers WHERE city = :city")
params: Dict[str, Any] = Field(default_factory=dict)
max_rows: Optional[int] = Field(None, ge=1, example=1000)
class ExecutePayload(ConnectionPayload):
sql: str = Field(..., example="UPDATE dbo.Customers SET name = :name WHERE id = :id")
params: Dict[str, Any] = Field(default_factory=dict)
autocommit: bool = Field(default=False)

7
docker-compose.yml Normal file
View File

@ -0,0 +1,7 @@
services:
mssql-odbc-service:
build: .
container_name: mssql-odbc-service
ports:
- "38500:8000"
restart: unless-stopped

4
requirements.txt Normal file
View File

@ -0,0 +1,4 @@
fastapi==0.115.2
uvicorn[standard]==0.30.6
SQLAlchemy==2.0.35
pyodbc==5.2.0