diff --git a/frameworks/turboapi/Dockerfile b/frameworks/turboapi/Dockerfile new file mode 100644 index 000000000..1ffd4da16 --- /dev/null +++ b/frameworks/turboapi/Dockerfile @@ -0,0 +1,63 @@ +# TurboAPI — Python 3.14 free-threaded + Zig 0.15 native backend +FROM python:3.14-bookworm AS builder + +# 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 + +# 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-* + +ENV PATH="/opt/python3.14t/bin:$PATH" + +WORKDIR /app + +# 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 + +# 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 the Zig native backend (dhi fetched automatically via build.zig.zon) +RUN python3 zig/build_turbonet.py --install --release + + +# --- Runtime stage --- + +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 new file mode 100644 index 000000000..fe6c2f077 --- /dev/null +++ b/frameworks/turboapi/app.py @@ -0,0 +1,99 @@ +import os +import sys +import multiprocessing +import json + +os.environ["TURBO_DISABLE_RATE_LIMITING"] = "1" +os.environ["TURBO_DISABLE_CACHE"] = "1" + +# -- 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 + + +# -- 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) + + +# -- Routes ------------------------------------------------------------------ + +@app.get("/pipeline") +def pipeline(): + return PlainTextResponse(b"ok") + + +@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) + 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 { "items": items, "count": len(items) } + except Exception: + return { "items": [ ], "count": 0 } + + +@app.get("/json/{count}") +def json_endpoint(count, m): + count = int(count) + m = float(m) + return json_common(count, m) + + +@app.get("/json-comp/{count}") +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) diff --git a/frameworks/turboapi/meta.json b/frameworks/turboapi/meta.json new file mode 100644 index 000000000..e1efb7d16 --- /dev/null +++ b/frameworks/turboapi/meta.json @@ -0,0 +1,16 @@ +{ + "display_name": "turboapi", + "language": "Python", + "type": "tuned", + "engine": "TurboNet-Zig", + "description": "FastAPI-compatible Python framework (Zig HTTP core)", + "repo": "https://github.com/justrach/turboAPI", + "enabled": true, + "tests": [ + "baseline", + "pipelined", + "json", + "json-comp" + ], + "maintainers": [] +}