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.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: Plinx
3
- Version: 1.0.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.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
1
+ Metadata-Version: 2.4
2
2
  Name: Plinx
3
- Version: 1.0.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.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,4 @@
1
+ webob
2
+ parse
3
+ requests
4
+ requests-wsgi-adapter
@@ -47,7 +47,7 @@ app = Plinx()
47
47
 
48
48
  @app.route("/")
49
49
  def index(request, response):
50
- response.text = "Hello, Plinx 1.0.0!"
50
+ response.text = "Hello, Plinx 1.0.1!"
51
51
 
52
52
  # Example using the ORM (requires database setup)
53
53
  # from plinx.orm import Database, Table, Column
@@ -1 +1,3 @@
1
1
  from .applications import Plinx
2
+
3
+ __version__ = "1.0.1"
@@ -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()