railpy 0.1.0__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.
- railpy-0.1.0/LICENSE +21 -0
- railpy-0.1.0/PKG-INFO +690 -0
- railpy-0.1.0/README.md +651 -0
- railpy-0.1.0/pyproject.toml +75 -0
- railpy-0.1.0/setup.cfg +4 -0
- railpy-0.1.0/src/examples/main.py +127 -0
- railpy-0.1.0/src/railpy/__init__.py +58 -0
- railpy-0.1.0/src/railpy/adapter/lambda_adapter.py +70 -0
- railpy-0.1.0/src/railpy/core/app.py +263 -0
- railpy-0.1.0/src/railpy/core/asgi.py +100 -0
- railpy-0.1.0/src/railpy/core/compose.py +49 -0
- railpy-0.1.0/src/railpy/core/context.py +207 -0
- railpy-0.1.0/src/railpy/core/decorators.py +115 -0
- railpy-0.1.0/src/railpy/core/decorators_resovler.py +165 -0
- railpy-0.1.0/src/railpy/core/hooks.py +27 -0
- railpy-0.1.0/src/railpy/core/logger.py +52 -0
- railpy-0.1.0/src/railpy/core/openapi.py +138 -0
- railpy-0.1.0/src/railpy/core/pipeline.py +81 -0
- railpy-0.1.0/src/railpy/core/radix_router.py +174 -0
- railpy-0.1.0/src/railpy/core/utils.py +35 -0
- railpy-0.1.0/src/railpy/core/wrap.py +49 -0
- railpy-0.1.0/src/railpy/middlewares/__init__.py +30 -0
- railpy-0.1.0/src/railpy/middlewares/body_parser.py +127 -0
- railpy-0.1.0/src/railpy/middlewares/cors.py +52 -0
- railpy-0.1.0/src/railpy/middlewares/error_handler.py +25 -0
- railpy-0.1.0/src/railpy/middlewares/helmet.py +30 -0
- railpy-0.1.0/src/railpy/middlewares/json_parser.py +76 -0
- railpy-0.1.0/src/railpy/middlewares/jwt_auth.py +52 -0
- railpy-0.1.0/src/railpy/middlewares/logger.py +31 -0
- railpy-0.1.0/src/railpy/middlewares/normalize_headers.py +25 -0
- railpy-0.1.0/src/railpy/middlewares/query_parser.py +28 -0
- railpy-0.1.0/src/railpy/middlewares/rate_limit.py +56 -0
- railpy-0.1.0/src/railpy/middlewares/serve_static.py +48 -0
- railpy-0.1.0/src/railpy/middlewares/session.py +86 -0
- railpy-0.1.0/src/railpy.egg-info/PKG-INFO +690 -0
- railpy-0.1.0/src/railpy.egg-info/SOURCES.txt +37 -0
- railpy-0.1.0/src/railpy.egg-info/dependency_links.txt +1 -0
- railpy-0.1.0/src/railpy.egg-info/requires.txt +13 -0
- railpy-0.1.0/src/railpy.egg-info/top_level.txt +2 -0
railpy-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Delpi.Kye
|
|
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.
|
railpy-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: railpy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Deterministic high-performance ASGI engine for modern Python backends
|
|
5
|
+
Author: Delpi.Kye
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/delpikye-v/railpy
|
|
8
|
+
Project-URL: Repository, https://github.com/delpikye-v/railpy
|
|
9
|
+
Project-URL: Documentation, https://github.com/delpikye-v/railpy#readme
|
|
10
|
+
Project-URL: Issues, https://github.com/delpikye-v/railpy/issues
|
|
11
|
+
Keywords: asgi,web-framework,python,backend,fastapi-alternative,flask-alternative,microframework
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Framework :: AsyncIO
|
|
19
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
20
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Operating System :: OS Independent
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: uvicorn>=0.23
|
|
27
|
+
Requires-Dist: pydantic>=2.0
|
|
28
|
+
Requires-Dist: pyjwt>=2.7
|
|
29
|
+
Requires-Dist: itsdangerous>=2.1
|
|
30
|
+
Requires-Dist: aiofiles>=23.1
|
|
31
|
+
Requires-Dist: cryptography>=41.0
|
|
32
|
+
Requires-Dist: python-multipart>=0.0.6
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: pytest; extra == "dev"
|
|
35
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
36
|
+
Requires-Dist: black; extra == "dev"
|
|
37
|
+
Requires-Dist: ruff; extra == "dev"
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
# 🌐 railpy
|
|
41
|
+
|
|
42
|
+
  
