Skip to content

Commit a2e3ab1

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

3 files changed

Lines changed: 125 additions & 147 deletions

File tree

frameworks/turboapi/Dockerfile

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,63 @@
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 .
31+
RUN git clone https://github.com/justrach/turboAPI.git . && git checkout 1c80c67fbe002892db661247c770ad0fbd447904
2132

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

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"
36+
# Build the Zig native backend (dhi fetched automatically via build.zig.zon)
37+
RUN python3 zig/build_turbonet.py --install --release
2938

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

32-
EXPOSE 8080
40+
# --- Runtime stage ---
3341

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

frameworks/turboapi/app.py

Lines changed: 69 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,10 @@
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
14-
from turboapi.responses import PlainTextResponse, JSONResponse
15-
from turboapi.middleware.gzip import GZipMiddleware
16-
from turboapi.staticfiles import StaticFiles
17-
189
# -- Dataset and constants --------------------------------------------------------
1910

2011
CPU_COUNT = int(multiprocessing.cpu_count())
@@ -30,78 +21,84 @@
3021
except Exception:
3122
pass
3223

33-
# -- Postgres DB ------------------------------------------------------------
3424

35-
PG_POOL: asyncpg.Pool | None = None
25+
# -- APP -----------------------------------------------------------------------
3626

37-
PG_QUERY = (
38-
"SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count "
39-
"FROM items WHERE price BETWEEN $1 AND $2 LIMIT $3"
40-
)
27+
from turboapi.request_handler import RequestBodyParser
4128

42-
class NoResetConnection(asyncpg.Connection):
43-
__slots__ = ()
44-
def get_reset_query(self):
45-
return ""
29+
original_parse_json_body = RequestBodyParser.parse_json_body
4630

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
31+
def fixed_parse_json_body(body, handler_signature):
32+
return original_parse_json_body(body, handler_signature)
33+
'''
34+
if not body:
35+
return { }
36+
if body.startswith(b'{') or body.startswith(b'['):
37+
return original_parse_json_body(body, handler_signature)
38+
return { "_BODY_": body }
39+
'''
40+
RequestBodyParser.parse_json_body = staticmethod(fixed_parse_json_body)
7241

42+
from turboapi import TurboAPI, Request, Path, Query, File, UploadFile, HTTPException
43+
from turboapi.responses import PlainTextResponse, JSONResponse
44+
from turboapi.middleware import GZipMiddleware
45+
from turboapi.staticfiles import StaticFiles
7346

74-
app = TurboAPI(lifespan=lifespan)
47+
app = TurboAPI()
7548

7649
app.add_middleware(GZipMiddleware, minimum_size=1, compresslevel=5)
7750

7851

7952
# -- Routes ------------------------------------------------------------------
8053

8154
@app.get("/pipeline")
82-
async def pipeline():
55+
def pipeline():
8356
return PlainTextResponse(b"ok")
8457

8558

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():
59+
@app.get("/baseline11")
60+
def baseline11(a: str = Query(), b: str = Query()):
61+
try:
62+
return PlainTextResponse( str( int(a) + int(b) ) )
63+
except Exception:
64+
return PlainTextResponse("-444")
65+
66+
67+
@app.post("/baseline11")
68+
def baseline11(a, b, _BODY_):
69+
try:
70+
return PlainTextResponse( str( int(a) + int(b) ) + f' body = {_BODY_}' )
71+
except Exception:
72+
return PlainTextResponse("-555")
73+
74+
'''
75+
try:
76+
a = int(a)
77+
b = int(b)
78+
except Exception:
9079
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
80+
return PlainTextResponse(f"a=<{a}> b=<{b}>")
81+
except Exception:
82+
return PlainTextResponse("-444")
83+
total = 0
84+
try:
85+
total = a + b
86+
except Exception:
87+
pass
88+
try:
89+
if request.method == "POST":
90+
body = request.body.read()
91+
if body:
92+
try:
93+
total += int(body.strip())
94+
except ValueError:
95+
pass
96+
except Exception:
97+
return PlainTextResponse("-1")
10198
return PlainTextResponse(str(total))
10299
103100
104-
def json_common(request: Request, count: int, m_val: float):
101+
def json_common(count: int, m_val: float):
105102
global DATASET_ITEMS
106103
if not DATASET_ITEMS:
107104
return PlainTextResponse("No dataset", 500)
@@ -115,64 +112,22 @@ def json_common(request: Request, count: int, m_val: float):
115112
items.append(item)
116113
return JSONResponse( { "items": items, "count": len(items) } )
117114
except Exception:
118-
return JSONResponse( { "items": [ ], "count": 0 } )
115+
return JSONResponse( { "items": [ ], "count": -1 } )
119116
120117
121118
@app.get("/json/{count}")
122-
async def json_endpoint(request: Request, count: int = Path(...), m: float = Query(...)):
123-
return json_common(request, count, m)
119+
def json_endpoint(count: int, m: float):
120+
count = int(count)
121+
m = float(m)
122+
return json_common(count, m)
124123
125124
126125
@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)
129-
130-
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 } )
161-
162-
163-
@app.post("/upload")
164-
async def upload_endpoint(request: Request):
165-
size = 0
166-
async for chunk in request.stream():
167-
size += len(chunk)
168-
return PlainTextResponse(str(size))
169-
170-
171-
try:
172-
app.mount("/static", StaticFiles(directory="/data/static/"), name="static")
173-
except Exception:
174-
pass
175-
126+
def json_comp_endpoint(count: int, m: float):
127+
count = int(count)
128+
m = float(m)
129+
return json_common(count, m)
130+
'''
176131

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

frameworks/turboapi/meta.json

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,16 @@
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",
88
"enabled": true,
99
"tests": [
1010
"baseline",
1111
"pipelined",
12-
"limited-conn",
1312
"json",
14-
"json-comp",
15-
"upload",
16-
"api-4",
17-
"api-16",
18-
"async-db",
19-
"static"
13+
"json-comp"
2014
],
21-
"maintainers": [ "justrach" ]
15+
"maintainers": []
2216
}

0 commit comments

Comments
 (0)