From d6c7e92d11db4e926170e7828ab88e68b5bf85aa Mon Sep 17 00:00:00 2001 From: remittor Date: Sat, 18 Apr 2026 08:53:25 +0300 Subject: [PATCH 1/2] [python] Add TurboAPI framework --- frameworks/turboapi/Dockerfile | 34 +++++++ frameworks/turboapi/app.py | 178 +++++++++++++++++++++++++++++++++ frameworks/turboapi/meta.json | 22 ++++ 3 files changed, 234 insertions(+) create mode 100644 frameworks/turboapi/Dockerfile create mode 100644 frameworks/turboapi/app.py create mode 100644 frameworks/turboapi/meta.json diff --git a/frameworks/turboapi/Dockerfile b/frameworks/turboapi/Dockerfile new file mode 100644 index 000000000..cc9490b43 --- /dev/null +++ b/frameworks/turboapi/Dockerfile @@ -0,0 +1,34 @@ +FROM ubuntu:24.04 + +RUN apt-get update && apt-get install -y wget xz-utils build-essential git curl && rm -rf /var/lib/apt/lists/* + +# Zig 0.15.2 +RUN ARCH=$(uname -m) && \ + wget -q "https://ziglang.org/download/0.15.2/zig-${ARCH}-linux-0.15.2.tar.xz" && \ + tar xf zig-*.tar.xz && mv zig-*-linux-0.15.2 /opt/zig && rm zig-*.tar.xz +ENV PATH="/opt/zig:$PATH" + +# uv + Python 3.14t free-threaded +RUN curl -LsSf https://astral.sh/uv/install.sh | sh +ENV PATH="/root/.local/bin:$PATH" +RUN uv python install 3.14t && uv venv --python 3.14t /venv +ENV PATH="/venv/bin:$PATH" VIRTUAL_ENV="/venv" + +WORKDIR /turboapi + +RUN git clone --depth 1 --branch v1.0.27 https://github.com/justrach/turboAPI.git . && \ + git clone --depth 1 --branch v1.2.1 https://github.com/justrach/dhi.git /dhi + +# Install Python deps +RUN uv pip install dhi && uv pip install -e ./python + +# Build Zig backend + copy .so with correct SOABI name +RUN python zig/build_turbonet.py && \ + SOABI=$(python -c "import sysconfig; print(sysconfig.get_config_var('SOABI'))") && \ + cp zig/zig-out/lib/libturbonet.so "python/turboapi/turbonet.${SOABI}.so" + +COPY app.py /turboapi/app.py + +EXPOSE 8080 + +CMD ["/venv/bin/python", "/turboapi/app.py"] diff --git a/frameworks/turboapi/app.py b/frameworks/turboapi/app.py new file mode 100644 index 000000000..511b3168a --- /dev/null +++ b/frameworks/turboapi/app.py @@ -0,0 +1,178 @@ +import os +import sys +import multiprocessing +import json +from contextlib import asynccontextmanager + +import asyncpg +import orjson + +os.environ["TURBO_DISABLE_RATE_LIMITING"] = "1" +os.environ["TURBO_DISABLE_CACHE"] = "1" + +from turboapi import TurboAPI, Request, Response, Path, Query, HTTPException +from turboapi.responses import PlainTextResponse, JSONResponse +from turboapi.middleware.gzip import GZipMiddleware +from turboapi.staticfiles import StaticFiles + +# -- Dataset and constants -------------------------------------------------------- + +CPU_COUNT = int(multiprocessing.cpu_count()) +WRK_COUNT = min(len(os.sched_getaffinity(0)), 128) +WRK_COUNT = max(WRK_COUNT, 4) + +DATASET_LARGE_PATH = "/data/dataset-large.json" +DATASET_PATH = os.environ.get("DATASET_PATH", "/data/dataset.json") +DATASET_ITEMS = None +try: + with open(DATASET_PATH) as file: + DATASET_ITEMS = json.load(file) +except Exception: + pass + +# -- Postgres DB ------------------------------------------------------------ + +PG_POOL: asyncpg.Pool | None = None + +PG_QUERY = ( + "SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count " + "FROM items WHERE price BETWEEN $1 AND $2 LIMIT $3" +) + +class NoResetConnection(asyncpg.Connection): + __slots__ = () + def get_reset_query(self): + return "" + +@asynccontextmanager +async def lifespan(application: TurboAPI): + global PG_POOL, NoResetConnection + DATABASE_URL = os.environ.get("DATABASE_URL") + if DATABASE_URL: + try: + if DATABASE_URL.startswith("postgres://"): + DATABASE_URL = "postgresql://" + DATABASE_URL[len("postgres://"):] + PG_POOL_MAX_SIZE = 2 + DATABASE_MAX_CONN = os.environ.get("DATABASE_MAX_CONN", None) + if DATABASE_MAX_CONN: + pool_size = int(DATABASE_MAX_CONN) * 0.92 / WRK_COUNT + PG_POOL_MAX_SIZE = int(pool_size + 0.95) + PG_POOL = await asyncpg.create_pool( + dsn = DATABASE_URL, + min_size = 1, + max_size = max(PG_POOL_MAX_SIZE, 2), + connection_class = NoResetConnection + ) + except Exception: + PG_POOL = None + yield + if PG_POOL: + await PG_POOL.close() + PG_POOL = None + + +app = TurboAPI(lifespan=lifespan) + +app.add_middleware(GZipMiddleware, minimum_size=1, compresslevel=5) + + +# -- Routes ------------------------------------------------------------------ + +@app.get("/pipeline") +async def pipeline(): + return PlainTextResponse(b"ok") + + +@app.api_route("/baseline11", methods=["GET", "POST"]) +async def baseline11(request: Request): + total = 0 + for v in request.query_params.values(): + try: + total += int(v) + except ValueError: + pass + if request.method == "POST": + body = await request.body() + if body: + try: + total += int(body.strip()) + except ValueError: + pass + return PlainTextResponse(str(total)) + + +def json_common(request: Request, count: int, m_val: float): + global DATASET_ITEMS + if not DATASET_ITEMS: + return PlainTextResponse("No dataset", 500) + try: + items = [ ] + for idx, dsitem in enumerate(DATASET_ITEMS): + if idx >= count: + break + item = dict(dsitem) + item["total"] = dsitem["price"] * dsitem["quantity"] * m_val + items.append(item) + return JSONResponse( { "items": items, "count": len(items) } ) + except Exception: + return JSONResponse( { "items": [ ], "count": 0 } ) + + +@app.get("/json/{count}") +async def json_endpoint(request: Request, count: int = Path(...), m: float = Query(...)): + return json_common(request, count, m) + + +@app.get("/json-comp/{count}") +async def json_comp_endpoint(request: Request, count: int = Path(...), m: float = Query(...)): + return json_common(request, count, m) + + +@app.get("/async-db") +async def async_db_endpoint(request: Request, min_val: float = Query(..., alias="min"), max_val: float = Query(..., alias="max"), limit: int = Query(...)): + global PG_POOL + if not PG_POOL: + return JSONResponse( { "items": [ ], "count": 0 } ) + try: + db_conn = await PG_POOL.acquire() + try: + rows = await db_conn.fetch(PG_QUERY, min_val, max_val, limit) + finally: + await PG_POOL.release(db_conn) + items = [ + { + 'id' : row['id'], + 'name' : row['name'], + 'category': row['category'], + 'price' : row['price'], + 'quantity': row['quantity'], + 'active' : row['active'], + 'tags' : json.loads(row['tags']) if isinstance(row['tags'], str) else row['tags'], + 'rating': { + 'score': row['rating_score'], + 'count': row['rating_count'], + } + } + for row in rows + ] + return JSONResponse( { "items": items, "count": len(items) } ) + except Exception: + return JSONResponse( { "items": [ ], "count": 0 } ) + + +@app.post("/upload") +async def upload_endpoint(request: Request): + size = 0 + async for chunk in request.stream(): + size += len(chunk) + return PlainTextResponse(str(size)) + + +try: + app.mount("/static", StaticFiles(directory="/data/static/"), name="static") +except Exception: + pass + + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=8080, workers=WRK_COUNT) diff --git a/frameworks/turboapi/meta.json b/frameworks/turboapi/meta.json new file mode 100644 index 000000000..68fdfc7ad --- /dev/null +++ b/frameworks/turboapi/meta.json @@ -0,0 +1,22 @@ +{ + "display_name": "turboapi", + "language": "Python", + "type": "production", + "engine": "TurboNet-Zig", + "description": "FastAPI-compatible Python framework (Zig HTTP core)", + "repo": "https://github.com/justrach/turboAPI", + "enabled": true, + "tests": [ + "baseline", + "pipelined", + "limited-conn", + "json", + "json-comp", + "upload", + "api-4", + "api-16", + "async-db", + "static" + ], + "maintainers": [ "justrach" ] +} From 4986635952b04821514ebb091ff9a0ded80c3172 Mon Sep 17 00:00:00 2001 From: remittor Date: Sat, 18 Apr 2026 20:09:06 +0300 Subject: [PATCH 2/2] [python] TurboAPI: fix bugs --- frameworks/turboapi/Dockerfile | 77 ++++++++++----- frameworks/turboapi/app.py | 169 +++++++++------------------------ frameworks/turboapi/meta.json | 12 +-- 3 files changed, 101 insertions(+), 157 deletions(-) diff --git a/frameworks/turboapi/Dockerfile b/frameworks/turboapi/Dockerfile index cc9490b43..1ffd4da16 100644 --- a/frameworks/turboapi/Dockerfile +++ b/frameworks/turboapi/Dockerfile @@ -1,34 +1,63 @@ -FROM ubuntu:24.04 +# TurboAPI — Python 3.14 free-threaded + Zig 0.15 native backend +FROM python:3.14-bookworm AS builder -RUN apt-get update && apt-get install -y wget xz-utils build-essential git curl && rm -rf /var/lib/apt/lists/* +# Install Zig 0.15.2 +RUN ARCH=$(dpkg --print-architecture) \ + && if [ "$ARCH" = "arm64" ]; then ZIG_ARCH=aarch64; else ZIG_ARCH=x86_64; fi \ + && curl -fSL "https://ziglang.org/download/0.15.2/zig-${ZIG_ARCH}-linux-0.15.2.tar.xz" \ + | tar -xJ -C /opt \ + && ln -s /opt/zig-${ZIG_ARCH}-linux-0.15.2/zig /usr/local/bin/zig -# Zig 0.15.2 -RUN ARCH=$(uname -m) && \ - wget -q "https://ziglang.org/download/0.15.2/zig-${ARCH}-linux-0.15.2.tar.xz" && \ - tar xf zig-*.tar.xz && mv zig-*-linux-0.15.2 /opt/zig && rm zig-*.tar.xz -ENV PATH="/opt/zig:$PATH" +# Build Python 3.14 free-threaded from source +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev \ + libsqlite3-dev libncurses5-dev libffi-dev liblzma-dev \ + && PYVER=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}')") \ + && curl -fSL "https://www.python.org/ftp/python/${PYVER}/Python-${PYVER}.tgz" | tar xz -C /tmp \ + && cd /tmp/Python-${PYVER} \ + && ./configure --prefix=/opt/python3.14t --disable-gil --enable-shared --with-ensurepip=install \ + LDFLAGS="-Wl,-rpath,/opt/python3.14t/lib" 2>&1 | tail -5 \ + && make -j$(nproc) 2>&1 | tail -3 \ + && make install 2>&1 | tail -3 \ + && /opt/python3.14t/bin/python3 -c "import sys; assert not sys._is_gil_enabled(); print('Free-threaded OK')" \ + && rm -rf /tmp/Python-* -# uv + Python 3.14t free-threaded -RUN curl -LsSf https://astral.sh/uv/install.sh | sh -ENV PATH="/root/.local/bin:$PATH" -RUN uv python install 3.14t && uv venv --python 3.14t /venv -ENV PATH="/venv/bin:$PATH" VIRTUAL_ENV="/venv" +ENV PATH="/opt/python3.14t/bin:$PATH" -WORKDIR /turboapi +WORKDIR /app -RUN git clone --depth 1 --branch v1.0.27 https://github.com/justrach/turboAPI.git . && \ - git clone --depth 1 --branch v1.2.1 https://github.com/justrach/dhi.git /dhi +# download TurboAPI sources +#RUN git clone --depth 1 --branch v1.0.27 https://github.com/justrach/turboAPI.git . +RUN git clone https://github.com/justrach/turboAPI.git . && git checkout 1c80c67fbe002892db661247c770ad0fbd447904 -# Install Python deps -RUN uv pip install dhi && uv pip install -e ./python +# patch dhi +RUN sed -i 's|\(\.url = "\)[^"]*github\.com/justrach/dhi/[^"]*"|\1git+https://github.com/justrach/dhi?ref=main#44a3b88f37ffb095c05c668e0b2561f75d6aed1e"|' /app/zig/build.zig.zon -# Build Zig backend + copy .so with correct SOABI name -RUN python zig/build_turbonet.py && \ - SOABI=$(python -c "import sysconfig; print(sysconfig.get_config_var('SOABI'))") && \ - cp zig/zig-out/lib/libturbonet.so "python/turboapi/turbonet.${SOABI}.so" +# Build the Zig native backend (dhi fetched automatically via build.zig.zon) +RUN python3 zig/build_turbonet.py --install --release -COPY app.py /turboapi/app.py -EXPOSE 8080 +# --- Runtime stage --- -CMD ["/venv/bin/python", "/turboapi/app.py"] +FROM debian:bookworm-slim + +# Copy free-threaded Python + turboapi +COPY --from=builder /opt/python3.14t /opt/python3.14t +ENV PATH="/opt/python3.14t/bin:$PATH" + +# Runtime deps for Python +RUN apt-get update && apt-get install -y --no-install-recommends \ + libssl3 zlib1g libbz2-1.0 libreadline8 libsqlite3-0 \ + libncurses6 libffi8 liblzma5 \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app +COPY --from=builder /app /app +COPY app.py /app/app.py + +# Install turboapi + deps +RUN pip3 install --no-cache-dir -e . + +EXPOSE 8000 + +CMD ["python3", "app.py"] diff --git a/frameworks/turboapi/app.py b/frameworks/turboapi/app.py index 511b3168a..fe6c2f077 100644 --- a/frameworks/turboapi/app.py +++ b/frameworks/turboapi/app.py @@ -2,19 +2,10 @@ import sys import multiprocessing import json -from contextlib import asynccontextmanager - -import asyncpg -import orjson os.environ["TURBO_DISABLE_RATE_LIMITING"] = "1" os.environ["TURBO_DISABLE_CACHE"] = "1" -from turboapi import TurboAPI, Request, Response, Path, Query, HTTPException -from turboapi.responses import PlainTextResponse, JSONResponse -from turboapi.middleware.gzip import GZipMiddleware -from turboapi.staticfiles import StaticFiles - # -- Dataset and constants -------------------------------------------------------- CPU_COUNT = int(multiprocessing.cpu_count()) @@ -30,48 +21,28 @@ except Exception: pass -# -- Postgres DB ------------------------------------------------------------ - -PG_POOL: asyncpg.Pool | None = None - -PG_QUERY = ( - "SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count " - "FROM items WHERE price BETWEEN $1 AND $2 LIMIT $3" -) - -class NoResetConnection(asyncpg.Connection): - __slots__ = () - def get_reset_query(self): - return "" - -@asynccontextmanager -async def lifespan(application: TurboAPI): - global PG_POOL, NoResetConnection - DATABASE_URL = os.environ.get("DATABASE_URL") - if DATABASE_URL: - try: - if DATABASE_URL.startswith("postgres://"): - DATABASE_URL = "postgresql://" + DATABASE_URL[len("postgres://"):] - PG_POOL_MAX_SIZE = 2 - DATABASE_MAX_CONN = os.environ.get("DATABASE_MAX_CONN", None) - if DATABASE_MAX_CONN: - pool_size = int(DATABASE_MAX_CONN) * 0.92 / WRK_COUNT - PG_POOL_MAX_SIZE = int(pool_size + 0.95) - PG_POOL = await asyncpg.create_pool( - dsn = DATABASE_URL, - min_size = 1, - max_size = max(PG_POOL_MAX_SIZE, 2), - connection_class = NoResetConnection - ) - except Exception: - PG_POOL = None - yield - if PG_POOL: - await PG_POOL.close() - PG_POOL = None - - -app = TurboAPI(lifespan=lifespan) + +# -- APP ----------------------------------------------------------------------- + +from turboapi.request_handler import RequestBodyParser + +original_parse_json_body = RequestBodyParser.parse_json_body + +def fixed_parse_json_body(body, handler_signature): + if not body: + return { } + if body.startswith(b'{') or body.startswith(b'['): + return original_parse_json_body(body, handler_signature) + return { "_BODY_": body.decode(errors="replace") } + +RequestBodyParser.parse_json_body = staticmethod(fixed_parse_json_body) + +from turboapi import TurboAPI, Request, Path, Query, File, UploadFile, HTTPException +from turboapi.responses import PlainTextResponse, JSONResponse +from turboapi.middleware import GZipMiddleware +from turboapi.staticfiles import StaticFiles + +app = TurboAPI() app.add_middleware(GZipMiddleware, minimum_size=1, compresslevel=5) @@ -79,29 +50,21 @@ async def lifespan(application: TurboAPI): # -- Routes ------------------------------------------------------------------ @app.get("/pipeline") -async def pipeline(): +def pipeline(): return PlainTextResponse(b"ok") -@app.api_route("/baseline11", methods=["GET", "POST"]) -async def baseline11(request: Request): - total = 0 - for v in request.query_params.values(): - try: - total += int(v) - except ValueError: - pass - if request.method == "POST": - body = await request.body() - if body: - try: - total += int(body.strip()) - except ValueError: - pass - return PlainTextResponse(str(total)) - - -def json_common(request: Request, count: int, m_val: float): +@app.get("/baseline11") +def baseline11(a, b): + return PlainTextResponse( str( int(a) + int(b) ) ) + + +@app.post("/baseline11") +def baseline11body(a, b, _BODY_): + return PlainTextResponse( str( int(a) + int(b) + int(_BODY_) ) ) + + +def json_common(count: int, m_val: float): global DATASET_ITEMS if not DATASET_ITEMS: return PlainTextResponse("No dataset", 500) @@ -113,66 +76,24 @@ def json_common(request: Request, count: int, m_val: float): item = dict(dsitem) item["total"] = dsitem["price"] * dsitem["quantity"] * m_val items.append(item) - return JSONResponse( { "items": items, "count": len(items) } ) + return { "items": items, "count": len(items) } except Exception: - return JSONResponse( { "items": [ ], "count": 0 } ) + return { "items": [ ], "count": 0 } @app.get("/json/{count}") -async def json_endpoint(request: Request, count: int = Path(...), m: float = Query(...)): - return json_common(request, count, m) +def json_endpoint(count, m): + count = int(count) + m = float(m) + return json_common(count, m) @app.get("/json-comp/{count}") -async def json_comp_endpoint(request: Request, count: int = Path(...), m: float = Query(...)): - return json_common(request, count, m) - - -@app.get("/async-db") -async def async_db_endpoint(request: Request, min_val: float = Query(..., alias="min"), max_val: float = Query(..., alias="max"), limit: int = Query(...)): - global PG_POOL - if not PG_POOL: - return JSONResponse( { "items": [ ], "count": 0 } ) - try: - db_conn = await PG_POOL.acquire() - try: - rows = await db_conn.fetch(PG_QUERY, min_val, max_val, limit) - finally: - await PG_POOL.release(db_conn) - items = [ - { - 'id' : row['id'], - 'name' : row['name'], - 'category': row['category'], - 'price' : row['price'], - 'quantity': row['quantity'], - 'active' : row['active'], - 'tags' : json.loads(row['tags']) if isinstance(row['tags'], str) else row['tags'], - 'rating': { - 'score': row['rating_score'], - 'count': row['rating_count'], - } - } - for row in rows - ] - return JSONResponse( { "items": items, "count": len(items) } ) - except Exception: - return JSONResponse( { "items": [ ], "count": 0 } ) - - -@app.post("/upload") -async def upload_endpoint(request: Request): - size = 0 - async for chunk in request.stream(): - size += len(chunk) - return PlainTextResponse(str(size)) - - -try: - app.mount("/static", StaticFiles(directory="/data/static/"), name="static") -except Exception: - pass +def json_comp_endpoint(count, m): + count = int(count) + m = float(m) + return json_common(count, m) if __name__ == "__main__": - app.run(host="0.0.0.0", port=8080, workers=WRK_COUNT) + app.run(host="0.0.0.0", port=8080) diff --git a/frameworks/turboapi/meta.json b/frameworks/turboapi/meta.json index 68fdfc7ad..e1efb7d16 100644 --- a/frameworks/turboapi/meta.json +++ b/frameworks/turboapi/meta.json @@ -1,7 +1,7 @@ { "display_name": "turboapi", "language": "Python", - "type": "production", + "type": "tuned", "engine": "TurboNet-Zig", "description": "FastAPI-compatible Python framework (Zig HTTP core)", "repo": "https://github.com/justrach/turboAPI", @@ -9,14 +9,8 @@ "tests": [ "baseline", "pipelined", - "limited-conn", "json", - "json-comp", - "upload", - "api-4", - "api-16", - "async-db", - "static" + "json-comp" ], - "maintainers": [ "justrach" ] + "maintainers": [] }