Plinx 1.0.0__tar.gz → 1.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.
- {Plinx-1.0.0 → plinx-1.0.1}/PKG-INFO +18 -3
- {Plinx-1.0.0 → plinx-1.0.1}/Plinx.egg-info/PKG-INFO +18 -3
- plinx-1.0.1/Plinx.egg-info/requires.txt +4 -0
- {Plinx-1.0.0 → plinx-1.0.1}/README.md +1 -1
- {Plinx-1.0.0 → plinx-1.0.1}/plinx/__init__.py +2 -0
- plinx-1.0.1/plinx/applications.py +376 -0
- plinx-1.0.1/plinx/methods.py +77 -0
- plinx-1.0.1/plinx/middleware.py +158 -0
- plinx-1.0.1/plinx/orm/orm.py +550 -0
- {Plinx-1.0.0 → plinx-1.0.1}/plinx/orm/utils.py +2 -2
- plinx-1.0.1/plinx/response.py +120 -0
- plinx-1.0.1/plinx/status_codes.py +57 -0
- plinx-1.0.1/plinx/utils.py +28 -0
- {Plinx-1.0.0 → plinx-1.0.1}/setup.py +1 -1
- {Plinx-1.0.0 → plinx-1.0.1}/tests/test_application.py +3 -3
- {Plinx-1.0.0 → plinx-1.0.1}/tests/test_orm.py +3 -7
- Plinx-1.0.0/Plinx.egg-info/requires.txt +0 -2
- Plinx-1.0.0/plinx/applications.py +0 -192
- Plinx-1.0.0/plinx/methods.py +0 -18
- Plinx-1.0.0/plinx/middleware.py +0 -81
- Plinx-1.0.0/plinx/orm/orm.py +0 -240
- Plinx-1.0.0/plinx/response.py +0 -48
- Plinx-1.0.0/plinx/status_codes.py +0 -36
- Plinx-1.0.0/plinx/utils.py +0 -13
- {Plinx-1.0.0 → plinx-1.0.1}/LICENSE +0 -0
- {Plinx-1.0.0 → plinx-1.0.1}/Plinx.egg-info/SOURCES.txt +0 -0
- {Plinx-1.0.0 → plinx-1.0.1}/Plinx.egg-info/dependency_links.txt +0 -0
- {Plinx-1.0.0 → plinx-1.0.1}/Plinx.egg-info/top_level.txt +0 -0
- {Plinx-1.0.0 → plinx-1.0.1}/plinx/orm/__init__.py +0 -0
- {Plinx-1.0.0 → plinx-1.0.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: Plinx
|
3
|
-
Version: 1.0.
|
3
|
+
Version: 1.0.1
|
4
4
|
Summary: Plinx is an experimental, minimalistic, and extensible web framework and ORM written in Python.
|
5
5
|
Home-page: https://github.com/dhavalsavalia/plinx
|
6
6
|
Author: Dhaval Savalia
|
@@ -15,6 +15,21 @@ Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
15
15
|
Requires-Python: >=3.11.0
|
16
16
|
Description-Content-Type: text/markdown
|
17
17
|
License-File: LICENSE
|
18
|
+
Requires-Dist: webob
|
19
|
+
Requires-Dist: parse
|
20
|
+
Requires-Dist: requests
|
21
|
+
Requires-Dist: requests-wsgi-adapter
|
22
|
+
Dynamic: author
|
23
|
+
Dynamic: author-email
|
24
|
+
Dynamic: classifier
|
25
|
+
Dynamic: description
|
26
|
+
Dynamic: description-content-type
|
27
|
+
Dynamic: home-page
|
28
|
+
Dynamic: license
|
29
|
+
Dynamic: license-file
|
30
|
+
Dynamic: requires-dist
|
31
|
+
Dynamic: requires-python
|
32
|
+
Dynamic: summary
|
18
33
|
|
19
34
|
|
20
35
|
# Plinx
|
@@ -66,7 +81,7 @@ app = Plinx()
|
|
66
81
|
|
67
82
|
@app.route("/")
|
68
83
|
def index(request, response):
|
69
|
-
response.text = "Hello, Plinx 1.0.
|
84
|
+
response.text = "Hello, Plinx 1.0.1!"
|
70
85
|
|
71
86
|
# Example using the ORM (requires database setup)
|
72
87
|
# from plinx.orm import Database, Table, Column
|
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: Plinx
|
3
|
-
Version: 1.0.
|
3
|
+
Version: 1.0.1
|
4
4
|
Summary: Plinx is an experimental, minimalistic, and extensible web framework and ORM written in Python.
|
5
5
|
Home-page: https://github.com/dhavalsavalia/plinx
|
6
6
|
Author: Dhaval Savalia
|
@@ -15,6 +15,21 @@ Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
15
15
|
Requires-Python: >=3.11.0
|
16
16
|
Description-Content-Type: text/markdown
|
17
17
|
License-File: LICENSE
|
18
|
+
Requires-Dist: webob
|
19
|
+
Requires-Dist: parse
|
20
|
+
Requires-Dist: requests
|
21
|
+
Requires-Dist: requests-wsgi-adapter
|
22
|
+
Dynamic: author
|
23
|
+
Dynamic: author-email
|
24
|
+
Dynamic: classifier
|
25
|
+
Dynamic: description
|
26
|
+
Dynamic: description-content-type
|
27
|
+
Dynamic: home-page
|
28
|
+
Dynamic: license
|
29
|
+
Dynamic: license-file
|
30
|
+
Dynamic: requires-dist
|
31
|
+
Dynamic: requires-python
|
32
|
+
Dynamic: summary
|
18
33
|
|
19
34
|
|
20
35
|
# Plinx
|
@@ -66,7 +81,7 @@ app = Plinx()
|
|
66
81
|
|
67
82
|
@app.route("/")
|
68
83
|
def index(request, response):
|
69
|
-
response.text = "Hello, Plinx 1.0.
|
84
|
+
response.text = "Hello, Plinx 1.0.1!"
|
70
85
|
|
71
86
|
# Example using the ORM (requires database setup)
|
72
87
|
# from plinx.orm import Database, Table, Column
|
@@ -0,0 +1,376 @@
|
|
1
|
+
import inspect
|
2
|
+
from typing import Callable, Dict, Iterable, Tuple
|
3
|
+
from wsgiref.types import StartResponse, WSGIEnvironment
|
4
|
+
|
5
|
+
from parse import parse
|
6
|
+
from requests import Session as RequestsSession
|
7
|
+
from webob import Request
|
8
|
+
from wsgiadapter import WSGIAdapter as RequestsWSGIAdapter
|
9
|
+
|
10
|
+
from .methods import HTTPMethods
|
11
|
+
from .middleware import Middleware
|
12
|
+
from .response import PlinxResponse as Response
|
13
|
+
from .status_codes import StatusCodes
|
14
|
+
from .utils import handle_404
|
15
|
+
|
16
|
+
|
17
|
+
class Plinx:
|
18
|
+
"""
|
19
|
+
The main application class for the Plinx web framework.
|
20
|
+
|
21
|
+
This class serves as the WSGI application entry point and manages routes,
|
22
|
+
middleware, and request handling. It provides a Flask-like decorator syntax
|
23
|
+
for adding routes and Django-like method for explicitly registering them.
|
24
|
+
|
25
|
+
Examples:
|
26
|
+
Creating a simple app with a route:
|
27
|
+
|
28
|
+
```python
|
29
|
+
from plinx import Plinx
|
30
|
+
|
31
|
+
app = Plinx()
|
32
|
+
|
33
|
+
@app.route("/")
|
34
|
+
def home(request, response):
|
35
|
+
response.text = "Hello, World!"
|
36
|
+
```
|
37
|
+
|
38
|
+
Using HTTP method-specific decorators:
|
39
|
+
|
40
|
+
```python
|
41
|
+
@app.get("/users")
|
42
|
+
def list_users(request, response):
|
43
|
+
response.json = {"users": ["user1", "user2"]}
|
44
|
+
|
45
|
+
@app.post("/users")
|
46
|
+
def create_user(request, response):
|
47
|
+
response.text = "User created"
|
48
|
+
```
|
49
|
+
|
50
|
+
Using class-based views:
|
51
|
+
|
52
|
+
```python
|
53
|
+
@app.route("/books")
|
54
|
+
class BooksResource:
|
55
|
+
def get(self, request, response):
|
56
|
+
response.text = "List of books"
|
57
|
+
|
58
|
+
def post(self, request, response):
|
59
|
+
response.text = "Book created"
|
60
|
+
```
|
61
|
+
"""
|
62
|
+
|
63
|
+
def __init__(self):
|
64
|
+
"""
|
65
|
+
Initialize a new Plinx application instance.
|
66
|
+
|
67
|
+
Sets up the routing table, middleware stack, and
|
68
|
+
dynamically generates HTTP method-specific decorators.
|
69
|
+
"""
|
70
|
+
self.routes: Dict[str, Tuple[HTTPMethods, Callable]] = {}
|
71
|
+
self.exception_handler = None
|
72
|
+
self.middleware = Middleware(self)
|
73
|
+
|
74
|
+
self._method_decorators = {}
|
75
|
+
for method in HTTPMethods:
|
76
|
+
self._method_decorators[method.name.lower()] = (
|
77
|
+
self._create_method_decorator(method)
|
78
|
+
)
|
79
|
+
|
80
|
+
def __call__(
|
81
|
+
self,
|
82
|
+
environ: WSGIEnvironment,
|
83
|
+
start_response: StartResponse,
|
84
|
+
) -> Iterable[bytes]:
|
85
|
+
"""
|
86
|
+
WSGI entry point for the application.
|
87
|
+
|
88
|
+
This method makes the Plinx instance callable as required by the WSGI spec,
|
89
|
+
allowing it to be used directly with WSGI servers like Gunicorn or uWSGI.
|
90
|
+
|
91
|
+
Args:
|
92
|
+
environ: The WSGI environment dictionary containing request information
|
93
|
+
start_response: The WSGI start_response callable
|
94
|
+
|
95
|
+
Returns:
|
96
|
+
An iterable of bytes representing the response body
|
97
|
+
"""
|
98
|
+
return self.middleware(environ, start_response)
|
99
|
+
|
100
|
+
def add_route(
|
101
|
+
self,
|
102
|
+
path: str,
|
103
|
+
handler: Callable,
|
104
|
+
method: HTTPMethods = HTTPMethods.GET,
|
105
|
+
):
|
106
|
+
"""
|
107
|
+
Explicitly register a route with the application.
|
108
|
+
|
109
|
+
This provides a Django-like syntax for registering routes,
|
110
|
+
as an alternative to the decorator approach.
|
111
|
+
|
112
|
+
Args:
|
113
|
+
path: URL pattern to match (may contain parameters)
|
114
|
+
handler: Function or class to handle matching requests
|
115
|
+
method: HTTP method to match (defaults to GET)
|
116
|
+
|
117
|
+
Raises:
|
118
|
+
RuntimeError: If the path is already registered
|
119
|
+
|
120
|
+
Example:
|
121
|
+
```python
|
122
|
+
def home(request, response):
|
123
|
+
response.text = "Hello, World!"
|
124
|
+
|
125
|
+
app.add_route("/home", home)
|
126
|
+
```
|
127
|
+
"""
|
128
|
+
if path in self.routes:
|
129
|
+
raise RuntimeError(f"Route '{path}' is already registered.")
|
130
|
+
|
131
|
+
self.routes[path] = (method, handler)
|
132
|
+
|
133
|
+
def route(
|
134
|
+
self,
|
135
|
+
path: str,
|
136
|
+
):
|
137
|
+
"""
|
138
|
+
Register a route via decorator syntax.
|
139
|
+
|
140
|
+
This implements Flask-like syntax for registering routes. It can be used
|
141
|
+
with both function-based handlers and class-based handlers.
|
142
|
+
|
143
|
+
Args:
|
144
|
+
path: URL pattern to match (may contain parameters)
|
145
|
+
|
146
|
+
Returns:
|
147
|
+
A decorator function that registers the handler
|
148
|
+
|
149
|
+
Example:
|
150
|
+
```python
|
151
|
+
@app.route("/home")
|
152
|
+
def home(request, response):
|
153
|
+
response.text = "Hello, World!"
|
154
|
+
```
|
155
|
+
|
156
|
+
For class-based views:
|
157
|
+
|
158
|
+
```python
|
159
|
+
@app.route("/books")
|
160
|
+
class BooksResource:
|
161
|
+
def get(self, request, response):
|
162
|
+
response.text = "List of books"
|
163
|
+
```
|
164
|
+
"""
|
165
|
+
|
166
|
+
def wrapper(handler):
|
167
|
+
self.add_route(path, handler)
|
168
|
+
return handler
|
169
|
+
|
170
|
+
return wrapper
|
171
|
+
|
172
|
+
def __getattr__(
|
173
|
+
self,
|
174
|
+
name: str,
|
175
|
+
):
|
176
|
+
"""
|
177
|
+
Enable HTTP method-specific decorators like app.get, app.post, etc.
|
178
|
+
|
179
|
+
This magic method is called when an attribute lookup fails, allowing
|
180
|
+
us to dynamically provide HTTP method decorators.
|
181
|
+
|
182
|
+
Args:
|
183
|
+
name: The attribute name being looked up
|
184
|
+
|
185
|
+
Returns:
|
186
|
+
A method-specific decorator function if name matches a HTTP method
|
187
|
+
|
188
|
+
Raises:
|
189
|
+
RuntimeError: If the attribute doesn't match a known HTTP method
|
190
|
+
"""
|
191
|
+
if name in self._method_decorators:
|
192
|
+
return self._method_decorators[name]
|
193
|
+
raise RuntimeError(
|
194
|
+
f"'{self.__class__.__name__}' object has no attribute '{name}'"
|
195
|
+
)
|
196
|
+
|
197
|
+
def _create_method_decorator(self, method: HTTPMethods):
|
198
|
+
"""
|
199
|
+
Create a decorator for a specific HTTP method.
|
200
|
+
|
201
|
+
This internal method generates the decorators used for HTTP method-specific
|
202
|
+
route registration like @app.get(), @app.post(), etc.
|
203
|
+
|
204
|
+
Args:
|
205
|
+
method: The HTTP method enum value
|
206
|
+
|
207
|
+
Returns:
|
208
|
+
A decorator function for the specified HTTP method
|
209
|
+
"""
|
210
|
+
|
211
|
+
def decorator(path: str):
|
212
|
+
def wrapper(handler):
|
213
|
+
self.add_route(path, handler, method)
|
214
|
+
return handler
|
215
|
+
|
216
|
+
return wrapper
|
217
|
+
|
218
|
+
return decorator
|
219
|
+
|
220
|
+
def handle_request(
|
221
|
+
self,
|
222
|
+
request: Request,
|
223
|
+
) -> Response:
|
224
|
+
"""
|
225
|
+
Process an incoming request and generate a response.
|
226
|
+
|
227
|
+
This is the core request handling logic that finds a matching route handler,
|
228
|
+
executes it, and handles any exceptions.
|
229
|
+
|
230
|
+
Args:
|
231
|
+
request: The incoming WebOb Request object
|
232
|
+
|
233
|
+
Returns:
|
234
|
+
A Response object containing the response data
|
235
|
+
"""
|
236
|
+
response: Response = Response()
|
237
|
+
|
238
|
+
handler_definition, kwargs = self.find_handler(request, response)
|
239
|
+
|
240
|
+
try:
|
241
|
+
if handler_definition is not None:
|
242
|
+
method, handler = handler_definition
|
243
|
+
|
244
|
+
# Handle CBVs
|
245
|
+
if inspect.isclass(handler):
|
246
|
+
handler = getattr(
|
247
|
+
handler(),
|
248
|
+
request.method.lower(),
|
249
|
+
None,
|
250
|
+
)
|
251
|
+
# only allow methods that are defined in the class
|
252
|
+
if handler is None:
|
253
|
+
response.status_code = StatusCodes.METHOD_NOT_ALLOWED.value
|
254
|
+
response.text = "Method Not Allowed"
|
255
|
+
return response
|
256
|
+
|
257
|
+
if inspect.isfunction(handler):
|
258
|
+
# Handle function-based views
|
259
|
+
if request.method != method.value:
|
260
|
+
response.status_code = StatusCodes.METHOD_NOT_ALLOWED.value
|
261
|
+
response.text = "Method Not Allowed"
|
262
|
+
return response
|
263
|
+
|
264
|
+
handler(request, response, **kwargs)
|
265
|
+
|
266
|
+
except Exception as e:
|
267
|
+
if self.exception_handler:
|
268
|
+
self.exception_handler(request, response, e)
|
269
|
+
else:
|
270
|
+
response.status_code = StatusCodes.INTERNAL_SERVER_ERROR.value
|
271
|
+
response.text = str(e)
|
272
|
+
|
273
|
+
return response
|
274
|
+
|
275
|
+
def find_handler(
|
276
|
+
self,
|
277
|
+
request: Request,
|
278
|
+
response: Response,
|
279
|
+
) -> Tuple[Tuple[HTTPMethods, Callable] | None, dict | None]:
|
280
|
+
"""
|
281
|
+
Find the appropriate handler for a request based on URL path matching.
|
282
|
+
|
283
|
+
This method iterates through registered routes and uses the parse library
|
284
|
+
to match URL patterns and extract parameters.
|
285
|
+
|
286
|
+
Args:
|
287
|
+
request: The incoming WebOb Request object
|
288
|
+
response: The Response object being built
|
289
|
+
|
290
|
+
Returns:
|
291
|
+
A tuple containing:
|
292
|
+
- The handler definition (method, handler) if found, or None
|
293
|
+
- A dictionary of extracted URL parameters, or None
|
294
|
+
"""
|
295
|
+
for path, handler in self.routes.items():
|
296
|
+
parse_result = parse(path, request.path)
|
297
|
+
if parse_result is not None:
|
298
|
+
return handler, parse_result.named
|
299
|
+
|
300
|
+
handle_404(response)
|
301
|
+
return None, None
|
302
|
+
|
303
|
+
def add_exception_handler(
|
304
|
+
self,
|
305
|
+
exception_handler,
|
306
|
+
):
|
307
|
+
"""
|
308
|
+
Register a global exception handler for the application.
|
309
|
+
|
310
|
+
The exception handler will be called whenever an uncaught exception
|
311
|
+
occurs during request handling.
|
312
|
+
|
313
|
+
Args:
|
314
|
+
exception_handler: Callable that takes (request, response, exception)
|
315
|
+
|
316
|
+
Example:
|
317
|
+
```python
|
318
|
+
def handle_exceptions(request, response, exception):
|
319
|
+
response.status_code = 500
|
320
|
+
response.text = f"Error: {str(exception)}"
|
321
|
+
|
322
|
+
app.add_exception_handler(handle_exceptions)
|
323
|
+
```
|
324
|
+
"""
|
325
|
+
self.exception_handler = exception_handler
|
326
|
+
|
327
|
+
def add_middleware(
|
328
|
+
self,
|
329
|
+
middleware_cls: type[Middleware],
|
330
|
+
):
|
331
|
+
"""
|
332
|
+
Add a middleware class to the application's middleware stack.
|
333
|
+
|
334
|
+
Middleware classes must inherit from the Middleware base class and can
|
335
|
+
process requests before they reach handlers and responses before they're returned.
|
336
|
+
|
337
|
+
Args:
|
338
|
+
middleware_cls: A class inheriting from Middleware
|
339
|
+
|
340
|
+
Example:
|
341
|
+
```python
|
342
|
+
class SimpleMiddleware(Middleware):
|
343
|
+
def process_request(self, request):
|
344
|
+
print("Processing request")
|
345
|
+
|
346
|
+
def process_response(self, request, response):
|
347
|
+
print("Processing response")
|
348
|
+
|
349
|
+
app.add_middleware(SimpleMiddleware)
|
350
|
+
```
|
351
|
+
"""
|
352
|
+
self.middleware.add(middleware_cls)
|
353
|
+
|
354
|
+
def test_session(self, base_url="http://testserver"):
|
355
|
+
"""
|
356
|
+
Create a test client session for this application.
|
357
|
+
|
358
|
+
This provides an interface similar to the requests library for testing
|
359
|
+
your application without making actual HTTP calls.
|
360
|
+
|
361
|
+
Args:
|
362
|
+
base_url: Base URL to use for requests (default: "http://testserver")
|
363
|
+
|
364
|
+
Returns:
|
365
|
+
A requests.Session object configured to call this application
|
366
|
+
|
367
|
+
Example:
|
368
|
+
```python
|
369
|
+
client = app.test_session()
|
370
|
+
response = client.get("/home")
|
371
|
+
assert response.status_code == 200
|
372
|
+
```
|
373
|
+
"""
|
374
|
+
session = RequestsSession()
|
375
|
+
session.mount(prefix=base_url, adapter=RequestsWSGIAdapter(self))
|
376
|
+
return session
|
@@ -0,0 +1,77 @@
|
|
1
|
+
from enum import Enum
|
2
|
+
|
3
|
+
|
4
|
+
class HTTPMethods(Enum):
|
5
|
+
"""
|
6
|
+
Enumeration of HTTP methods supported by the Plinx framework.
|
7
|
+
|
8
|
+
This enum defines the standard HTTP methods as defined in RFC 7231 and RFC 5789,
|
9
|
+
categorized by their safety and idempotency properties.
|
10
|
+
|
11
|
+
Safe methods (should not modify resources):
|
12
|
+
- GET: Retrieve a representation of a resource
|
13
|
+
- HEAD: Same as GET but returns only headers, no body
|
14
|
+
|
15
|
+
Idempotent methods (multiple identical requests have same effect as single request):
|
16
|
+
- PUT: Replace a resource with the request payload
|
17
|
+
- DELETE: Remove the specified resource
|
18
|
+
- OPTIONS: Describe the communication options for the target resource
|
19
|
+
|
20
|
+
Non-idempotent methods (multiple identical requests may have different effects):
|
21
|
+
- POST: Submit data to be processed, typically creating a new resource
|
22
|
+
- PATCH: Apply partial modifications to a resource
|
23
|
+
|
24
|
+
Usage:
|
25
|
+
```python
|
26
|
+
from plinx.methods import HTTPMethods
|
27
|
+
|
28
|
+
# Check if a method is GET
|
29
|
+
if method == HTTPMethods.GET:
|
30
|
+
# handle GET request
|
31
|
+
|
32
|
+
# Get the string value of a method
|
33
|
+
method_str = HTTPMethods.POST.value # "POST"
|
34
|
+
```
|
35
|
+
"""
|
36
|
+
|
37
|
+
# Safe methods
|
38
|
+
GET = "GET"
|
39
|
+
HEAD = "HEAD"
|
40
|
+
|
41
|
+
# Idempotent methods
|
42
|
+
PUT = "PUT"
|
43
|
+
DELETE = "DELETE"
|
44
|
+
OPTIONS = "OPTIONS"
|
45
|
+
|
46
|
+
# Non-idempotent methods
|
47
|
+
POST = "POST"
|
48
|
+
PATCH = "PATCH"
|
49
|
+
|
50
|
+
|
51
|
+
def is_valid_method(method: str) -> bool:
|
52
|
+
"""
|
53
|
+
Check if the given method is a valid HTTP method.
|
54
|
+
|
55
|
+
Args:
|
56
|
+
method: The HTTP method to check
|
57
|
+
|
58
|
+
Returns:
|
59
|
+
bool: True if the method is valid, False otherwise
|
60
|
+
"""
|
61
|
+
return method in HTTPMethods.__members__.values()
|
62
|
+
|
63
|
+
|
64
|
+
def get_handler_name_for_method(method: str) -> str:
|
65
|
+
"""
|
66
|
+
Get the handler name for a given HTTP method.
|
67
|
+
|
68
|
+
Args:
|
69
|
+
method: The HTTP method to get the handler name for
|
70
|
+
|
71
|
+
Returns:
|
72
|
+
str: The handler name corresponding to the HTTP method
|
73
|
+
"""
|
74
|
+
if not is_valid_method(method):
|
75
|
+
raise ValueError(f"Invalid HTTP method: {method}")
|
76
|
+
|
77
|
+
return method.lower()
|