|
|
43
|
+
|
|
44
|
+
[LIVE EXAMPLE](https://codesandbox.io/p/devbox/ncdwx6)
|
|
45
|
+
|
|
46
|
+
**A deterministic, high-performance ASGI engine** for modern Python backends.
|
|
47
|
+
|
|
48
|
+
> Railpy is **not just a framework** --- it is an **execution engine**
|
|
49
|
+
> for building scalable backend architectures.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
# Why railpy?
|
|
54
|
+
|
|
55
|
+
Most Python frameworks force trade-offs.
|
|
56
|
+
|
|
57
|
+
| Framework | Problem |
|
|
58
|
+
| --------- | ------------------------- |
|
|
59
|
+
| Flask | Simple but messy at scale |
|
|
60
|
+
| Django | Powerful but heavy |
|
|
61
|
+
| FastAPI | Great DX but opinionated |
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
👉 **Railpy** gives you **control + performance + predictable
|
|
65
|
+
execution**.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
# Advantages
|
|
70
|
+
|
|
71
|
+
⚡ Radix-tree router (O(k))\
|
|
72
|
+
🧠 Deterministic middleware execution\
|
|
73
|
+
🧩 Pipeline-based orchestration\
|
|
74
|
+
🔌 Middleware-first architecture\
|
|
75
|
+
🪶 Lightweight ASGI core\
|
|
76
|
+
📝 Pydantic validation support\
|
|
77
|
+
🌐 Works with Uvicorn / ASGI / Lambda
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
# Mental Model
|
|
82
|
+
```plaintext
|
|
83
|
+
Request
|
|
84
|
+
↓
|
|
85
|
+
Context (ctx)
|
|
86
|
+
↓
|
|
87
|
+
Middleware Pipeline
|
|
88
|
+
↓
|
|
89
|
+
Router (Radix)
|
|
90
|
+
↓
|
|
91
|
+
Handler
|
|
92
|
+
↓
|
|
93
|
+
Response
|
|
94
|
+
```
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
# Installation
|
|
98
|
+
|
|
99
|
+
``` plaintext
|
|
100
|
+
pip install railpy
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
# Quick
|
|
106
|
+
|
|
107
|
+
``` python
|
|
108
|
+
from railpy import Railpy
|
|
109
|
+
from railpy.middleware import logger, cors
|
|
110
|
+
|
|
111
|
+
app = Railpy()
|
|
112
|
+
|
|
113
|
+
app.use(logger)
|
|
114
|
+
app.use(cors())
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@app.get("/")
|
|
118
|
+
async def hello(ctx):
|
|
119
|
+
return {"message": "Hello Railpy 🚀"}
|
|
120
|
+
|
|
121
|
+
@app.get("/users/:id")
|
|
122
|
+
async def get_user(ctx):
|
|
123
|
+
return {
|
|
124
|
+
"id": ctx.params["id"]
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
app.run()
|
|
128
|
+
|
|
129
|
+
# uvicorn main:app
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
# Context API
|
|
135
|
+
|
|
136
|
+
Railpy provides a powerful request context.
|
|
137
|
+
|
|
138
|
+
| Method | Description |
|
|
139
|
+
| ---------------------- | --------------------- |
|
|
140
|
+
| `ctx.params` | URL parameters |
|
|
141
|
+
| `ctx.query` | Query string |
|
|
142
|
+
| `ctx.data["body"]` | Parsed request body |
|
|
143
|
+
| `ctx.state` | Request state storage |
|
|
144
|
+
| `ctx.json()` | Send JSON |
|
|
145
|
+
| `ctx.text()` | Send text |
|
|
146
|
+
| `ctx.html()` | Send HTML |
|
|
147
|
+
| `ctx.file()` | Send / download file |
|
|
148
|
+
| `ctx.redirect()` | Redirect |
|
|
149
|
+
| `ctx.ok()` | 200 response |
|
|
150
|
+
| `ctx.created()` | 201 response |
|
|
151
|
+
| `ctx.no_content()` | 204 response |
|
|
152
|
+
| `ctx.bad_request()` | 400 response |
|
|
153
|
+
| `ctx.unauthorized()` | 401 response |
|
|
154
|
+
| `ctx.forbidden()` | 403 response |
|
|
155
|
+
| `ctx.not_found()` | 404 response |
|
|
156
|
+
| `ctx.internal_error()` | 500 response |
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
# File Download (Railpy Feature)
|
|
162
|
+
|
|
163
|
+
Railpy has built-in **file download support**.
|
|
164
|
+
|
|
165
|
+
``` python
|
|
166
|
+
@app.get("/download")
|
|
167
|
+
async def download(ctx):
|
|
168
|
+
ctx.file(
|
|
169
|
+
"./files/report.pdf",
|
|
170
|
+
filename="monthly-report.pdf"
|
|
171
|
+
)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
This automatically sets:
|
|
175
|
+
|
|
176
|
+
```plaintext
|
|
177
|
+
Content-Disposition: attachment
|
|
178
|
+
Content-Type: application/pdf
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Browsers will **download the file automatically**.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
# Routing
|
|
186
|
+
|
|
187
|
+
``` python
|
|
188
|
+
@app.get("/users")
|
|
189
|
+
async def users(ctx):
|
|
190
|
+
return {"users": []}
|
|
191
|
+
|
|
192
|
+
@app.get("/users/:id")
|
|
193
|
+
async def user(ctx):
|
|
194
|
+
return {"id": ctx.params["id"]}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
# Route Groups
|
|
200
|
+
```python
|
|
201
|
+
app.group("/admin", lambda r: (
|
|
202
|
+
r.get("/dashboard", dashboard_handler),
|
|
203
|
+
r.get("/users/:id", admin_user_handler)
|
|
204
|
+
))
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
# Validation (Pydantic)
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
from pydantic import BaseModel
|
|
213
|
+
from railpy import ValidateBody
|
|
214
|
+
|
|
215
|
+
class UserCreate(BaseModel):
|
|
216
|
+
name: str
|
|
217
|
+
email: str
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@app.post("/users")
|
|
221
|
+
@ValidateBody(UserCreate)
|
|
222
|
+
async def create_user(ctx, body: UserCreate):
|
|
223
|
+
return body
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
# Controller (Decorator Style)
|
|
230
|
+
|
|
231
|
+
``` python
|
|
232
|
+
from railpy import Controller, Get
|
|
233
|
+
|
|
234
|
+
@Controller("/users")
|
|
235
|
+
class UserController:
|
|
236
|
+
|
|
237
|
+
@Get("/")
|
|
238
|
+
async def list(self, ctx):
|
|
239
|
+
return {"users": []}
|
|
240
|
+
|
|
241
|
+
@Get("/:id")
|
|
242
|
+
async def get(self, ctx, id: int):
|
|
243
|
+
return {"id": id}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Register controller:
|
|
247
|
+
|
|
248
|
+
``` python
|
|
249
|
+
from railpy import register_controller
|
|
250
|
+
|
|
251
|
+
register_controller(app, UserController)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
# Swagger / OpenAPI
|
|
257
|
+
|
|
258
|
+
``` python
|
|
259
|
+
from railpy import setup_swagger
|
|
260
|
+
|
|
261
|
+
setup_swagger(app)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Docs:
|
|
265
|
+
```
|
|
266
|
+
/docs
|
|
267
|
+
```
|
|
268
|
+
OpenAPI JSON:
|
|
269
|
+
```
|
|
270
|
+
/openapi.json
|
|
271
|
+
```
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# Background Tasks
|
|
276
|
+
|
|
277
|
+
Railpy allows you to run asynchronous **background tasks** after the response is sent.
|
|
278
|
+
This is useful for sending emails, logging, or other post-response operations without delaying the client.
|
|
279
|
+
|
|
280
|
+
### Usage
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
from railpy import Railpy, Context
|
|
284
|
+
import asyncio
|
|
285
|
+
|
|
286
|
+
app = Railpy()
|
|
287
|
+
|
|
288
|
+
async def send_email(user_id: int):
|
|
289
|
+
await asyncio.sleep(1) # simulate async operation
|
|
290
|
+
print(f"Email sent to user {user_id}")
|
|
291
|
+
|
|
292
|
+
async def register_user(ctx: Context):
|
|
293
|
+
user_id = ctx.params["id"]
|
|
294
|
+
|
|
295
|
+
# Define background task
|
|
296
|
+
async def send_email(u_id):
|
|
297
|
+
await asyncio.sleep(1) # simulate async operation
|
|
298
|
+
print(f"Email sent to {u_id}")
|
|
299
|
+
|
|
300
|
+
# Add the background task
|
|
301
|
+
ctx.background_tasks.append((send_email, (user_id,), {}))
|
|
302
|
+
|
|
303
|
+
return {"message": "User registered"}
|
|
304
|
+
|
|
305
|
+
# Register the route with Railpy
|
|
306
|
+
app.get("/register/:id", register_user)
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
How it works
|
|
310
|
+
|
|
311
|
+
- During request processing, append tasks to ctx.background_tasks.
|
|
312
|
+
Each task is a tuple: (callable, args, kwargs).
|
|
313
|
+
- After the response is finalized, Railpy automatically runs each task with asyncio.create_task().
|
|
314
|
+
- Tasks run concurrently and do not block the client.
|
|
315
|
+
|
|
316
|
+
```python
|
|
317
|
+
ctx.background_tasks = [
|
|
318
|
+
(some_async_fn, (arg1, arg2), {"kwarg1": "value"}),
|
|
319
|
+
# (another_async_fn, (), {}),
|
|
320
|
+
]
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
# Lambda Support
|
|
326
|
+
|
|
327
|
+
``` python
|
|
328
|
+
from railpy import Railpy, RailpyLambda
|
|
329
|
+
|
|
330
|
+
app = Railpy()
|
|
331
|
+
|
|
332
|
+
@app.get("/")
|
|
333
|
+
async def hello(ctx):
|
|
334
|
+
return {"message": "Hello from Lambda 🚀"}
|
|
335
|
+
|
|
336
|
+
handler = RailpyLambda(app)
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Deploy with:
|
|
340
|
+
```plaintext
|
|
341
|
+
AWS Lambda
|
|
342
|
+
+ API Gateway
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Your handler:
|
|
346
|
+
```plaintext
|
|
347
|
+
handler.handler
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Directory structure:
|
|
351
|
+
```plaintext
|
|
352
|
+
project/
|
|
353
|
+
├── main.py
|
|
354
|
+
└── requirements.txt
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
## Serverless Architecture
|
|
358
|
+
```plaintext
|
|
359
|
+
API Gateway
|
|
360
|
+
↓
|
|
361
|
+
AWS Lambda
|
|
362
|
+
↓
|
|
363
|
+
RailpyLambda Adapter
|
|
364
|
+
↓
|
|
365
|
+
Railpy Core
|
|
366
|
+
↓
|
|
367
|
+
Router → Handler
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
# Railpy Architecture
|
|
373
|
+
```plaintext
|
|
374
|
+
ASGI
|
|
375
|
+
↓
|
|
376
|
+
Railpy Core
|
|
377
|
+
↓
|
|
378
|
+
Middleware Pipeline
|
|
379
|
+
↓
|
|
380
|
+
Radix Router
|
|
381
|
+
↓
|
|
382
|
+
Handler
|
|
383
|
+
↓
|
|
384
|
+
Response
|
|
385
|
+
```
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
# Ecosystem Middleware
|
|
389
|
+
|
|
390
|
+
Railpy uses a pipeline-first middleware architecture. Key middleware include:
|
|
391
|
+
|
|
392
|
+
| Middleware | Purpose |
|
|
393
|
+
| ---------------------- | ------------------------------------------------------------- |
|
|
394
|
+
| `logger()` | Logs requests and response times |
|
|
395
|
+
| `error_handler()` | Global error boundary |
|
|
396
|
+
| `cors()` | Adds CORS headers |
|
|
397
|
+
| `body_parser()` | Parses JSON / form / multipart bodies |
|
|
398
|
+
| `json_parser()` | JSON body parser with size limit and strict validation |
|
|
399
|
+
| `query_parser()` | Parses query strings into `ctx.query` |
|
|
400
|
+
| `rate_limit()` | IP-based request rate limiting |
|
|
401
|
+
| `jwt_auth(secret)` | JWT authentication and user injection into `ctx.data["user"]` |
|
|
402
|
+
| `session(SessionOpts)` | Session management with HMAC signing |
|
|
403
|
+
| `serve_static(path)` | Serve static files safely from disk |
|
|
404
|
+
| `helmet()` | Adds standard security headers |
|
|
405
|
+
| `normalize_headers()` | Lowercase all headers for consistency |
|
|
406
|
+
|
|
407
|
+
<br />
|
|
408
|
+
|
|
409
|
+
Quick Example: Full Middleware Stack
|
|
410
|
+
|
|
411
|
+
```python
|
|
412
|
+
from railpy import Railpy, Controller, Get, Post, Param, Context, register_controller, Use
|
|
413
|
+
from railpy.middlewares import (
|
|
414
|
+
logger, error_handler, cors, body_parser,
|
|
415
|
+
rate_limit, jwt_auth, session, SessionOpts,
|
|
416
|
+
query_parser, serve_static
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
app = Railpy()
|
|
420
|
+
|
|
421
|
+
# =========================
|
|
422
|
+
# Middleware Stack
|
|
423
|
+
# =========================
|
|
424
|
+
SESSION_SECRET = "super-secret-session"
|
|
425
|
+
JWT_SECRET = "super-secret-jwt"
|
|
426
|
+
|
|
427
|
+
app.use(logger) # Logs requests/responses
|
|
428
|
+
app.use(error_handler) # Global error handling
|
|
429
|
+
app.use(rate_limit(limit=100)) # Rate limit per IP
|
|
430
|
+
app.use(cors()) # Allow all origins
|
|
431
|
+
app.use(body_parser) # Parse JSON/form bodies
|
|
432
|
+
app.use(query_parser()) # Parse query string into ctx.query
|
|
433
|
+
app.use(session(SessionOpts(secret=SESSION_SECRET))) # Session management
|
|
434
|
+
app.use(serve_static("./downloads")) # Serve static files from ./downloads
|
|
435
|
+
|
|
436
|
+
# =========================
|
|
437
|
+
# Public Route
|
|
438
|
+
# =========================
|
|
439
|
+
@app.get("/")
|
|
440
|
+
async def home(ctx: Context):
|
|
441
|
+
return {"message": "Welcome to Railpy 🚀"}
|
|
442
|
+
|
|
443
|
+
# =========================
|
|
444
|
+
# Protected Route
|
|
445
|
+
# =========================
|
|
446
|
+
@app.get("/secret", jwt_auth(JWT_SECRET))
|
|
447
|
+
async def secret_route(ctx: Context):
|
|
448
|
+
user = ctx.data.get("user")
|
|
449
|
+
return {"message": "Hello JWT", "user": user}
|
|
450
|
+
|
|
451
|
+
# =========================
|
|
452
|
+
# Controller Example
|
|
453
|
+
# =========================
|
|
454
|
+
@Controller("/api/v1")
|
|
455
|
+
@Use(jwt_auth(JWT_SECRET))
|
|
456
|
+
class UserExpress:
|
|
457
|
+
|
|
458
|
+
@Get("/users/:id")
|
|
459
|
+
@Param("id")
|
|
460
|
+
async def get_user(self, user_id: str, ctx: Context):
|
|
461
|
+
async def get_user(self, user_id: str, ctx: Context):
|
|
462
|
+
user = ctx.data.get("user")
|
|
463
|
+
|
|
464
|
+
# Add a background task inside controller
|
|
465
|
+
async def send_welcome_email(u_id: str):
|
|
466
|
+
await asyncio.sleep(1) # simulate async operation
|
|
467
|
+
print(f"Email sent to user {u_id}")
|
|
468
|
+
|
|
469
|
+
ctx.background_tasks.append((send_welcome_email, (user_id,), {}))
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
"user_id": user_id,
|
|
473
|
+
"logged_in_user": ctx.data.get("user")
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
@Post("/users")
|
|
477
|
+
async def create_user(self, ctx: Context):
|
|
478
|
+
body = ctx.data.get("body")
|
|
479
|
+
return {
|
|
480
|
+
"message": "User created",
|
|
481
|
+
"body_received": body,
|
|
482
|
+
"logged_in_user": ctx.data.get("user")
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
@Get("/profile")
|
|
486
|
+
async def profile(self, ctx: Context):
|
|
487
|
+
return {
|
|
488
|
+
"message": "Your profile",
|
|
489
|
+
"user": ctx.data.get("user"),
|
|
490
|
+
"session": ctx.state["session"]
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
register_controller(app, UserExpress)
|
|
494
|
+
|
|
495
|
+
# =========================
|
|
496
|
+
# Run Server
|
|
497
|
+
# =========================
|
|
498
|
+
if __name__ == "__main__":
|
|
499
|
+
app.start(port=3000)
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
## Features demoed:
|
|
505
|
+
|
|
506
|
+
- Logging (logger)
|
|
507
|
+
- Error handling (error_handler)
|
|
508
|
+
- Rate limiting (rate_limit)
|
|
509
|
+
- CORS (cors)
|
|
510
|
+
- Body parsing (body_parser)
|
|
511
|
+
- Query parsing (query_parser)
|
|
512
|
+
- JWT auth (jwt_auth)
|
|
513
|
+
- Session management (session)
|
|
514
|
+
- Static file serving (serve_static)
|
|
515
|
+
- Controller routes (@Controller)
|
|
516
|
+
- Static File Example
|
|
517
|
+
|
|
518
|
+
---
|
|
519
|
+
|
|
520
|
+
## Static File Example
|
|
521
|
+
|
|
522
|
+
Put two files in ./downloads/ folder:
|
|
523
|
+
|
|
524
|
+
```
|
|
525
|
+
./downloads/readme_download1.txt
|
|
526
|
+
./downloads/readme_download2.txt
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
Then access via browser or curl:
|
|
530
|
+
|
|
531
|
+
```
|
|
532
|
+
curl http://localhost:3000/readme_download1.txt
|
|
533
|
+
curl http://localhost:3000/readme_download2.txt
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
## Session Example
|
|
537
|
+
|
|
538
|
+
```python
|
|
539
|
+
@app.get("/set-session")
|
|
540
|
+
async def set_session(ctx: Context):
|
|
541
|
+
ctx.state["session"]["user"] = "admin"
|
|
542
|
+
return {"session_set": True}
|
|
543
|
+
|
|
544
|
+
@app.get("/get-session")
|
|
545
|
+
async def get_session(ctx: Context):
|
|
546
|
+
return {"session": ctx.state["session"]}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
## JWT Example
|
|
550
|
+
```python
|
|
551
|
+
from jwt import encode
|
|
552
|
+
|
|
553
|
+
token = encode({"sub": "1234"}, JWT_SECRET, algorithm="HS256")
|
|
554
|
+
|
|
555
|
+
# Send in Authorization header: "Bearer <token>"
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
## Query Parsing Example
|
|
559
|
+
```python
|
|
560
|
+
@app.get("/search")
|
|
561
|
+
async def search(ctx: Context):
|
|
562
|
+
# ?q=python&page=2
|
|
563
|
+
q = ctx.query.get("q")
|
|
564
|
+
page = ctx.query.get("page", 1)
|
|
565
|
+
return {"query": q, "page": page}
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
## Body Parsing Example
|
|
569
|
+
```python
|
|
570
|
+
@app.post("/echo")
|
|
571
|
+
async def echo(ctx: Context):
|
|
572
|
+
body = ctx.data.get("body")
|
|
573
|
+
return {"you_sent": body}
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
Downloadable README Example Files
|
|
577
|
+
|
|
578
|
+
```
|
|
579
|
+
./downloads/readme_download1.txt:
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
Railpy Download File 1
|
|
583
|
+
|
|
584
|
+
This is a sample file for testing Railpy static file serving.
|
|
585
|
+
|
|
586
|
+
```
|
|
587
|
+
./downloads/readme_download2.txt:
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
Railpy Download File 2
|
|
592
|
+
|
|
593
|
+
Another sample file for testing static file downloads.
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
---
|
|
597
|
+
|
|
598
|
+
# Comparison
|
|
599
|
+
|
|
600
|
+
| Criteria | Railpy | FastAPI | Flask | Django |
|
|
601
|
+
| ------------ | ---------------- | ------------- | --------------- | -------------- |
|
|
602
|
+
| Core concept | Execution engine | API framework | Micro framework | Full framework |
|
|
603
|
+
| Performance | ⚡ High | ⚡ High | ⚠️ Medium | ⚠️ Medium |
|
|
604
|
+
| Routing | Radix | Starlette | Werkzeug | Django |
|
|
605
|
+
| Middleware | Deterministic | Stack | Stack | Stack |
|
|
606
|
+
| Architecture | Flexible | Opinionated | Flexible | Structured |
|
|
607
|
+
| Serverless | ✅ Native | ⚠️ Adapter | ⚠️ Adapter | ❌ |
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
---
|
|
611
|
+
|
|
612
|
+
# Benchmark
|
|
613
|
+
|
|
614
|
+
Railpy is designed for minimal overhead and deterministic execution.
|
|
615
|
+
|
|
616
|
+
Basic benchmark comparison (simple JSON response):
|
|
617
|
+
|
|
618
|
+
| Framework | Req/sec | Notes |
|
|
619
|
+
| ---------- | --------- | ---------------------------------- |
|
|
620
|
+
| Flask | ~5k | WSGI |
|
|
621
|
+
| Django | ~7k | Full framework |
|
|
622
|
+
| FastAPI | ~18k | Starlette + Pydantic |
|
|
623
|
+
| Starlette | ~22k | Minimal ASGI |
|
|
624
|
+
| **Railpy** | **~25k+** | Radix router + compiled middleware |
|
|
625
|
+
|
|
626
|
+
<br />
|
|
627
|
+
|
|
628
|
+
Benchmark example:
|
|
629
|
+
|
|
630
|
+
```python
|
|
631
|
+
from railpy import Railpy
|
|
632
|
+
|
|
633
|
+
app = Railpy()
|
|
634
|
+
|
|
635
|
+
@app.get("/")
|
|
636
|
+
async def hello(ctx):
|
|
637
|
+
return {"hello": "world"}
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
Run benchmark:
|
|
641
|
+
```plaintext
|
|
642
|
+
uvicorn main:app --workers 1
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
Test using wrk:
|
|
646
|
+
```plaintext
|
|
647
|
+
wrk -t4 -c100 -d30s http://localhost:8000/
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
Example output:
|
|
651
|
+
```plaintext
|
|
652
|
+
Running 30s test @ http://localhost:8000
|
|
653
|
+
4 threads and 100 connections
|
|
654
|
+
|
|
655
|
+
Requests/sec: 25000+
|
|
656
|
+
Latency: ~3ms
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
> Performance varies depending on hardware and middleware stack.
|
|
660
|
+
|
|
661
|
+
---
|
|
662
|
+
|
|
663
|
+
# When to Use
|
|
664
|
+
|
|
665
|
+
✔ High-performance APIs.
|
|
666
|
+
✔ Microservices.
|
|
667
|
+
✔ Serverless backends.
|
|
668
|
+
✔ Custom frameworks.
|
|
669
|
+
|
|
670
|
+
---
|
|
671
|
+
|
|
672
|
+
# Philosophy
|
|
673
|
+
|
|
674
|
+
```plaintext
|
|
675
|
+
You control:
|
|
676
|
+
- architecture
|
|
677
|
+
- middleware
|
|
678
|
+
- data
|
|
679
|
+
|
|
680
|
+
Railpy controls:
|
|
681
|
+
- execution
|
|
682
|
+
- routing
|
|
683
|
+
- lifecycle
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
---
|
|
687
|
+
|
|
688
|
+
# License
|
|
689
|
+
|
|
690
|
+
MIT
|