Skip to content

Commit 9a9cc9f

Browse files
committed
[python] TurboAPI: fix bugs
1 parent d6c7e92 commit 9a9cc9f

3 files changed

Lines changed: 114 additions & 120 deletions

File tree

frameworks/turboapi/Dockerfile

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,62 @@
1-
FROM ubuntu:24.04
1+
# TurboAPI — Python 3.14 free-threaded + Zig 0.15 native backend
2+
FROM python:3.14-bookworm AS builder
23

3-
RUN apt-get update && apt-get install -y wget xz-utils build-essential git curl && rm -rf /var/lib/apt/lists/*
4+
# Install Zig 0.15.2
5+
RUN ARCH=$(dpkg --print-architecture) \
6+
&& if [ "$ARCH" = "arm64" ]; then ZIG_ARCH=aarch64; else ZIG_ARCH=x86_64; fi \
7+
&& curl -fSL "https://ziglang.org/download/0.15.2/zig-${ZIG_ARCH}-linux-0.15.2.tar.xz" \
8+
| tar -xJ -C /opt \
9+
&& ln -s /opt/zig-${ZIG_ARCH}-linux-0.15.2/zig /usr/local/bin/zig
410

5-
# Zig 0.15.2
6-
RUN ARCH=$(uname -m) && \
7-
wget -q "https://ziglang.org/download/0.15.2/zig-${ARCH}-linux-0.15.2.tar.xz" && \
8-
tar xf zig-*.tar.xz && mv zig-*-linux-0.15.2 /opt/zig && rm zig-*.tar.xz
9-
ENV PATH="/opt/zig:$PATH"
11+
# Build Python 3.14 free-threaded from source
12+
RUN apt-get update && apt-get install -y --no-install-recommends \
13+
build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev \
14+
libsqlite3-dev libncurses5-dev libffi-dev liblzma-dev \
15+
&& PYVER=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}')") \
16+
&& curl -fSL "https://www.python.org/ftp/python/${PYVER}/Python-${PYVER}.tgz" | tar xz -C /tmp \
17+
&& cd /tmp/Python-${PYVER} \
18+
&& ./configure --prefix=/opt/python3.14t --disable-gil --enable-shared --with-ensurepip=install \
19+
LDFLAGS="-Wl,-rpath,/opt/python3.14t/lib" 2>&1 | tail -5 \
20+
&& make -j$(nproc) 2>&1 | tail -3 \
21+
&& make install 2>&1 | tail -3 \
22+
&& /opt/python3.14t/bin/python3 -c "import sys; assert not sys._is_gil_enabled(); print('Free-threaded OK')" \
23+
&& rm -rf /tmp/Python-*
1024

11-
# uv + Python 3.14t free-threaded
12-
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
13-
ENV PATH="/root/.local/bin:$PATH"
14-
RUN uv python install 3.14t && uv venv --python 3.14t /venv
15-
ENV PATH="/venv/bin:$PATH" VIRTUAL_ENV="/venv"
25+
ENV PATH="/opt/python3.14t/bin:$PATH"
1626

17-
WORKDIR /turboapi
27+
WORKDIR /app
1828

19-
RUN git clone --depth 1 --branch v1.0.27 https://github.com/justrach/turboAPI.git . && \
20-
git clone --depth 1 --branch v1.2.1 https://github.com/justrach/dhi.git /dhi
29+
# download TurboAPI sources
30+
RUN git clone --depth 1 --branch v1.0.27 https://github.com/justrach/turboAPI.git .
2131

22-
# Install Python deps
23-
RUN uv pip install dhi && uv pip install -e ./python
32+
# patch dhi
33+
RUN sed -i 's|\(\.url = "\)[^"]*github\.com/justrach/dhi/[^"]*"|\1git+https://github.com/justrach/dhi?ref=main#44a3b88f37ffb095c05c668e0b2561f75d6aed1e"|' /app/zig/build.zig.zon
2434

25-
# Build Zig backend + copy .so with correct SOABI name
26-
RUN python zig/build_turbonet.py && \
27-
SOABI=$(python -c "import sysconfig; print(sysconfig.get_config_var('SOABI'))") && \
28-
cp zig/zig-out/lib/libturbonet.so "python/turboapi/turbonet.${SOABI}.so"
35+
# Build the Zig native backend (dhi fetched automatically via build.zig.zon)
36+
RUN python3 zig/build_turbonet.py --install --release
2937

30-
COPY app.py /turboapi/app.py
3138

32-
EXPOSE 8080
39+
# --- Runtime stage ---
3340

34-
CMD ["/venv/bin/python", "/turboapi/app.py"]
41+
FROM debian:bookworm-slim
42+
43+
# Copy free-threaded Python + turboapi
44+
COPY --from=builder /opt/python3.14t /opt/python3.14t
45+
ENV PATH="/opt/python3.14t/bin:$PATH"
46+
47+
# Runtime deps for Python
48+
RUN apt-get update && apt-get install -y --no-install-recommends \
49+
libssl3 zlib1g libbz2-1.0 libreadline8 libsqlite3-0 \
50+
libncurses6 libffi8 liblzma5 \
51+
&& rm -rf /var/lib/apt/lists/*
52+
53+
WORKDIR /app
54+
COPY --from=builder /app /app
55+
COPY app.py /app/app.py
56+
57+
# Install turboapi + deps
58+
RUN pip3 install --no-cache-dir -e .
59+
60+
EXPOSE 8000
61+
62+
CMD ["python3", "app.py"]

frameworks/turboapi/app.py

Lines changed: 60 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,13 @@
22
import sys
33
import multiprocessing
44
import json
5-
from contextlib import asynccontextmanager
6-
7-
import asyncpg
8-
import orjson
95

106
os.environ["TURBO_DISABLE_RATE_LIMITING"] = "1"
117
os.environ["TURBO_DISABLE_CACHE"] = "1"
128

13-
from turboapi import TurboAPI, Request, Response, Path, Query, HTTPException
9+
from turboapi import TurboAPI, Request, Path, Query, HTTPException
1410
from turboapi.responses import PlainTextResponse, JSONResponse
15-
from turboapi.middleware.gzip import GZipMiddleware
11+
from turboapi.middleware import GZipMiddleware
1612
from turboapi.staticfiles import StaticFiles
1713

1814
# -- Dataset and constants --------------------------------------------------------
@@ -32,76 +28,70 @@
3228

3329
# -- Postgres DB ------------------------------------------------------------
3430

35-
PG_POOL: asyncpg.Pool | None = None
36-
3731
PG_QUERY = (
3832
"SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count "
3933
"FROM items WHERE price BETWEEN $1 AND $2 LIMIT $3"
4034
)
4135

42-
class NoResetConnection(asyncpg.Connection):
43-
__slots__ = ()
44-
def get_reset_query(self):
45-
return ""
36+
PG_POOL_MIN_SIZE = 1
37+
PG_POOL_MAX_SIZE = 2
4638

47-
@asynccontextmanager
48-
async def lifespan(application: TurboAPI):
49-
global PG_POOL, NoResetConnection
50-
DATABASE_URL = os.environ.get("DATABASE_URL")
51-
if DATABASE_URL:
52-
try:
53-
if DATABASE_URL.startswith("postgres://"):
54-
DATABASE_URL = "postgresql://" + DATABASE_URL[len("postgres://"):]
55-
PG_POOL_MAX_SIZE = 2
56-
DATABASE_MAX_CONN = os.environ.get("DATABASE_MAX_CONN", None)
57-
if DATABASE_MAX_CONN:
58-
pool_size = int(DATABASE_MAX_CONN) * 0.92 / WRK_COUNT
59-
PG_POOL_MAX_SIZE = int(pool_size + 0.95)
60-
PG_POOL = await asyncpg.create_pool(
61-
dsn = DATABASE_URL,
62-
min_size = 1,
63-
max_size = max(PG_POOL_MAX_SIZE, 2),
64-
connection_class = NoResetConnection
65-
)
66-
except Exception:
67-
PG_POOL = None
68-
yield
69-
if PG_POOL:
70-
await PG_POOL.close()
71-
PG_POOL = None
39+
DATABASE_URL = os.environ.get("DATABASE_URL")
40+
if DATABASE_URL and DATABASE_URL.startswith("postgres://"):
41+
DATABASE_URL = "postgresql://" + DATABASE_URL[len("postgres://"):]
42+
43+
DATABASE_MAX_CONN = os.environ.get("DATABASE_MAX_CONN")
44+
if DATABASE_MAX_CONN:
45+
pool_size = int(DATABASE_MAX_CONN) * 0.92 / WRK_COUNT
46+
PG_POOL_MAX_SIZE = int(pool_size + 0.95)
7247

7348

74-
app = TurboAPI(lifespan=lifespan)
49+
# -- APP -----------------------------------------------------------------------
50+
51+
app = TurboAPI()
7552

7653
app.add_middleware(GZipMiddleware, minimum_size=1, compresslevel=5)
7754

55+
#if DATABASE_URL:
56+
# app.configure_db(DATABASE_URL, pool_size=PG_POOL_MAX_SIZE)
57+
7858

7959
# -- Routes ------------------------------------------------------------------
8060

8161
@app.get("/pipeline")
82-
async def pipeline():
62+
def pipeline():
8363
return PlainTextResponse(b"ok")
8464

8565

86-
@app.api_route("/baseline11", methods=["GET", "POST"])
87-
async def baseline11(request: Request):
88-
total = 0
89-
for v in request.query_params.values():
66+
@app.get("/baseline11")
67+
def baseline11(a: int, b: int):
68+
try:
69+
a = int(a)
70+
b = int(b)
71+
except Exception:
9072
try:
91-
total += int(v)
92-
except ValueError:
93-
pass
94-
if request.method == "POST":
95-
body = await request.body()
96-
if body:
97-
try:
98-
total += int(body.strip())
99-
except ValueError:
100-
pass
73+
return PlainTextResponse(f"a=<{a}> b=<{b}>")
74+
except Exception:
75+
return PlainTextResponse("-444")
76+
total = 0
77+
try:
78+
total = a + b
79+
except Exception:
80+
pass
81+
try:
82+
if request.method == "POST":
83+
body = await request.body()
84+
if body:
85+
try:
86+
total += int(body.strip())
87+
except ValueError:
88+
pass
89+
except Exception:
90+
return PlainTextResponse("-1")
10191
return PlainTextResponse(str(total))
10292

10393

104-
def json_common(request: Request, count: int, m_val: float):
94+
def json_common(count: int, m_val: float):
10595
global DATASET_ITEMS
10696
if not DATASET_ITEMS:
10797
return PlainTextResponse("No dataset", 500)
@@ -115,56 +105,33 @@ def json_common(request: Request, count: int, m_val: float):
115105
items.append(item)
116106
return JSONResponse( { "items": items, "count": len(items) } )
117107
except Exception:
118-
return JSONResponse( { "items": [ ], "count": 0 } )
108+
return JSONResponse( { "items": [ ], "count": -1 } )
119109

120110

121111
@app.get("/json/{count}")
122-
async def json_endpoint(request: Request, count: int = Path(...), m: float = Query(...)):
123-
return json_common(request, count, m)
112+
def json_endpoint(count: int, m: float):
113+
count = int(count)
114+
m = float(m)
115+
return json_common(count, m)
124116

125117

126118
@app.get("/json-comp/{count}")
127-
async def json_comp_endpoint(request: Request, count: int = Path(...), m: float = Query(...)):
128-
return json_common(request, count, m)
119+
def json_comp_endpoint(count: int, m: float):
120+
count = int(count)
121+
m = float(m)
122+
return json_common(count, m)
129123

130124

131-
@app.get("/async-db")
132-
async def async_db_endpoint(request: Request, min_val: float = Query(..., alias="min"), max_val: float = Query(..., alias="max"), limit: int = Query(...)):
133-
global PG_POOL
134-
if not PG_POOL:
135-
return JSONResponse( { "items": [ ], "count": 0 } )
136-
try:
137-
db_conn = await PG_POOL.acquire()
138-
try:
139-
rows = await db_conn.fetch(PG_QUERY, min_val, max_val, limit)
140-
finally:
141-
await PG_POOL.release(db_conn)
142-
items = [
143-
{
144-
'id' : row['id'],
145-
'name' : row['name'],
146-
'category': row['category'],
147-
'price' : row['price'],
148-
'quantity': row['quantity'],
149-
'active' : row['active'],
150-
'tags' : json.loads(row['tags']) if isinstance(row['tags'], str) else row['tags'],
151-
'rating': {
152-
'score': row['rating_score'],
153-
'count': row['rating_count'],
154-
}
155-
}
156-
for row in rows
157-
]
158-
return JSONResponse( { "items": items, "count": len(items) } )
159-
except Exception:
160-
return JSONResponse( { "items": [ ], "count": 0 } )
125+
#@app.get("/async-db")
126+
#async def async_db_endpoint(request: Request, min_val: float = Query(..., alias="min"), max_val: float = Query(..., alias="max"), limit: int = Query(...)):
127+
# return JSONResponse( { "items": [ ], "count": 0 } )
161128

162129

163130
@app.post("/upload")
164-
async def upload_endpoint(request: Request):
131+
async def upload_endpoint(): # request: Request):
165132
size = 0
166-
async for chunk in request.stream():
167-
size += len(chunk)
133+
#async for chunk in request.stream():
134+
# size += len(chunk)
168135
return PlainTextResponse(str(size))
169136

170137

@@ -175,4 +142,4 @@ async def upload_endpoint(request: Request):
175142

176143

177144
if __name__ == "__main__":
178-
app.run(host="0.0.0.0", port=8080, workers=WRK_COUNT)
145+
app.run(host="0.0.0.0", port=8080)

frameworks/turboapi/meta.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"display_name": "turboapi",
33
"language": "Python",
4-
"type": "production",
4+
"type": "tuned",
55
"engine": "TurboNet-Zig",
66
"description": "FastAPI-compatible Python framework (Zig HTTP core)",
77
"repo": "https://github.com/justrach/turboAPI",
@@ -15,8 +15,7 @@
1515
"upload",
1616
"api-4",
1717
"api-16",
18-
"async-db",
1918
"static"
2019
],
21-
"maintainers": [ "justrach" ]
20+
"maintainers": []
2221
}

0 commit comments

Comments
 (0)