python-in-underwear 0.5.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.
Files changed (33) hide show
  1. python_in_underwear-0.5.0/LICENSE +21 -0
  2. python_in_underwear-0.5.0/PKG-INFO +461 -0
  3. python_in_underwear-0.5.0/README.md +436 -0
  4. python_in_underwear-0.5.0/piu/__init__.py +36 -0
  5. python_in_underwear-0.5.0/piu/__main__.py +3 -0
  6. python_in_underwear-0.5.0/piu/app.py +226 -0
  7. python_in_underwear-0.5.0/piu/auth.py +114 -0
  8. python_in_underwear-0.5.0/piu/cli.py +161 -0
  9. python_in_underwear-0.5.0/piu/config.py +85 -0
  10. python_in_underwear-0.5.0/piu/csrf.py +65 -0
  11. python_in_underwear-0.5.0/piu/helpers.py +26 -0
  12. python_in_underwear-0.5.0/piu/middleware.py +40 -0
  13. python_in_underwear-0.5.0/piu/openapi.py +129 -0
  14. python_in_underwear-0.5.0/piu/plugins.py +14 -0
  15. python_in_underwear-0.5.0/piu/ratelimit.py +119 -0
  16. python_in_underwear-0.5.0/piu/routing.py +70 -0
  17. python_in_underwear-0.5.0/piu/serving.py +119 -0
  18. python_in_underwear-0.5.0/piu/sessions.py +86 -0
  19. python_in_underwear-0.5.0/piu/static.py +39 -0
  20. python_in_underwear-0.5.0/piu/tasks.py +46 -0
  21. python_in_underwear-0.5.0/piu/templating.py +30 -0
  22. python_in_underwear-0.5.0/piu/testing.py +111 -0
  23. python_in_underwear-0.5.0/piu/websocket.py +67 -0
  24. python_in_underwear-0.5.0/piu/wrappers.py +86 -0
  25. python_in_underwear-0.5.0/pyproject.toml +26 -0
  26. python_in_underwear-0.5.0/python_in_underwear.egg-info/PKG-INFO +461 -0
  27. python_in_underwear-0.5.0/python_in_underwear.egg-info/SOURCES.txt +31 -0
  28. python_in_underwear-0.5.0/python_in_underwear.egg-info/dependency_links.txt +1 -0
  29. python_in_underwear-0.5.0/python_in_underwear.egg-info/entry_points.txt +2 -0
  30. python_in_underwear-0.5.0/python_in_underwear.egg-info/requires.txt +20 -0
  31. python_in_underwear-0.5.0/python_in_underwear.egg-info/top_level.txt +1 -0
  32. python_in_underwear-0.5.0/setup.cfg +4 -0
  33. python_in_underwear-0.5.0/tests/test_piu.py +432 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vuk Todorovic
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,461 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-in-underwear
3
+ Version: 0.5.0
4
+ Summary: A lightweight, Flask-inspired web framework for Python.
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Provides-Extra: templates
10
+ Requires-Dist: jinja2>=3.0; extra == "templates"
11
+ Provides-Extra: sessions
12
+ Requires-Dist: cryptography>=41.0; extra == "sessions"
13
+ Provides-Extra: dev
14
+ Requires-Dist: watchdog>=3.0; extra == "dev"
15
+ Requires-Dist: jinja2>=3.0; extra == "dev"
16
+ Requires-Dist: pytest>=7.0; extra == "dev"
17
+ Provides-Extra: yaml
18
+ Requires-Dist: pyyaml>=6.0; extra == "yaml"
19
+ Provides-Extra: full
20
+ Requires-Dist: jinja2>=3.0; extra == "full"
21
+ Requires-Dist: watchdog>=3.0; extra == "full"
22
+ Requires-Dist: pyyaml>=6.0; extra == "full"
23
+ Requires-Dist: pytest>=7.0; extra == "full"
24
+ Dynamic: license-file
25
+
26
+ # 🩲 Python In Underwear (PIU)
27
+
28
+ > A lightweight, Flask-inspired web framework for Python. Sync & async, no fluff.
29
+
30
+ ![Version](https://img.shields.io/badge/version-0.5.0-blue)
31
+ ![Python](https://img.shields.io/badge/python-3.10+-brightgreen)
32
+ ![License](https://img.shields.io/badge/license-MIT-orange)
33
+
34
+ ---
35
+
36
+ ## What is PIU?
37
+
38
+ **Python In Underwear** is a minimal web framework built for developers who want Flask-like simplicity with modern Python support — native `async/await`, WSGI and ASGI interfaces, sessions, auth, rate limiting, WebSockets, OpenAPI docs, and more.
39
+
40
+ No magic. No bloat. Zero required dependencies.
41
+
42
+ ---
43
+
44
+ ## Features
45
+
46
+ - **Routing** — Decorator-based URL rules with dynamic path parameters (`/user/<id>`)
47
+ - **Blueprints** — Group routes into reusable modules with URL prefixes
48
+ - **Request & Response** — Clean wrappers with JSON, form, cookie, and redirect support
49
+ - **Middleware** — Chainable `next`-style stack, sync and async compatible
50
+ - **Sessions** — HMAC-signed cookie-based sessions, no server-side storage needed
51
+ - **CSRF Protection** — Token-based CSRF middleware with form and header support
52
+ - **Rate Limiting** — Global and per-route sliding window rate limiting
53
+ - **Auth** — `@require_auth` decorator with role-based access control
54
+ - **Templates** — Jinja2 integration with autoescaping out of the box
55
+ - **Static Files** — Automatic static file serving from a configurable directory
56
+ - **Config** — Load config from dict, `.env`, YAML, or environment variables
57
+ - **Hot Reload** — File watcher restarts the server on code changes
58
+ - **Test Client** — In-process HTTP client with cookie jar, no server needed
59
+ - **Plugins** — `app.register_plugin()` API for modular extensions
60
+ - **Background Tasks** — Fire-and-forget async/sync tasks from within handlers
61
+ - **WebSockets** — `@app.ws()` decorator over the ASGI interface
62
+ - **OpenAPI / Swagger** — Auto-generated docs served at `/docs`
63
+ - **WSGI & ASGI** — Deploy with Gunicorn, uWSGI, Uvicorn, or Hypercorn
64
+ - **CLI** — `piu new` and `piu run` commands
65
+
66
+ ---
67
+
68
+ ## Installation
69
+
70
+ ```bash
71
+ git clone https://github.com/TodorW/PythonInUnderwear.git
72
+ cd PythonInUnderwear
73
+ pip install -e ".[full]"
74
+ ```
75
+
76
+ Or install extras individually:
77
+
78
+ ```bash
79
+ pip install -e ".[templates]" # jinja2
80
+ pip install -e ".[dev]" # jinja2 + watchdog + pytest
81
+ pip install -e ".[full]" # everything
82
+ ```
83
+
84
+ ---
85
+
86
+ ## Quickstart
87
+
88
+ ```python
89
+ from piu import PIU, Request, Response
90
+
91
+ app = PIU()
92
+
93
+ @app.get("/")
94
+ def index(request: Request):
95
+ return Response(body="<h1>Hello from PIU 🩲</h1>")
96
+
97
+ @app.get("/hello/<name>")
98
+ async def hello(request: Request, name: str):
99
+ return Response.json({"message": f"Hello, {name}!"})
100
+
101
+ @app.post("/echo")
102
+ def echo(request: Request):
103
+ return Response.json({"you_sent": request.json()})
104
+
105
+ if __name__ == "__main__":
106
+ app.run()
107
+ ```
108
+
109
+ ---
110
+
111
+ ## CLI
112
+
113
+ ```bash
114
+ piu new myapp # scaffold a new project
115
+ piu run # run the dev server
116
+ piu run --reload # run with hot reload
117
+ ```
118
+
119
+ If `piu` isn't on PATH, use:
120
+
121
+ ```bash
122
+ python -m piu new myapp
123
+ python -m piu run --reload
124
+ ```
125
+
126
+ ---
127
+
128
+ ## Routing
129
+
130
+ ```python
131
+ @app.get("/users")
132
+ @app.post("/users")
133
+ @app.put("/users/<id>")
134
+ @app.patch("/users/<id>")
135
+ @app.delete("/users/<id>")
136
+
137
+ # Or explicitly:
138
+ @app.route("/users", methods=["GET", "POST"])
139
+ def users(request):
140
+ ...
141
+ ```
142
+
143
+ ---
144
+
145
+ ## Blueprints
146
+
147
+ ```python
148
+ from piu import Blueprint
149
+
150
+ api = Blueprint("api", prefix="/api")
151
+
152
+ @api.get("/users")
153
+ def users(request):
154
+ return Response.json([{"id": 1}])
155
+
156
+ app.register(api)
157
+ # or override prefix:
158
+ app.register(api, prefix="/v2")
159
+ ```
160
+
161
+ ---
162
+
163
+ ## Request
164
+
165
+ ```python
166
+ request.method # "GET", "POST", etc.
167
+ request.path # "/hello/world"
168
+ request.headers # dict of headers
169
+ request.query_params # parsed query string dict
170
+ request.body # raw bytes
171
+ request.cookies # dict of incoming cookies
172
+ request.session # session dict (requires SessionMiddleware)
173
+ request.csrf_token # current CSRF token (requires CSRFMiddleware)
174
+ request.background_tasks # BackgroundTasks instance
175
+ request.json() # parsed JSON body
176
+ request.form() # parsed form body
177
+ ```
178
+
179
+ ---
180
+
181
+ ## Response
182
+
183
+ ```python
184
+ return Response(body="<h1>Hello</h1>")
185
+ return Response.json({"key": "value"})
186
+ return Response(body="Created", status=201)
187
+ return Response.redirect("/new-location")
188
+ return Response(body="OK", headers={"X-Custom": "value"})
189
+
190
+ # Cookies
191
+ resp = Response(body="ok")
192
+ resp.set_cookie("token", "abc123", max_age=3600, httponly=True)
193
+ resp.delete_cookie("token")
194
+ ```
195
+
196
+ ---
197
+
198
+ ## Middleware
199
+
200
+ ```python
201
+ def logger(request, next):
202
+ print(f"{request.method} {request.path}")
203
+ return next(request)
204
+
205
+ async def auth_check(request, next):
206
+ if "Authorization" not in request.headers:
207
+ return Response(body="Unauthorized", status=401)
208
+ return await next(request)
209
+
210
+ app.middleware.use(logger)
211
+ app.middleware.use(auth_check)
212
+ ```
213
+
214
+ ---
215
+
216
+ ## Sessions
217
+
218
+ ```python
219
+ from piu import SessionMiddleware
220
+
221
+ app.middleware.use(SessionMiddleware(secret_key="your-secret", max_age=3600))
222
+
223
+ @app.get("/set")
224
+ def set_session(request):
225
+ request.session["user"] = "alice"
226
+ return Response(body="ok")
227
+
228
+ @app.get("/get")
229
+ def get_session(request):
230
+ return Response.json({"user": request.session.get("user")})
231
+ ```
232
+
233
+ ---
234
+
235
+ ## CSRF Protection
236
+
237
+ ```python
238
+ from piu import CSRFMiddleware
239
+
240
+ app.middleware.use(CSRFMiddleware(exempt_paths=["/api/"]))
241
+
242
+ # In your form template:
243
+ # <input type="hidden" name="_csrf_token" value="{{ request.csrf_token }}">
244
+
245
+ # Or in JS via header:
246
+ # X-CSRF-Token: <token>
247
+ ```
248
+
249
+ ---
250
+
251
+ ## Rate Limiting
252
+
253
+ ```python
254
+ from piu import RateLimitMiddleware, rate_limit
255
+
256
+ # Global: 100 req/min per IP
257
+ app.middleware.use(RateLimitMiddleware(limit=100, window=60))
258
+
259
+ # Per-route
260
+ @app.post("/login")
261
+ @rate_limit(limit=5, window=60)
262
+ def login(request):
263
+ ...
264
+ ```
265
+
266
+ ---
267
+
268
+ ## Auth
269
+
270
+ ```python
271
+ from piu import require_auth, login_user, logout_user, current_user
272
+
273
+ @app.post("/login")
274
+ def login(request):
275
+ login_user(request, {"id": 1, "role": "admin"})
276
+ return Response.redirect("/dashboard")
277
+
278
+ @app.get("/dashboard")
279
+ @require_auth(redirect_to="/login")
280
+ def dashboard(request):
281
+ user = current_user(request)
282
+ return Response(body=f"Hello {user['id']}")
283
+
284
+ @app.get("/admin")
285
+ @require_auth(role="admin", redirect_to="/login")
286
+ def admin(request):
287
+ ...
288
+
289
+ @app.get("/logout")
290
+ def logout(request):
291
+ logout_user(request)
292
+ return Response.redirect("/login")
293
+ ```
294
+
295
+ ---
296
+
297
+ ## Templates
298
+
299
+ ```python
300
+ app = PIU(template_dir="templates")
301
+
302
+ @app.get("/page")
303
+ def page(request):
304
+ return app.render("index.html", title="Home", user="Alice")
305
+ ```
306
+
307
+ ```html
308
+ <!-- templates/index.html -->
309
+ <h1>{{ title }}</h1>
310
+ <p>Welcome, {{ user }}!</p>
311
+ ```
312
+
313
+ ---
314
+
315
+ ## Config
316
+
317
+ ```python
318
+ app.config.from_env_file(".env") # load from .env
319
+ app.config.from_yaml("config.yaml") # load from YAML
320
+ app.config.from_dict({"DEBUG": True}) # load from dict
321
+ app.config.load_env(prefix="PIU_") # load PIU_* env vars
322
+
323
+ app.config["SECRET_KEY"] = "abc"
324
+ val = app.config.get("PORT", 5000)
325
+ ```
326
+
327
+ ---
328
+
329
+ ## Background Tasks
330
+
331
+ ```python
332
+ async def send_email(to: str):
333
+ ...
334
+
335
+ @app.post("/register")
336
+ def register(request):
337
+ request.background_tasks.add(send_email, "user@example.com")
338
+ return Response(body="registered", status=201)
339
+ ```
340
+
341
+ ---
342
+
343
+ ## Plugins
344
+
345
+ ```python
346
+ from piu import Plugin
347
+
348
+ class HealthPlugin(Plugin):
349
+ name = "health"
350
+
351
+ def setup(self, app):
352
+ @app.get("/health")
353
+ def health(req):
354
+ return Response.json({"status": "ok"})
355
+
356
+ app.register_plugin(HealthPlugin())
357
+ ```
358
+
359
+ ---
360
+
361
+ ## WebSockets
362
+
363
+ Requires Uvicorn (`pip install uvicorn`):
364
+
365
+ ```python
366
+ from piu import WebSocket
367
+
368
+ @app.ws("/ws/echo")
369
+ async def echo(ws: WebSocket):
370
+ while True:
371
+ msg = await ws.receive_text()
372
+ if msg is None:
373
+ break
374
+ await ws.send_text(f"echo: {msg}")
375
+ ```
376
+
377
+ ```bash
378
+ uvicorn app:app
379
+ ```
380
+
381
+ ---
382
+
383
+ ## OpenAPI / Swagger
384
+
385
+ ```python
386
+ app.enable_docs(title="My API")
387
+ # Swagger UI → http://127.0.0.1:5000/docs
388
+ # Raw schema → http://127.0.0.1:5000/openapi.json
389
+ ```
390
+
391
+ ---
392
+
393
+ ## Testing
394
+
395
+ ```python
396
+ from piu.testing import TestClient
397
+ from app import app
398
+
399
+ client = TestClient(app)
400
+
401
+ def test_index():
402
+ resp = client.get("/")
403
+ assert resp.status == 200
404
+
405
+ def test_json():
406
+ resp = client.post("/echo", json={"hello": "world"})
407
+ assert resp.json() == {"hello": "world"}
408
+ ```
409
+
410
+ ```bash
411
+ pytest tests/ -v
412
+ ```
413
+
414
+ ---
415
+
416
+ ## Deployment
417
+
418
+ **WSGI (Gunicorn)**
419
+ ```bash
420
+ gunicorn "app:app.wsgi"
421
+ ```
422
+
423
+ **ASGI (Uvicorn)**
424
+ ```bash
425
+ uvicorn app:app
426
+ ```
427
+
428
+ ---
429
+
430
+ ## Project Structure
431
+
432
+ ```
433
+ piu/
434
+ ├── __init__.py # Public API & version
435
+ ├── __main__.py # python -m piu entry point
436
+ ├── app.py # Core application class
437
+ ├── auth.py # @require_auth, login/logout helpers
438
+ ├── cli.py # CLI commands
439
+ ├── config.py # Config management
440
+ ├── csrf.py # CSRF middleware
441
+ ├── helpers.py # HTTP status utilities
442
+ ├── middleware.py # MiddlewareStack
443
+ ├── openapi.py # OpenAPI schema + Swagger UI
444
+ ├── plugins.py # Plugin base class
445
+ ├── ratelimit.py # Rate limiting middleware & decorator
446
+ ├── routing.py # Route, Router & Blueprint
447
+ ├── serving.py # Dev server & hot reload
448
+ ├── sessions.py # Session middleware
449
+ ├── static.py # Static file serving
450
+ ├── tasks.py # Background tasks
451
+ ├── templating.py # Jinja2 TemplateEngine
452
+ ├── testing.py # TestClient
453
+ ├── websocket.py # WebSocket support
454
+ └── wrappers.py # Request & Response
455
+ ```
456
+
457
+ ---
458
+
459
+ ## License
460
+
461
+ MIT — do whatever you want with it.