freastal 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
freastal-0.0.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joseph Bylund
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.4
2
+ Name: freastal
3
+ Version: 0.0.1
4
+ Summary: Fast libuv + picohttpparser WSGI/ASGI server (Irish: freastal — service)
5
+ License: MIT
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: Programming Language :: Python :: Implementation :: CPython
8
+ Classifier: Operating System :: POSIX :: Linux
9
+ Classifier: Operating System :: MacOS
10
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Dynamic: license-file
15
+
16
+ # freastal
17
+
18
+ A fast WSGI/ASGI server for Python, built as a C extension on top of [libuv](https://libuv.org/) and [picohttpparser](https://github.com/h2o/picohttpparser). Optional TLS 1.3 via [picotls](https://github.com/h2o/picotls).
19
+
20
+ *Freastal* (IPA: /ˈfʲɾʲasˠtəl/) is Irish Gaelic for "service."
21
+
22
+ ## Performance
23
+
24
+ Benchmarked against gunicorn+uvicorn (the most common production Python stack) as baseline. 30-second runs, `wrk -t4 -c40`, 4 worker processes, ARM64 Linux.
25
+
26
+ ### 500B response
27
+
28
+ | Server | Protocol | Req/s | p50 | p99 | vs baseline |
29
+ |--------|----------|------:|----:|----:|------------:|
30
+ | **gunicorn+uvicorn** | **ASGI** | **~225k** | **156µs** | **476µs** | **1.00×** |
31
+ | bjoern | WSGI | ~370k | 90µs | 390µs | 1.65× |
32
+ | freastal | WSGI | ~424k | 78µs | 312µs | 1.88× |
33
+ | freastal | ASGI | ~408k | 81µs | 317µs | 1.81× |
34
+ | freastal | TLS 1.3 | ~421k | 78µs | 342µs | 1.87× |
35
+
36
+ ### 12KB response
37
+
38
+ | Server | Protocol | Req/s | p50 | p99 | vs baseline |
39
+ |--------|----------|------:|----:|----:|------------:|
40
+ | **gunicorn+uvicorn** | **ASGI** | **~201k** | **173µs** | **524µs** | **1.00×** |
41
+ | bjoern | WSGI | ~293k | 120µs | 360µs | 1.46× |
42
+ | freastal | WSGI | ~299k | 114µs | 391µs | 1.49× |
43
+ | freastal | ASGI | ~295k | 115µs | 409µs | 1.47× |
44
+ | freastal | TLS 1.3 | ~279k | 121µs | 555µs | 1.39× |
45
+
46
+ ## Installation
47
+
48
+ Pre-built wheels for Linux (x86\_64, aarch64) and macOS (arm64, x86\_64) are available on PyPI:
49
+
50
+ ```bash
51
+ pip install freastal
52
+ ```
53
+
54
+ Building from source requires libuv ≥ 1.44, a C compiler, and (optionally) OpenSSL for TLS support. See [Building from source](#building-from-source).
55
+
56
+ ## Usage
57
+
58
+ ### WSGI
59
+
60
+ ```python
61
+ import freastal
62
+
63
+ def app(environ, start_response):
64
+ body = b"Hello, world!"
65
+ start_response("200 OK", [("Content-Type", "text/plain")])
66
+ return [body]
67
+
68
+ freastal.serve(app, host="0.0.0.0", port=8000, workers=4)
69
+ ```
70
+
71
+ ### ASGI
72
+
73
+ ```python
74
+ import freastal
75
+
76
+ async def app(scope, receive, send):
77
+ await send({"type": "http.response.start", "status": 200,
78
+ "headers": [[b"content-type", b"text/plain"]]})
79
+ await send({"type": "http.response.body", "body": b"Hello, world!"})
80
+
81
+ freastal.serve_asgi(app, host="0.0.0.0", port=8000, workers=4)
82
+ ```
83
+
84
+ ### TLS 1.3
85
+
86
+ ```python
87
+ freastal.serve(app, host="0.0.0.0", port=8000, workers=4,
88
+ certfile="/path/to/cert.pem", keyfile="/path/to/key.pem")
89
+ ```
90
+
91
+ TLS requires OpenSSL headers at build time. Wheels published to PyPI include TLS support.
92
+
93
+ ## Architecture
94
+
95
+ - **libuv** — cross-platform event loop; io\_uring-ready on Linux (libuv ≥ 1.45 batches syscalls automatically)
96
+ - **picohttpparser** — SSE4.2/NEON SIMD HTTP/1.1 parser from the h2o project; vendored
97
+ - **picotls** — TLS 1.3 library from the h2o project; vendored, gated by `FREASTAL_TLS`
98
+ - **io_uring fixed-buffer path** (Linux, optional) — when built with `liburing`, responses > 4 KB are copied into pre-registered kernel buffers and sent with `io_uring_prep_write_fixed`, eliminating per-write `get_user_pages()` overhead. libuv ≥ 1.45 also transparently batches `accept`/`read`/`write` via io_uring regardless of this flag.
99
+ - Single `uv_write` per response — headers and body sent together, no extra copy
100
+ - HTTP/1.1 keep-alive: connections re-armed in-place without close/reopen; `TCP_NODELAY` set on every accepted socket
101
+ - Slab allocator for per-connection state — no per-request malloc on the hot path
102
+ - Pre-interned Python strings for all WSGI/ASGI environ keys
103
+ - GIL released for the duration of the libuv event loop; acquired only when calling the WSGI/ASGI application and touching Python response objects
104
+ - `SO_REUSEPORT` (`UV_TCP_REUSEPORT`) for kernel-level load balancing across worker processes
105
+
106
+ **Multi-process model:** `workers=N` forks N independent OS processes, each with its own libuv loop and Python interpreter (and therefore its own GIL). The kernel distributes incoming connections across workers via `SO_REUSEPORT`.
107
+
108
+ **ASGI event loop bridge (libuv ↔ asyncio):**
109
+
110
+ freastal runs asyncio inside the libuv event loop rather than the other way around. A `uv_check_t` steps asyncio after each I/O poll; a `uv_poll_t` on asyncio's selector fd wakes libuv when external async I/O (database calls, aiohttp, etc.) completes.
111
+
112
+ ## Building from source
113
+
114
+ ```bash
115
+ # macOS
116
+ brew install libuv openssl@3
117
+ pip install freastal --no-binary freastal
118
+
119
+ # Debian/Ubuntu
120
+ apt-get install libuv1-dev libssl-dev
121
+ pip install freastal --no-binary freastal
122
+
123
+ # Debian/Ubuntu with io_uring fixed-buffer path (Linux ≥ 5.6)
124
+ apt-get install libuv1-dev libssl-dev liburing-dev
125
+ pip install freastal --no-binary freastal
126
+ ```
127
+
128
+ picohttpparser and picotls are vendored — no extra steps required.
129
+
130
+ ## Requirements
131
+
132
+ - Python ≥ 3.10
133
+ - Linux or macOS
134
+ - libuv ≥ 1.44 (shared library, found via pkg-config or standard include paths)
135
+ - OpenSSL (optional, for TLS 1.3)
136
+
137
+ ## License
138
+
139
+ MIT
@@ -0,0 +1,124 @@
1
+ # freastal
2
+
3
+ A fast WSGI/ASGI server for Python, built as a C extension on top of [libuv](https://libuv.org/) and [picohttpparser](https://github.com/h2o/picohttpparser). Optional TLS 1.3 via [picotls](https://github.com/h2o/picotls).
4
+
5
+ *Freastal* (IPA: /ˈfʲɾʲasˠtəl/) is Irish Gaelic for "service."
6
+
7
+ ## Performance
8
+
9
+ Benchmarked against gunicorn+uvicorn (the most common production Python stack) as baseline. 30-second runs, `wrk -t4 -c40`, 4 worker processes, ARM64 Linux.
10
+
11
+ ### 500B response
12
+
13
+ | Server | Protocol | Req/s | p50 | p99 | vs baseline |
14
+ |--------|----------|------:|----:|----:|------------:|
15
+ | **gunicorn+uvicorn** | **ASGI** | **~225k** | **156µs** | **476µs** | **1.00×** |
16
+ | bjoern | WSGI | ~370k | 90µs | 390µs | 1.65× |
17
+ | freastal | WSGI | ~424k | 78µs | 312µs | 1.88× |
18
+ | freastal | ASGI | ~408k | 81µs | 317µs | 1.81× |
19
+ | freastal | TLS 1.3 | ~421k | 78µs | 342µs | 1.87× |
20
+
21
+ ### 12KB response
22
+
23
+ | Server | Protocol | Req/s | p50 | p99 | vs baseline |
24
+ |--------|----------|------:|----:|----:|------------:|
25
+ | **gunicorn+uvicorn** | **ASGI** | **~201k** | **173µs** | **524µs** | **1.00×** |
26
+ | bjoern | WSGI | ~293k | 120µs | 360µs | 1.46× |
27
+ | freastal | WSGI | ~299k | 114µs | 391µs | 1.49× |
28
+ | freastal | ASGI | ~295k | 115µs | 409µs | 1.47× |
29
+ | freastal | TLS 1.3 | ~279k | 121µs | 555µs | 1.39× |
30
+
31
+ ## Installation
32
+
33
+ Pre-built wheels for Linux (x86\_64, aarch64) and macOS (arm64, x86\_64) are available on PyPI:
34
+
35
+ ```bash
36
+ pip install freastal
37
+ ```
38
+
39
+ Building from source requires libuv ≥ 1.44, a C compiler, and (optionally) OpenSSL for TLS support. See [Building from source](#building-from-source).
40
+
41
+ ## Usage
42
+
43
+ ### WSGI
44
+
45
+ ```python
46
+ import freastal
47
+
48
+ def app(environ, start_response):
49
+ body = b"Hello, world!"
50
+ start_response("200 OK", [("Content-Type", "text/plain")])
51
+ return [body]
52
+
53
+ freastal.serve(app, host="0.0.0.0", port=8000, workers=4)
54
+ ```
55
+
56
+ ### ASGI
57
+
58
+ ```python
59
+ import freastal
60
+
61
+ async def app(scope, receive, send):
62
+ await send({"type": "http.response.start", "status": 200,
63
+ "headers": [[b"content-type", b"text/plain"]]})
64
+ await send({"type": "http.response.body", "body": b"Hello, world!"})
65
+
66
+ freastal.serve_asgi(app, host="0.0.0.0", port=8000, workers=4)
67
+ ```
68
+
69
+ ### TLS 1.3
70
+
71
+ ```python
72
+ freastal.serve(app, host="0.0.0.0", port=8000, workers=4,
73
+ certfile="/path/to/cert.pem", keyfile="/path/to/key.pem")
74
+ ```
75
+
76
+ TLS requires OpenSSL headers at build time. Wheels published to PyPI include TLS support.
77
+
78
+ ## Architecture
79
+
80
+ - **libuv** — cross-platform event loop; io\_uring-ready on Linux (libuv ≥ 1.45 batches syscalls automatically)
81
+ - **picohttpparser** — SSE4.2/NEON SIMD HTTP/1.1 parser from the h2o project; vendored
82
+ - **picotls** — TLS 1.3 library from the h2o project; vendored, gated by `FREASTAL_TLS`
83
+ - **io_uring fixed-buffer path** (Linux, optional) — when built with `liburing`, responses > 4 KB are copied into pre-registered kernel buffers and sent with `io_uring_prep_write_fixed`, eliminating per-write `get_user_pages()` overhead. libuv ≥ 1.45 also transparently batches `accept`/`read`/`write` via io_uring regardless of this flag.
84
+ - Single `uv_write` per response — headers and body sent together, no extra copy
85
+ - HTTP/1.1 keep-alive: connections re-armed in-place without close/reopen; `TCP_NODELAY` set on every accepted socket
86
+ - Slab allocator for per-connection state — no per-request malloc on the hot path
87
+ - Pre-interned Python strings for all WSGI/ASGI environ keys
88
+ - GIL released for the duration of the libuv event loop; acquired only when calling the WSGI/ASGI application and touching Python response objects
89
+ - `SO_REUSEPORT` (`UV_TCP_REUSEPORT`) for kernel-level load balancing across worker processes
90
+
91
+ **Multi-process model:** `workers=N` forks N independent OS processes, each with its own libuv loop and Python interpreter (and therefore its own GIL). The kernel distributes incoming connections across workers via `SO_REUSEPORT`.
92
+
93
+ **ASGI event loop bridge (libuv ↔ asyncio):**
94
+
95
+ freastal runs asyncio inside the libuv event loop rather than the other way around. A `uv_check_t` steps asyncio after each I/O poll; a `uv_poll_t` on asyncio's selector fd wakes libuv when external async I/O (database calls, aiohttp, etc.) completes.
96
+
97
+ ## Building from source
98
+
99
+ ```bash
100
+ # macOS
101
+ brew install libuv openssl@3
102
+ pip install freastal --no-binary freastal
103
+
104
+ # Debian/Ubuntu
105
+ apt-get install libuv1-dev libssl-dev
106
+ pip install freastal --no-binary freastal
107
+
108
+ # Debian/Ubuntu with io_uring fixed-buffer path (Linux ≥ 5.6)
109
+ apt-get install libuv1-dev libssl-dev liburing-dev
110
+ pip install freastal --no-binary freastal
111
+ ```
112
+
113
+ picohttpparser and picotls are vendored — no extra steps required.
114
+
115
+ ## Requirements
116
+
117
+ - Python ≥ 3.10
118
+ - Linux or macOS
119
+ - libuv ≥ 1.44 (shared library, found via pkg-config or standard include paths)
120
+ - OpenSSL (optional, for TLS 1.3)
121
+
122
+ ## License
123
+
124
+ MIT
@@ -0,0 +1,126 @@
1
+ """freastal – libuv + picohttpparser WSGI/ASGI server."""
2
+
3
+ import asyncio
4
+ import os
5
+ import signal
6
+ import sys
7
+ import multiprocessing
8
+ import time
9
+
10
+ from ._freastal import (
11
+ serve as _serve_single,
12
+ serve_asgi as _serve_asgi_single,
13
+ __version__,
14
+ )
15
+
16
+ __all__ = ["serve", "serve_asgi", "__version__"]
17
+
18
+
19
+ def serve(
20
+ app,
21
+ host="0.0.0.0",
22
+ port=8000,
23
+ workers=1,
24
+ reuse_port=True,
25
+ certfile=None,
26
+ keyfile=None,
27
+ ):
28
+ """Start freastal.
29
+
30
+ With workers=1 (default) runs in-process.
31
+ With workers>1 forks worker processes, each binding with SO_REUSEPORT
32
+ so the kernel load-balances connections across them.
33
+ Pass certfile and keyfile (PEM paths) to enable TLS 1.3 (requires picotls).
34
+ """
35
+ if workers <= 1:
36
+ _serve_single(
37
+ app,
38
+ host=host,
39
+ port=port,
40
+ reuse_port=reuse_port,
41
+ certfile=certfile,
42
+ keyfile=keyfile,
43
+ )
44
+ return
45
+
46
+ processes = []
47
+
48
+ def _worker(worker_id):
49
+ print(f"[freastal] worker {worker_id} pid={os.getpid()} starting", flush=True)
50
+ try:
51
+ _serve_single(
52
+ app,
53
+ host=host,
54
+ port=port,
55
+ reuse_port=True,
56
+ certfile=certfile,
57
+ keyfile=keyfile,
58
+ )
59
+ except KeyboardInterrupt:
60
+ pass
61
+
62
+ def _shutdown(sig, frame):
63
+ for p in processes:
64
+ p.terminate()
65
+ for p in processes:
66
+ p.join(timeout=5)
67
+ sys.exit(0)
68
+
69
+ signal.signal(signal.SIGINT, _shutdown)
70
+ signal.signal(signal.SIGTERM, _shutdown)
71
+
72
+ for i in range(workers):
73
+ p = multiprocessing.Process(target=_worker, args=(i + 1,), daemon=True)
74
+ p.start()
75
+ processes.append(p)
76
+
77
+ for p in processes:
78
+ p.join()
79
+
80
+
81
+ def serve_asgi(app, host="0.0.0.0", port=8000, workers=1, reuse_port=True):
82
+ """Start freastal in ASGI mode.
83
+
84
+ With workers=1 runs in-process.
85
+ With workers>1 forks worker processes using SO_REUSEPORT.
86
+ Each worker creates its own asyncio event loop.
87
+ """
88
+
89
+ def _run_single():
90
+ loop = asyncio.new_event_loop()
91
+ asyncio.set_event_loop(loop)
92
+ _serve_asgi_single(app, loop, host=host, port=port, reuse_port=reuse_port)
93
+
94
+ if workers <= 1:
95
+ _run_single()
96
+ return
97
+
98
+ processes = []
99
+
100
+ def _worker(worker_id):
101
+ print(
102
+ f"[freastal] ASGI worker {worker_id} pid={os.getpid()} starting", flush=True
103
+ )
104
+ try:
105
+ _run_single()
106
+ except KeyboardInterrupt:
107
+ pass
108
+
109
+ def _shutdown(sig, frame):
110
+ for p in processes:
111
+ p.terminate()
112
+ for p in processes:
113
+ p.join(timeout=5)
114
+ sys.exit(0)
115
+
116
+ signal.signal(signal.SIGINT, _shutdown)
117
+ signal.signal(signal.SIGTERM, _shutdown)
118
+
119
+ for i in range(workers):
120
+ p = multiprocessing.Process(target=_worker, args=(i + 1,), daemon=True)
121
+ p.start()
122
+ processes.append(p)
123
+ time.sleep(0.05)
124
+
125
+ for p in processes:
126
+ p.join()
@@ -0,0 +1,42 @@
1
+ """Pure-Python ASGI protocol bridge for freastal.
2
+
3
+ run_asgi_request() is called from C (asgi_dispatch) once per HTTP request.
4
+ It creates the receive/send coroutines and schedules the ASGI app as an
5
+ asyncio Task on the loop that freastal is stepping from its uv_check_t callback.
6
+ """
7
+
8
+ from ._freastal import asgi_send_response
9
+
10
+
11
+ def run_asgi_request(loop, app, scope, body, capsule):
12
+ """Schedule `app(scope, receive, send)` as a Task and return it.
13
+
14
+ The Task runs inside the asyncio loop that freastal drives from
15
+ uv_check_t / uv_poll_t callbacks. Because receive() returns immediately
16
+ and send() calls back into C synchronously, a simple ASGI app completes
17
+ in one _run_once() step with no event-loop round-trips.
18
+
19
+ Apps that do real async I/O (await aiohttp.get(...), await db.query(...))
20
+ work normally: their futures are resolved by asyncio's selector, which
21
+ freastal monitors via a uv_poll_t on asyncio's selector fd.
22
+ """
23
+ status_cell = [None]
24
+ headers_cell = [None]
25
+
26
+ async def receive():
27
+ return {"type": "http.request", "body": body, "more_body": False}
28
+
29
+ async def send(event):
30
+ t = event["type"]
31
+ if t == "http.response.start":
32
+ status_cell[0] = event["status"]
33
+ headers_cell[0] = list(event.get("headers", []))
34
+ elif t == "http.response.body":
35
+ asgi_send_response(
36
+ capsule,
37
+ status_cell[0],
38
+ headers_cell[0],
39
+ event.get("body", b""),
40
+ )
41
+
42
+ return loop.create_task(app(scope, receive, send))