Plinx 0.0.1__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.
@@ -0,0 +1,8 @@
1
+ Copyright © 2025 Dhaval Savalia
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8
+
@@ -0,0 +1,110 @@
1
+ Metadata-Version: 2.1
2
+ Name: Plinx
3
+ Version: 0.0.1
4
+ Summary: Plinx is an experimental, minimalistic, and extensible web framework and ORM written in Python.
5
+ Home-page: https://github.com/dhavalsavalia/plinx
6
+ Author: Dhaval Savalia
7
+ Author-email: coder@dhavalsavalia.com
8
+ License: MIT
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: Implementation :: CPython
14
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
15
+ Requires-Python: >=3.11.0
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+
19
+
20
+ # Plinx
21
+
22
+ ![purpose](https://img.shields.io/badge/purpose-learning-green.svg)
23
+ ![PyPI](https://img.shields.io/pypi/v/bumbo.svg)
24
+
25
+ **Plinx** is an experimental, minimalistic, and extensible WSGI-based web framework and ORM written in Python.
26
+ It is designed to be simple, fast, and easy to extend, making it ideal for rapid prototyping and educational purposes.
27
+
28
+ ---
29
+
30
+ ## Features
31
+
32
+ - 🚀 Minimal and fast web framework
33
+ - 🛣️ Intuitive routing system
34
+ - 🧩 Extensible middleware support
35
+ - 🧪 Simple, readable codebase for learning and hacking
36
+ - 📝 Type hints and modern Python best practices
37
+
38
+ ---
39
+
40
+ ## Installation
41
+
42
+ Install directly from the git source:
43
+
44
+ ```bash
45
+ pip install git+https://github.com/dhavalsavalia/plinx.git
46
+ ```
47
+
48
+ ---
49
+
50
+ ## Quickstart
51
+
52
+ Create a simple web application in seconds:
53
+
54
+ ```python
55
+ from plinx import Plinx
56
+
57
+ app = Plinx()
58
+
59
+ @app.route("/")
60
+ def index(request, response):
61
+ response.text = "Hello, world!"
62
+ ```
63
+
64
+ Run your app (example, assuming you have an ASGI server like `uvicorn`):
65
+
66
+ ```bash
67
+ uvicorn myapp:app
68
+ ```
69
+
70
+ ## Testing
71
+
72
+ Use [pytest](https://docs.pytest.org/en/latest/) to unit test this framework.
73
+
74
+ ```bash
75
+ pytest --cov=.
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Roadmap
81
+
82
+ - [x] Web Framework
83
+ - [x] Routing
84
+ - [x] Explicit Routing Methods (GET, POST, etc.)
85
+ - [x] Parameterized Routes
86
+ - [x] Class Based Routes
87
+ - [x] Django-like Routes
88
+ - [x] Middleware Support
89
+ - [ ] ORM
90
+
91
+ ---
92
+
93
+ ## Contributing
94
+
95
+ Contributions are welcome! Please open issues or submit pull requests for improvements, bug fixes, or new features.
96
+
97
+ ---
98
+
99
+ ## License
100
+
101
+ This project is licensed under the MIT License. See [LICENSE](LICENSE) for details.
102
+
103
+ ---
104
+
105
+ ## Author & Contact
106
+
107
+ Created and maintained by [Dhaval Savalia](https://github.com/dhavalsavalia).
108
+ For questions or opportunities, feel free to reach out via [LinkedIn](https://www.linkedin.com/in/dhavalsavalia/) or open an issue.
109
+
110
+ ---
@@ -0,0 +1,12 @@
1
+ plinx/__init__.py,sha256=oMho3a3JJZllxtj4lLwS96pwWRBvctDJRFww5lfqlBs,32
2
+ plinx/applications.py,sha256=Oy2pO1zyfKWyYXeJ8AuzdoAab6XJThNHDbOdpKWA5-8,5957
3
+ plinx/methods.py,sha256=CEaousqNXbgdyaEL0BdOtAWh1p03jAaAFzXJkY9xII0,294
4
+ plinx/middleware.py,sha256=OiNDhKWpmpKkAIbSWxRzP_tQXSyfDToos-MQMDPSUWM,2161
5
+ plinx/response.py,sha256=oDZPF9V76HhqdUZLzSmoLw_q-GG9hc5krfCBgNm3OfU,1460
6
+ plinx/status_codes.py,sha256=V7vX7_ujYIaVjO3I3eVUnuNKiTXMSov56wJt8tvMbAM,700
7
+ plinx/utils.py,sha256=WaU_j7YXsdlZh3M33B2vXjRPAU26lBeQ5sV2ZasXVwE,352
8
+ Plinx-0.0.1.dist-info/LICENSE,sha256=MljMjTJD6oOY41eZWLDZ4x8FrTUcl7ElKxWIXJXUwLM,1066
9
+ Plinx-0.0.1.dist-info/METADATA,sha256=N-vDBHtG4pV2LKumS4yh40_IYzKKh8PgRf1G3Umw_Q0,2568
10
+ Plinx-0.0.1.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
11
+ Plinx-0.0.1.dist-info/top_level.txt,sha256=U_4P3aFsTEhhvfiE3sVbJKAG9Fp4IPC5d6zpttayAZw,6
12
+ Plinx-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.45.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ plinx
plinx/__init__.py ADDED
@@ -0,0 +1 @@
1
+ from .applications import Plinx
plinx/applications.py ADDED
@@ -0,0 +1,192 @@
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
+ def __init__(self):
19
+ self.routes: Dict[str, Tuple[HTTPMethods, Callable]] = {}
20
+ self.exception_handler = None
21
+ self.middleware = Middleware(self)
22
+
23
+ self._method_decorators = {}
24
+ for method in HTTPMethods:
25
+ self._method_decorators[method.name.lower()] = (
26
+ self._create_method_decorator(method)
27
+ )
28
+
29
+ def __call__(
30
+ self,
31
+ environ: WSGIEnvironment,
32
+ start_response: StartResponse,
33
+ ) -> Iterable[bytes]:
34
+ """
35
+ Entrypoint for the WSGI application.
36
+ :param environ: The WSGI environment.
37
+ :param start_response: The WSGI callable.
38
+ :return: The response body produced by the middleware.
39
+ """
40
+ return self.middleware(environ, start_response)
41
+
42
+ def add_route(
43
+ self,
44
+ path: str,
45
+ handler: Callable,
46
+ method: HTTPMethods = HTTPMethods.GET,
47
+ ):
48
+ """
49
+ Add a route to the application. Django-like syntax.
50
+ :param path: The path to register.
51
+ :param handler: The handler to register.
52
+ :return:
53
+ """
54
+ if path in self.routes:
55
+ raise RuntimeError(f"Route '{path}' is already registered.")
56
+
57
+ self.routes[path] = (method, handler)
58
+
59
+ def route(
60
+ self,
61
+ path: str,
62
+ ):
63
+ """
64
+ Register a route with the given path.
65
+ :param path: The path to register.
66
+ :return:
67
+ """
68
+
69
+ def wrapper(handler):
70
+ self.add_route(path, handler)
71
+ return handler
72
+
73
+ return wrapper
74
+
75
+ def __getattr__(
76
+ self,
77
+ name: str,
78
+ ):
79
+ """Allow access to HTTP method decorators like app.get, app.post etc."""
80
+ if name in self._method_decorators:
81
+ return self._method_decorators[name]
82
+ raise RuntimeError(
83
+ f"'{self.__class__.__name__}' object has no attribute '{name}'"
84
+ )
85
+
86
+ def _create_method_decorator(self, method: HTTPMethods):
87
+ """
88
+ Creates a decorator for registering routes with a specific HTTP method.
89
+ :param method: The HTTP method enum value
90
+ :return: Decorator function
91
+ """
92
+
93
+ def decorator(path: str):
94
+ def wrapper(handler):
95
+ self.add_route(path, handler, method)
96
+ return handler
97
+
98
+ return wrapper
99
+
100
+ return decorator
101
+
102
+ def handle_request(
103
+ self,
104
+ request: Request,
105
+ ) -> Response:
106
+ """
107
+ Handle the given request and return the response.
108
+ :param request: The request object.
109
+ :return: The response object.
110
+ """
111
+ response: Response = Response()
112
+
113
+ handler_definition, kwargs = self.find_handler(request, response)
114
+
115
+ try:
116
+ if handler_definition is not None:
117
+ method, handler = handler_definition
118
+
119
+ # Handle CBVs
120
+ if inspect.isclass(handler):
121
+ handler = getattr(
122
+ handler(),
123
+ request.method.lower(),
124
+ None,
125
+ )
126
+ # only allow methods that are defined in the class
127
+ if handler is None:
128
+ response.status_code = StatusCodes.METHOD_NOT_ALLOWED.value
129
+ response.text = "Method Not Allowed"
130
+ return response
131
+
132
+ if inspect.isfunction(handler):
133
+ # Handle function-based views
134
+ if request.method != method.value:
135
+ response.status_code = StatusCodes.METHOD_NOT_ALLOWED.value
136
+ response.text = "Method Not Allowed"
137
+ return response
138
+
139
+ handler(request, response, **kwargs)
140
+
141
+ except Exception as e:
142
+ if self.exception_handler:
143
+ self.exception_handler(request, response, e)
144
+ else:
145
+ response.status_code = StatusCodes.INTERNAL_SERVER_ERROR.value
146
+ response.text = str(e)
147
+
148
+ return response
149
+
150
+ def find_handler(
151
+ self,
152
+ request: Request,
153
+ response: Response,
154
+ ) -> Tuple[Callable, dict | None] | Tuple[None, None]:
155
+ """
156
+ Find the handler for the given request.
157
+ If no handler is found, set the response status code to 404
158
+ and return None.
159
+
160
+ :param request: The request object.
161
+ :param response: The response object.
162
+ :return: A tuple containing the handler and the named parameters.
163
+ If no handler is found, return tuple[None, None].
164
+ """
165
+ for path, handler in self.routes.items():
166
+ parse_result = parse(path, request.path)
167
+ if parse_result is not None:
168
+ return handler, parse_result.named
169
+
170
+ handle_404(response)
171
+ return None, None
172
+
173
+ def add_exception_handler(
174
+ self,
175
+ exception_handler,
176
+ ):
177
+ self.exception_handler = exception_handler
178
+
179
+ def add_middleware(
180
+ self,
181
+ middleware_cls: type[Middleware],
182
+ ):
183
+ """
184
+ Add a middleware class to the application.
185
+ :param middleware_cls: The middleware class to add.
186
+ """
187
+ self.middleware.add(middleware_cls)
188
+
189
+ def test_session(self, base_url="http://testserver"):
190
+ session = RequestsSession()
191
+ session.mount(prefix=base_url, adapter=RequestsWSGIAdapter(self))
192
+ return session
plinx/methods.py ADDED
@@ -0,0 +1,18 @@
1
+ from enum import Enum
2
+
3
+
4
+ class HTTPMethods(Enum):
5
+ """HTTP methods for the API."""
6
+
7
+ # Safe methods
8
+ GET = "GET"
9
+ HEAD = "HEAD"
10
+
11
+ # Idempotent methods
12
+ PUT = "PUT"
13
+ DELETE = "DELETE"
14
+ OPTIONS = "OPTIONS"
15
+
16
+ # Non-idempotent methods
17
+ POST = "POST"
18
+ PATCH = "PATCH"
plinx/middleware.py ADDED
@@ -0,0 +1,81 @@
1
+ from webob import Request, Response
2
+
3
+
4
+ class Middleware:
5
+ """
6
+ Middleware base class for all middleware classes.
7
+ This class is used to create middleware for the Plinx application.
8
+ Middleware classes should inherit from this class and implement the
9
+ `process_request` and `process_response` methods.
10
+ """
11
+
12
+ def __init__(
13
+ self,
14
+ app,
15
+ ):
16
+ """
17
+ Middleware base class for all middleware classes.
18
+ :param app: The WSGI application.
19
+ """
20
+ self.app = app
21
+
22
+ def __call__(
23
+ self,
24
+ environ: dict,
25
+ start_response: callable,
26
+ ):
27
+ """
28
+ Entrypoint for the WSGI middleware since it is now entrypoint for the WSGI application.
29
+ :param environ: The WSGI environment.
30
+ :param start_response: The WSGI callable.
31
+ :return: The response body.
32
+ """
33
+ request = Request(environ)
34
+
35
+ response = self.app.handle_request(request)
36
+
37
+ return response(environ, start_response)
38
+
39
+ def add(
40
+ self,
41
+ middleware_cls,
42
+ ):
43
+ """
44
+ Add a middleware class to the application.
45
+ :param middleware_cls: The middleware class to add.
46
+ """
47
+ self.app = middleware_cls(self.app)
48
+
49
+ def process_request(
50
+ self,
51
+ request: Request,
52
+ ):
53
+ """
54
+ Process the request before it is passed to the application.
55
+ :param request: The request object.
56
+ """
57
+ pass # pragma: no cover
58
+
59
+ def process_response(
60
+ self,
61
+ request: Request,
62
+ response: Response,
63
+ ):
64
+ """
65
+ Process the response after it is passed to the application.
66
+ :param request: The request object.
67
+ :param response: The response object.
68
+ """
69
+ pass # pragma: no cover
70
+
71
+ def handle_request(self, request: Request):
72
+ """
73
+ Handle the incoming request.
74
+ :param request: The request object.
75
+ :return: The response object.
76
+ """
77
+ self.process_request(request)
78
+ response = self.app.handle_request(request)
79
+ self.process_response(request, response)
80
+
81
+ return response
plinx/response.py ADDED
@@ -0,0 +1,48 @@
1
+ import json
2
+ from typing import Iterable
3
+ from wsgiref.types import StartResponse, WSGIEnvironment
4
+
5
+ from webob import Response as WebObResponse
6
+
7
+
8
+ class PlinxResponse:
9
+ def __init__(self):
10
+ self.json = None
11
+ self.text = None
12
+ self.content_type = None
13
+ self.body = b""
14
+ self.status_code = 200
15
+ self.headers = {}
16
+
17
+ def __call__(
18
+ self,
19
+ environ: WSGIEnvironment,
20
+ start_response: StartResponse,
21
+ ) -> Iterable[bytes]:
22
+ """
23
+ Entrypoint for the WSGI application.
24
+ :param environ: The WSGI environment.
25
+ :param start_response: The WSGI callable.
26
+ :return: The response body produced by the middleware.
27
+ """
28
+
29
+ self.set_body_and_content_type()
30
+
31
+ response = WebObResponse(
32
+ body=self.body,
33
+ content_type=self.content_type,
34
+ status=self.status_code,
35
+ headers=self.headers,
36
+ )
37
+ return response(environ, start_response)
38
+
39
+ def set_body_and_content_type(self):
40
+ if self.json is not None:
41
+ self.body = json.dumps(self.json).encode("UTF-8")
42
+ self.content_type = "application/json"
43
+ elif self.text is not None:
44
+ self.body = self.text.encode("utf-8") if isinstance(self.text, str) else self.text
45
+ self.content_type = "text/plain"
46
+
47
+ if self.content_type is not None:
48
+ self.headers["Content-Type"] = self.content_type
plinx/status_codes.py ADDED
@@ -0,0 +1,36 @@
1
+ from enum import Enum
2
+
3
+
4
+ class StatusCodes(Enum):
5
+ """Status codes for the API."""
6
+
7
+ # 2XX
8
+ OK = 200
9
+ CREATED = 201
10
+ ACCEPTED = 202
11
+ NO_CONTENT = 204
12
+
13
+ # 3XX
14
+ NOT_MODIFIED = 304
15
+
16
+ # 4XX
17
+ BAD_REQUEST = 400
18
+ UNAUTHORIZED = 401
19
+ FORBIDDEN = 403
20
+ NOT_FOUND = 404
21
+ METHOD_NOT_ALLOWED = 405
22
+ NOT_ACCEPTABLE = 406
23
+ CONFLICT = 409
24
+ GONE = 410
25
+ PRECONDITION_FAILED = 412
26
+ UNSUPPORTED_MEDIA_TYPE = 415
27
+ TOO_MANY_REQUESTS = 429
28
+ UNAVAILABLE_FOR_LEGAL_REASONS = 451
29
+
30
+ # 5XX
31
+ INTERNAL_SERVER_ERROR = 500
32
+ NOT_IMPLEMENTED = 501
33
+ BAD_GATEWAY = 502
34
+ SERVICE_UNAVAILABLE = 503
35
+ GATEWAY_TIMEOUT = 504
36
+ HTTP_VERSION_NOT_SUPPORTED = 505
plinx/utils.py ADDED
@@ -0,0 +1,13 @@
1
+ from webob import Response
2
+
3
+ from plinx.status_codes import StatusCodes
4
+
5
+
6
+ def handle_404(response: Response) -> None:
7
+ """
8
+ Set the response status code to 404 and return None.
9
+ :param response:
10
+ :return:
11
+ """
12
+ response.status_code = StatusCodes.NOT_FOUND.value
13
+ response.text = StatusCodes.NOT_FOUND.name.replace("_", " ").title()