py-flexo 0.1.0__py3-none-any.whl
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.
- py_flexo/__init__.py +0 -0
- py_flexo/app.py +111 -0
- py_flexo/middleware.py +26 -0
- py_flexo/response.py +35 -0
- py_flexo-0.1.0.dist-info/METADATA +391 -0
- py_flexo-0.1.0.dist-info/RECORD +8 -0
- py_flexo-0.1.0.dist-info/WHEEL +5 -0
- py_flexo-0.1.0.dist-info/top_level.txt +1 -0
py_flexo/__init__.py
ADDED
|
File without changes
|
py_flexo/app.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from webob import Request
|
|
2
|
+
from .response import Response
|
|
3
|
+
from parse import parse
|
|
4
|
+
import requests
|
|
5
|
+
import wsgiadapter
|
|
6
|
+
import inspect
|
|
7
|
+
from jinja2 import Environment, FileSystemLoader
|
|
8
|
+
import os
|
|
9
|
+
from whitenoise import WhiteNoise
|
|
10
|
+
from .middleware import Middleware
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PyFlexoApp:
|
|
14
|
+
def __init__(self, templates_dir="templates", static_dir = "static"):
|
|
15
|
+
self._routes = {}
|
|
16
|
+
self.template_env = Environment(
|
|
17
|
+
loader = FileSystemLoader(os.path.abspath(templates_dir))
|
|
18
|
+
)
|
|
19
|
+
self.exception_handler = None
|
|
20
|
+
|
|
21
|
+
self.whitenoise = WhiteNoise(self.wsgi_app, root=static_dir, prefix='/static')
|
|
22
|
+
self.middleware = Middleware(self)
|
|
23
|
+
|
|
24
|
+
def __call__(self, environ, start_response):
|
|
25
|
+
path_info = environ["PATH_INFO"]
|
|
26
|
+
if path_info.startswith('/static'):
|
|
27
|
+
return self.whitenoise(environ, start_response)
|
|
28
|
+
return self.middleware(environ, start_response)
|
|
29
|
+
|
|
30
|
+
def wsgi_app(self, environ, start_response):
|
|
31
|
+
request = Request(environ)
|
|
32
|
+
response = self.handle_request(request)
|
|
33
|
+
return response(environ, start_response)
|
|
34
|
+
|
|
35
|
+
def handle_request(self, request):
|
|
36
|
+
response = Response()
|
|
37
|
+
|
|
38
|
+
handler_data, kwargs = self.find_handler(request.path)
|
|
39
|
+
|
|
40
|
+
if handler_data:
|
|
41
|
+
handler = handler_data["handler"]
|
|
42
|
+
allow_methods = handler_data["allow_methods"]
|
|
43
|
+
if inspect.isclass(handler):
|
|
44
|
+
handler = getattr(handler(), request.method.lower(), None)
|
|
45
|
+
if handler:
|
|
46
|
+
handler(request, response, **kwargs)
|
|
47
|
+
else:
|
|
48
|
+
self.method_not_allowed_response(response)
|
|
49
|
+
else:
|
|
50
|
+
if request.method.lower() not in allow_methods:
|
|
51
|
+
self.method_not_allowed_response(response)
|
|
52
|
+
try:
|
|
53
|
+
handler(request, response, **kwargs)
|
|
54
|
+
except Exception as e:
|
|
55
|
+
if self.exception_handler is not None:
|
|
56
|
+
self.exception_handler(request, response, e)
|
|
57
|
+
else:
|
|
58
|
+
raise e
|
|
59
|
+
else:
|
|
60
|
+
self.default_response(response)
|
|
61
|
+
return response
|
|
62
|
+
|
|
63
|
+
def find_handler(self, path):
|
|
64
|
+
for route, handler_data in self._routes.items():
|
|
65
|
+
result = parse(route, path)
|
|
66
|
+
if result:
|
|
67
|
+
return handler_data, result.named
|
|
68
|
+
if route == path:
|
|
69
|
+
return handler_data, None
|
|
70
|
+
return None, None
|
|
71
|
+
|
|
72
|
+
def default_response(self, response):
|
|
73
|
+
response.status_code = 404
|
|
74
|
+
response.text = 'Not Found'
|
|
75
|
+
return response
|
|
76
|
+
|
|
77
|
+
def method_not_allowed_response(self, res):
|
|
78
|
+
res.status_code = 405
|
|
79
|
+
res.text = 'Method Not Allowed'
|
|
80
|
+
return res
|
|
81
|
+
|
|
82
|
+
def add_route(self, path, handler, allow_methods = None):
|
|
83
|
+
assert path not in self._routes, "Duplicate url. Please change the URL"
|
|
84
|
+
|
|
85
|
+
if allow_methods is None:
|
|
86
|
+
allow_methods = ["get", "post", "put", "path", "head", "options", "delete"]
|
|
87
|
+
|
|
88
|
+
self._routes[path] = {"handler":handler, "allow_methods": allow_methods}
|
|
89
|
+
|
|
90
|
+
def route(self, path, allow_methods = None):
|
|
91
|
+
def decorator(handler):
|
|
92
|
+
self.add_route(path, handler, allow_methods)
|
|
93
|
+
return handler
|
|
94
|
+
return decorator
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_session(self):
|
|
98
|
+
session = requests.Session()
|
|
99
|
+
session.mount('http://testserver', wsgiadapter.WSGIAdapter(self))
|
|
100
|
+
return session
|
|
101
|
+
|
|
102
|
+
def template(self, template_name, context = None):
|
|
103
|
+
if context is None:
|
|
104
|
+
context = {}
|
|
105
|
+
return self.template_env.get_template(template_name).render(**context).encode()
|
|
106
|
+
|
|
107
|
+
def add_exception_handler(self, handler):
|
|
108
|
+
self.exception_handler = handler
|
|
109
|
+
|
|
110
|
+
def add_middleware(self, middleware_cls):
|
|
111
|
+
self.middleware.add(middleware_cls)
|
py_flexo/middleware.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from webob import Request
|
|
2
|
+
|
|
3
|
+
class Middleware:
|
|
4
|
+
def __init__(self, app):
|
|
5
|
+
self.app = app
|
|
6
|
+
|
|
7
|
+
def add(self, middleware_cls):
|
|
8
|
+
self.app = middleware_cls(self.app)
|
|
9
|
+
|
|
10
|
+
def process_request(self, req):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
def process_response(self, resp):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
def handle_request(self, request):
|
|
17
|
+
self.process_request(request)
|
|
18
|
+
response = self.app.handle_request(request)
|
|
19
|
+
self.process_response(response)
|
|
20
|
+
|
|
21
|
+
return response
|
|
22
|
+
|
|
23
|
+
def __call__(self, environ, start_response):
|
|
24
|
+
request = Request(environ)
|
|
25
|
+
response = self.app.handle_request(request)
|
|
26
|
+
return response(environ, start_response)
|
py_flexo/response.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from webob import Response as WebResponse
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
class Response:
|
|
5
|
+
def __init__(self):
|
|
6
|
+
self.json = None
|
|
7
|
+
self.html = None
|
|
8
|
+
self.text = None
|
|
9
|
+
self.content_type = None
|
|
10
|
+
self.body = b''
|
|
11
|
+
self.status_code = 200
|
|
12
|
+
|
|
13
|
+
def set_body_and_content_type(self):
|
|
14
|
+
if self.json:
|
|
15
|
+
self.body = json.dumps(self.json).encode()
|
|
16
|
+
self.content_type = "application/json"
|
|
17
|
+
|
|
18
|
+
if self.html:
|
|
19
|
+
self.body = self.html.encode()
|
|
20
|
+
self.content_type = "text/html"
|
|
21
|
+
|
|
22
|
+
if self.text:
|
|
23
|
+
self.body = self.text
|
|
24
|
+
self.content_type = "text/plain"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def __call__(self, environ, start_response):
|
|
28
|
+
self.set_body_and_content_type()
|
|
29
|
+
response = WebResponse(
|
|
30
|
+
body = self.body,
|
|
31
|
+
content_type=self.content_type,
|
|
32
|
+
status = self.status_code
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
return response(environ, start_response)
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: py-flexo
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python Web Framework built for learning purposes
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: gunicorn>=26.0.0
|
|
8
|
+
Requires-Dist: jinja2>=3.1.6
|
|
9
|
+
Requires-Dist: parse>=1.22.1
|
|
10
|
+
Requires-Dist: requests-wsgi-adapter>=0.4.1
|
|
11
|
+
Requires-Dist: webob>=1.8.10
|
|
12
|
+
Requires-Dist: whitenoise>=6.12.0
|
|
13
|
+
|
|
14
|
+
# PyFlexo
|
|
15
|
+
|
|
16
|
+
<p align="center">
|
|
17
|
+
A modern, lightweight and extensible WSGI web framework for Python.
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
<p align="center">
|
|
21
|
+
|
|
22
|
+

|
|
23
|
+

|
|
24
|
+

|
|
25
|
+

|
|
26
|
+
|
|
27
|
+
</p>
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Why PyFlexo?
|
|
32
|
+
|
|
33
|
+
PyFlexo is a lightweight WSGI web framework focused on simplicity, readability, and extensibility.
|
|
34
|
+
|
|
35
|
+
Instead of hiding how web applications work, PyFlexo provides a clean API while remaining close to the WSGI specification. It is suitable for learning, small services, REST APIs, and custom web applications.
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
* Simple and intuitive routing
|
|
40
|
+
* Function-based views
|
|
41
|
+
* Class-based views
|
|
42
|
+
* Dynamic URL parameters
|
|
43
|
+
* Built-in template rendering (Jinja2)
|
|
44
|
+
* Static file serving
|
|
45
|
+
* JSON response helper
|
|
46
|
+
* Global exception handlers
|
|
47
|
+
* Middleware support
|
|
48
|
+
* Pure Python implementation
|
|
49
|
+
* WSGI compatible
|
|
50
|
+
* Easy to extend
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
# Installation
|
|
55
|
+
|
|
56
|
+
Using pip
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install py_flexo
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Using uv
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
uv add py_flexo
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
# Quick Start
|
|
71
|
+
main.py
|
|
72
|
+
```python
|
|
73
|
+
from py_flexo.app import PyFlexoApp
|
|
74
|
+
|
|
75
|
+
app = PyFlexoApp()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@app.route("/")
|
|
79
|
+
def home(request, response):
|
|
80
|
+
response.text = "Hello, PyFlexo!"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Run your application with any WSGI server.
|
|
84
|
+
|
|
85
|
+
Example:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
gunicorn main:app
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
# Routing
|
|
94
|
+
|
|
95
|
+
## Function Based View
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
@app.route("/")
|
|
99
|
+
def index(request, response):
|
|
100
|
+
response.text = "Hello World"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Route Parameters
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
@app.route("/hello/{name}")
|
|
109
|
+
def hello(request, response, name):
|
|
110
|
+
response.text = f"Hello {name}"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Example
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
GET /hello/Alice
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Response
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
Hello Alice
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Class Based Views
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
@app.route("/books")
|
|
131
|
+
class Book:
|
|
132
|
+
|
|
133
|
+
def get(self, request, response):
|
|
134
|
+
response.text = "Book List"
|
|
135
|
+
|
|
136
|
+
def post(self, request, response):
|
|
137
|
+
response.text = "Create Book"
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Supported methods
|
|
141
|
+
|
|
142
|
+
* GET
|
|
143
|
+
* POST
|
|
144
|
+
* PUT
|
|
145
|
+
* PATCH
|
|
146
|
+
* DELETE
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
# Templates
|
|
151
|
+
|
|
152
|
+
Directory
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
project/
|
|
156
|
+
|
|
157
|
+
templates/
|
|
158
|
+
home.html
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Route
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
@app.route("/home")
|
|
165
|
+
def home(request, response):
|
|
166
|
+
response.html = app.template(
|
|
167
|
+
"home.html",
|
|
168
|
+
context={
|
|
169
|
+
"title": "PyFlexo",
|
|
170
|
+
"header": "Welcome"
|
|
171
|
+
}
|
|
172
|
+
)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Template
|
|
176
|
+
|
|
177
|
+
```html
|
|
178
|
+
<!DOCTYPE html>
|
|
179
|
+
<html>
|
|
180
|
+
<head>
|
|
181
|
+
<title>{{ title }}</title>
|
|
182
|
+
</head>
|
|
183
|
+
|
|
184
|
+
<body>
|
|
185
|
+
|
|
186
|
+
<h1>{{ header }}</h1>
|
|
187
|
+
|
|
188
|
+
</body>
|
|
189
|
+
</html>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
# JSON Responses
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
@app.route("/api")
|
|
198
|
+
def api(request, response):
|
|
199
|
+
|
|
200
|
+
response.json = {
|
|
201
|
+
"framework": "PyFlexo",
|
|
202
|
+
"version": "0.1.0"
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Output
|
|
207
|
+
|
|
208
|
+
```json
|
|
209
|
+
{
|
|
210
|
+
"framework": "PyFlexo",
|
|
211
|
+
"version": "0.1.0"
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
# Middleware
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
from py_flexo.middleware import Middleware
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class LoggingMiddleware(Middleware):
|
|
224
|
+
|
|
225
|
+
def process_request(self, request):
|
|
226
|
+
print(request.method, request.path)
|
|
227
|
+
|
|
228
|
+
def process_response(self, response):
|
|
229
|
+
print(response.status_code)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
app.add_middleware(LoggingMiddleware)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
# Exception Handling
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
def exception_handler(request, response, exception):
|
|
241
|
+
|
|
242
|
+
response.status_code = 500
|
|
243
|
+
response.text = "Internal Server Error"
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
app.add_exception_handler(exception_handler)
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
# Static Files
|
|
252
|
+
|
|
253
|
+
Directory
|
|
254
|
+
|
|
255
|
+
```
|
|
256
|
+
project/
|
|
257
|
+
|
|
258
|
+
static/
|
|
259
|
+
|
|
260
|
+
style.css
|
|
261
|
+
app.js
|
|
262
|
+
logo.png
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Static files are automatically available under
|
|
266
|
+
|
|
267
|
+
```
|
|
268
|
+
/static/
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Example
|
|
272
|
+
|
|
273
|
+
```
|
|
274
|
+
/static/style.css
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
# Project Structure
|
|
280
|
+
|
|
281
|
+
```
|
|
282
|
+
project/
|
|
283
|
+
|
|
284
|
+
│
|
|
285
|
+
|
|
286
|
+
├── main.py
|
|
287
|
+
|
|
288
|
+
├── templates/
|
|
289
|
+
|
|
290
|
+
│ └── home.html
|
|
291
|
+
|
|
292
|
+
│
|
|
293
|
+
|
|
294
|
+
├── static/
|
|
295
|
+
|
|
296
|
+
│ ├── style.css
|
|
297
|
+
|
|
298
|
+
│ └── app.js
|
|
299
|
+
|
|
300
|
+
│
|
|
301
|
+
|
|
302
|
+
└── pyproject.toml
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
# Complete Example
|
|
308
|
+
|
|
309
|
+
```python
|
|
310
|
+
from py_flexo.app import PyFlexoApp
|
|
311
|
+
from py_flexo.middleware import Middleware
|
|
312
|
+
|
|
313
|
+
app = PyFlexoApp()
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
@app.route("/")
|
|
317
|
+
def index(request, response):
|
|
318
|
+
response.text = "Welcome to PyFlexo"
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@app.route("/hello/{name}")
|
|
322
|
+
def hello(request, response, name):
|
|
323
|
+
response.text = f"Hello {name}"
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@app.route("/books")
|
|
327
|
+
class Book:
|
|
328
|
+
|
|
329
|
+
def get(self, request, response):
|
|
330
|
+
response.text = "Book List"
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
@app.route("/home")
|
|
334
|
+
def home(request, response):
|
|
335
|
+
response.html = app.template(
|
|
336
|
+
"home.html",
|
|
337
|
+
context={
|
|
338
|
+
"title": "PyFlexo",
|
|
339
|
+
"header": "Welcome"
|
|
340
|
+
}
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
@app.route("/api")
|
|
345
|
+
def api(request, response):
|
|
346
|
+
response.json = {
|
|
347
|
+
"message": "Hello JSON"
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
class LoggingMiddleware(Middleware):
|
|
352
|
+
|
|
353
|
+
def process_request(self, request):
|
|
354
|
+
print(request.path)
|
|
355
|
+
|
|
356
|
+
def process_response(self, response):
|
|
357
|
+
print(response.status_code)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
app.add_middleware(LoggingMiddleware)
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
# Roadmap
|
|
366
|
+
|
|
367
|
+
* Cookies
|
|
368
|
+
* Sessions
|
|
369
|
+
* Authentication
|
|
370
|
+
* Blueprint support
|
|
371
|
+
* Request validation
|
|
372
|
+
* CLI
|
|
373
|
+
* Dependency Injection
|
|
374
|
+
* OpenAPI
|
|
375
|
+
* ASGI support
|
|
376
|
+
* WebSocket support
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
# Contributing
|
|
382
|
+
|
|
383
|
+
Contributions are welcome.
|
|
384
|
+
|
|
385
|
+
Please open an issue before submitting large changes.
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
# Author
|
|
390
|
+
|
|
391
|
+
PyFlexo is an open-source project created for developers who enjoy building fast and elegant Python web applications.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
py_flexo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
py_flexo/app.py,sha256=cXxYfCaolNvdcPRXGCc7ETX5zK-_M_OsVNoR047-aKs,3813
|
|
3
|
+
py_flexo/middleware.py,sha256=RXxZ-nEpbpT0gOSecdiU0MXZN-WjIvXst5DTmRgfOO8,660
|
|
4
|
+
py_flexo/response.py,sha256=3IyacG-Re60hlPUl_P5AETAOMUrxy4-_Kg5O8CVZRbk,936
|
|
5
|
+
py_flexo-0.1.0.dist-info/METADATA,sha256=W8j-4ux9n1DrKaKajdWfbeU3-psidSIwBSGoxrqzcAk,5444
|
|
6
|
+
py_flexo-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
7
|
+
py_flexo-0.1.0.dist-info/top_level.txt,sha256=3aF-mNjvgm11XVnHqNN1j-b_bqxWT-mG0017FKtKSZc,9
|
|
8
|
+
py_flexo-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
py_flexo
|