Plinx 0.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-0.0.1/LICENSE +8 -0
- Plinx-0.0.1/PKG-INFO +110 -0
- Plinx-0.0.1/Plinx.egg-info/PKG-INFO +110 -0
- Plinx-0.0.1/Plinx.egg-info/SOURCES.txt +19 -0
- Plinx-0.0.1/Plinx.egg-info/dependency_links.txt +1 -0
- Plinx-0.0.1/Plinx.egg-info/top_level.txt +1 -0
- Plinx-0.0.1/README.md +91 -0
- Plinx-0.0.1/plinx/__init__.py +1 -0
- Plinx-0.0.1/plinx/applications.py +192 -0
- Plinx-0.0.1/plinx/methods.py +18 -0
- Plinx-0.0.1/plinx/middleware.py +81 -0
- Plinx-0.0.1/plinx/response.py +48 -0
- Plinx-0.0.1/plinx/status_codes.py +36 -0
- Plinx-0.0.1/plinx/utils.py +13 -0
- Plinx-0.0.1/setup.cfg +4 -0
- Plinx-0.0.1/setup.py +65 -0
- Plinx-0.0.1/tests/test_application.py +293 -0
Plinx-0.0.1/LICENSE
ADDED
@@ -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
|
+
|
Plinx-0.0.1/PKG-INFO
ADDED
@@ -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
|
+

|
23
|
+

|
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,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
|
+

|
23
|
+

|
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,19 @@
|
|
1
|
+
LICENSE
|
2
|
+
README.md
|
3
|
+
setup.py
|
4
|
+
Plinx.egg-info/PKG-INFO
|
5
|
+
Plinx.egg-info/SOURCES.txt
|
6
|
+
Plinx.egg-info/dependency_links.txt
|
7
|
+
Plinx.egg-info/top_level.txt
|
8
|
+
plinx/__init__.py
|
9
|
+
plinx/applications.py
|
10
|
+
plinx/methods.py
|
11
|
+
plinx/middleware.py
|
12
|
+
plinx/response.py
|
13
|
+
plinx/status_codes.py
|
14
|
+
plinx/utils.py
|
15
|
+
plinx.egg-info/PKG-INFO
|
16
|
+
plinx.egg-info/SOURCES.txt
|
17
|
+
plinx.egg-info/dependency_links.txt
|
18
|
+
plinx.egg-info/top_level.txt
|
19
|
+
tests/test_application.py
|
@@ -0,0 +1 @@
|
|
1
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
plinx
|
Plinx-0.0.1/README.md
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
# Plinx
|
2
|
+
|
3
|
+

|
4
|
+

|
5
|
+
|
6
|
+
**Plinx** is an experimental, minimalistic, and extensible WSGI-based web framework and ORM written in Python.
|
7
|
+
It is designed to be simple, fast, and easy to extend, making it ideal for rapid prototyping and educational purposes.
|
8
|
+
|
9
|
+
---
|
10
|
+
|
11
|
+
## Features
|
12
|
+
|
13
|
+
- 🚀 Minimal and fast web framework
|
14
|
+
- 🛣️ Intuitive routing system
|
15
|
+
- 🧩 Extensible middleware support
|
16
|
+
- 🧪 Simple, readable codebase for learning and hacking
|
17
|
+
- 📝 Type hints and modern Python best practices
|
18
|
+
|
19
|
+
---
|
20
|
+
|
21
|
+
## Installation
|
22
|
+
|
23
|
+
Install directly from the git source:
|
24
|
+
|
25
|
+
```bash
|
26
|
+
pip install git+https://github.com/dhavalsavalia/plinx.git
|
27
|
+
```
|
28
|
+
|
29
|
+
---
|
30
|
+
|
31
|
+
## Quickstart
|
32
|
+
|
33
|
+
Create a simple web application in seconds:
|
34
|
+
|
35
|
+
```python
|
36
|
+
from plinx import Plinx
|
37
|
+
|
38
|
+
app = Plinx()
|
39
|
+
|
40
|
+
@app.route("/")
|
41
|
+
def index(request, response):
|
42
|
+
response.text = "Hello, world!"
|
43
|
+
```
|
44
|
+
|
45
|
+
Run your app (example, assuming you have an ASGI server like `uvicorn`):
|
46
|
+
|
47
|
+
```bash
|
48
|
+
uvicorn myapp:app
|
49
|
+
```
|
50
|
+
|
51
|
+
## Testing
|
52
|
+
|
53
|
+
Use [pytest](https://docs.pytest.org/en/latest/) to unit test this framework.
|
54
|
+
|
55
|
+
```bash
|
56
|
+
pytest --cov=.
|
57
|
+
```
|
58
|
+
|
59
|
+
---
|
60
|
+
|
61
|
+
## Roadmap
|
62
|
+
|
63
|
+
- [x] Web Framework
|
64
|
+
- [x] Routing
|
65
|
+
- [x] Explicit Routing Methods (GET, POST, etc.)
|
66
|
+
- [x] Parameterized Routes
|
67
|
+
- [x] Class Based Routes
|
68
|
+
- [x] Django-like Routes
|
69
|
+
- [x] Middleware Support
|
70
|
+
- [ ] ORM
|
71
|
+
|
72
|
+
---
|
73
|
+
|
74
|
+
## Contributing
|
75
|
+
|
76
|
+
Contributions are welcome! Please open issues or submit pull requests for improvements, bug fixes, or new features.
|
77
|
+
|
78
|
+
---
|
79
|
+
|
80
|
+
## License
|
81
|
+
|
82
|
+
This project is licensed under the MIT License. See [LICENSE](LICENSE) for details.
|
83
|
+
|
84
|
+
---
|
85
|
+
|
86
|
+
## Author & Contact
|
87
|
+
|
88
|
+
Created and maintained by [Dhaval Savalia](https://github.com/dhavalsavalia).
|
89
|
+
For questions or opportunities, feel free to reach out via [LinkedIn](https://www.linkedin.com/in/dhavalsavalia/) or open an issue.
|
90
|
+
|
91
|
+
---
|
@@ -0,0 +1 @@
|
|
1
|
+
from .applications import Plinx
|
@@ -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
|
@@ -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"
|
@@ -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
|
@@ -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
|
@@ -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
|
@@ -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()
|
Plinx-0.0.1/setup.cfg
ADDED
Plinx-0.0.1/setup.py
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
|
4
|
+
import io
|
5
|
+
import os
|
6
|
+
|
7
|
+
from setuptools import find_packages, setup
|
8
|
+
|
9
|
+
# Package meta-data.
|
10
|
+
NAME = "Plinx"
|
11
|
+
DESCRIPTION = "Plinx is an experimental, minimalistic, and extensible web framework and ORM written in Python."
|
12
|
+
URL = "https://github.com/dhavalsavalia/plinx"
|
13
|
+
EMAIL = "coder@dhavalsavalia.com"
|
14
|
+
AUTHOR = "Dhaval Savalia"
|
15
|
+
REQUIRES_PYTHON = ">=3.11.0"
|
16
|
+
VERSION = None
|
17
|
+
|
18
|
+
REQUIRED = []
|
19
|
+
EXTRAS = {}
|
20
|
+
|
21
|
+
with open("VERSION", "r") as f:
|
22
|
+
VERSION = f.read().strip()
|
23
|
+
|
24
|
+
# Below is from setup.py for humans (https://github.com/navdeep-G/setup.py)
|
25
|
+
here = os.path.abspath(os.path.dirname(__file__))
|
26
|
+
|
27
|
+
# Import the README and use it as the long-description.
|
28
|
+
try:
|
29
|
+
with io.open(os.path.join(here, "README.md"), encoding="utf-8") as f:
|
30
|
+
long_description = "\n" + f.read()
|
31
|
+
except FileNotFoundError:
|
32
|
+
long_description = DESCRIPTION
|
33
|
+
|
34
|
+
# Load the package's __version__.py module as a dictionary.
|
35
|
+
about = {}
|
36
|
+
about["__version__"] = VERSION
|
37
|
+
|
38
|
+
|
39
|
+
|
40
|
+
# Where the magic happens:
|
41
|
+
setup(
|
42
|
+
name=NAME,
|
43
|
+
version=VERSION,
|
44
|
+
description=DESCRIPTION,
|
45
|
+
long_description=long_description,
|
46
|
+
long_description_content_type="text/markdown",
|
47
|
+
author=AUTHOR,
|
48
|
+
author_email=EMAIL,
|
49
|
+
python_requires=REQUIRES_PYTHON,
|
50
|
+
url=URL,
|
51
|
+
packages=find_packages(exclude=["tests", "*.tests", "*.tests.*", "tests.*"]),
|
52
|
+
install_requires=REQUIRED,
|
53
|
+
extras_require=EXTRAS,
|
54
|
+
include_package_data=True,
|
55
|
+
license="MIT",
|
56
|
+
classifiers=[
|
57
|
+
"License :: OSI Approved :: MIT License",
|
58
|
+
"Programming Language :: Python",
|
59
|
+
"Programming Language :: Python :: 3",
|
60
|
+
"Programming Language :: Python :: 3.11",
|
61
|
+
"Programming Language :: Python :: Implementation :: CPython",
|
62
|
+
"Programming Language :: Python :: Implementation :: PyPy",
|
63
|
+
],
|
64
|
+
setup_requires=["wheel"],
|
65
|
+
)
|
@@ -0,0 +1,293 @@
|
|
1
|
+
import pytest
|
2
|
+
|
3
|
+
from plinx import Plinx
|
4
|
+
from plinx.methods import HTTPMethods
|
5
|
+
from plinx.middleware import Middleware
|
6
|
+
|
7
|
+
|
8
|
+
@pytest.fixture
|
9
|
+
def app():
|
10
|
+
return Plinx()
|
11
|
+
|
12
|
+
|
13
|
+
@pytest.fixture
|
14
|
+
def client(app):
|
15
|
+
return app.test_session()
|
16
|
+
|
17
|
+
|
18
|
+
class TestFlaskLikeApplication:
|
19
|
+
def test_app_object(self, app):
|
20
|
+
assert app.routes == {}
|
21
|
+
assert app is not None
|
22
|
+
|
23
|
+
def test_route_decorator(self, app):
|
24
|
+
@app.route("/home")
|
25
|
+
def home(request, response):
|
26
|
+
response.text = "Hello, World!"
|
27
|
+
|
28
|
+
method, handler = app.routes["/home"]
|
29
|
+
assert handler == home
|
30
|
+
assert method.value == "GET"
|
31
|
+
|
32
|
+
def test_404_response(self, client):
|
33
|
+
response = client.get("http://testserver/not-found")
|
34
|
+
assert response.status_code == 404
|
35
|
+
assert response.text == "Not Found"
|
36
|
+
|
37
|
+
def test_parameterized_route(self, app, client):
|
38
|
+
@app.route("/hello/{name}")
|
39
|
+
def hello(request, response, name):
|
40
|
+
response.text = f"Hello, {name}!"
|
41
|
+
|
42
|
+
response = client.get("http://testserver/hello/Dhaval")
|
43
|
+
assert response.status_code == 200
|
44
|
+
assert response.text == "Hello, Dhaval!"
|
45
|
+
|
46
|
+
def test_duplicate_route(self, app):
|
47
|
+
@app.route("/duplicate")
|
48
|
+
def handler1(request, response):
|
49
|
+
response.text = "First"
|
50
|
+
|
51
|
+
with pytest.raises(RuntimeError) as excinfo:
|
52
|
+
|
53
|
+
@app.route("/duplicate")
|
54
|
+
def handler2(request, response):
|
55
|
+
response.text = "Second"
|
56
|
+
|
57
|
+
assert isinstance(excinfo.value, RuntimeError)
|
58
|
+
assert "Route '/duplicate' is already registered." == str(excinfo.value)
|
59
|
+
|
60
|
+
def test_method_not_allowed(self, app, client):
|
61
|
+
@app.post("/method_not_allowed")
|
62
|
+
def method_not_allowed(request, response):
|
63
|
+
response.text = "Best POST method"
|
64
|
+
|
65
|
+
response = client.get("http://testserver/method_not_allowed")
|
66
|
+
assert response.status_code == 405
|
67
|
+
assert response.text == "Method Not Allowed"
|
68
|
+
|
69
|
+
def test_post_request(self, app, client):
|
70
|
+
@app.post("/post")
|
71
|
+
def post_handler(request, response):
|
72
|
+
response.text = "POST request received"
|
73
|
+
|
74
|
+
response = client.post("http://testserver/post")
|
75
|
+
assert response.status_code == 200
|
76
|
+
assert response.text == "POST request received"
|
77
|
+
|
78
|
+
def test_get_request(self, app, client):
|
79
|
+
@app.get("/get")
|
80
|
+
def get_handler(request, response):
|
81
|
+
response.text = "GET request received"
|
82
|
+
|
83
|
+
response = client.get("http://testserver/get")
|
84
|
+
assert response.status_code == 200
|
85
|
+
assert response.text == "GET request received"
|
86
|
+
|
87
|
+
def test_put_request(self, app, client):
|
88
|
+
@app.put("/put")
|
89
|
+
def put_handler(request, response):
|
90
|
+
response.text = "PUT request received"
|
91
|
+
|
92
|
+
response = client.put("http://testserver/put")
|
93
|
+
assert response.status_code == 200
|
94
|
+
assert response.text == "PUT request received"
|
95
|
+
|
96
|
+
def test_delete_request(self, app, client):
|
97
|
+
@app.delete("/delete")
|
98
|
+
def delete_handler(request, response):
|
99
|
+
response.text = "DELETE request received"
|
100
|
+
|
101
|
+
response = client.delete("http://testserver/delete")
|
102
|
+
assert response.status_code == 200
|
103
|
+
assert response.text == "DELETE request received"
|
104
|
+
|
105
|
+
def test_options_request(self, app, client):
|
106
|
+
@app.options("/options")
|
107
|
+
def options_handler(request, response):
|
108
|
+
response.text = "OPTIONS request received"
|
109
|
+
|
110
|
+
response = client.options("http://testserver/options")
|
111
|
+
assert response.status_code == 200
|
112
|
+
assert response.text == "OPTIONS request received"
|
113
|
+
|
114
|
+
def test_patch_request(self, app, client):
|
115
|
+
@app.patch("/patch")
|
116
|
+
def patch_handler(request, response):
|
117
|
+
response.text = "PATCH request received"
|
118
|
+
|
119
|
+
response = client.patch("http://testserver/patch")
|
120
|
+
assert response.status_code == 200
|
121
|
+
assert response.text == "PATCH request received"
|
122
|
+
|
123
|
+
def test_head_request(self, app, client):
|
124
|
+
@app.head("/head")
|
125
|
+
def head_handler(request, response):
|
126
|
+
response.headers["X-TEST-HEADER"] = "Test Header"
|
127
|
+
|
128
|
+
response = client.head("http://testserver/head")
|
129
|
+
assert response.status_code == 200
|
130
|
+
assert response.headers["X-TEST-HEADER"] == "Test Header"
|
131
|
+
|
132
|
+
def test_invalid_method(self, app, client):
|
133
|
+
with pytest.raises(RuntimeError) as excinfo:
|
134
|
+
|
135
|
+
@app.trace("/invalid")
|
136
|
+
def invalid_handler(request, response):
|
137
|
+
response.text = "Invalid method"
|
138
|
+
|
139
|
+
assert isinstance(excinfo.value, RuntimeError)
|
140
|
+
assert "'Plinx' object has no attribute 'trace'" == str(excinfo.value)
|
141
|
+
|
142
|
+
|
143
|
+
class TestClassBasedView:
|
144
|
+
def test_class_based_route(self, app, client):
|
145
|
+
@app.route("/book")
|
146
|
+
class BooksResource:
|
147
|
+
def get(self, req, resp):
|
148
|
+
resp.text = "Books Page"
|
149
|
+
|
150
|
+
def post(self, req, resp):
|
151
|
+
resp.text = "Endpoint to create a book"
|
152
|
+
|
153
|
+
response = client.get("http://testserver/book")
|
154
|
+
assert response.status_code == 200
|
155
|
+
assert response.text == "Books Page"
|
156
|
+
response = client.post("http://testserver/book")
|
157
|
+
assert response.status_code == 200
|
158
|
+
assert response.text == "Endpoint to create a book"
|
159
|
+
|
160
|
+
def test_method_not_allowed(self, app, client):
|
161
|
+
@app.route("/cbv")
|
162
|
+
class DummyCBV:
|
163
|
+
def get(self, req, resp):
|
164
|
+
resp.text = "GET OK"
|
165
|
+
|
166
|
+
response = client.post("http://testserver/cbv")
|
167
|
+
assert response.status_code == 405
|
168
|
+
assert response.text == "Method Not Allowed"
|
169
|
+
|
170
|
+
|
171
|
+
class TestExceptionHandling:
|
172
|
+
def test_custom_exception_handler(self, app, client):
|
173
|
+
def on_exception(request, response, exception):
|
174
|
+
response.text = "AttributeErrorHappened"
|
175
|
+
|
176
|
+
app.add_exception_handler(on_exception)
|
177
|
+
|
178
|
+
@app.route("/exception")
|
179
|
+
def exception_handler(request, response):
|
180
|
+
raise AttributeError("This is a test exception")
|
181
|
+
|
182
|
+
response = client.get("http://testserver/exception")
|
183
|
+
assert response.text == "AttributeErrorHappened"
|
184
|
+
|
185
|
+
def test_no_exception_handler(self, app, client):
|
186
|
+
@app.route("/exception")
|
187
|
+
def exception_handler(request, response):
|
188
|
+
raise AttributeError("This is a test exception")
|
189
|
+
|
190
|
+
response = client.get("http://testserver/exception")
|
191
|
+
|
192
|
+
assert response.status_code == 500
|
193
|
+
assert response.text == "This is a test exception"
|
194
|
+
|
195
|
+
|
196
|
+
class TestDjangoLikeApplication:
|
197
|
+
def test_add_route(self, app, client):
|
198
|
+
def home(request, response):
|
199
|
+
response.text = "Hello, World!"
|
200
|
+
|
201
|
+
app.add_route("/home", home)
|
202
|
+
|
203
|
+
response = client.get("http://testserver/home")
|
204
|
+
assert response.status_code == 200
|
205
|
+
assert response.text == "Hello, World!"
|
206
|
+
|
207
|
+
def test_method_not_allowed(self, app, client):
|
208
|
+
def home(request, response):
|
209
|
+
response.text = "Hello, World!"
|
210
|
+
|
211
|
+
app.add_route("/home", home, HTTPMethods.POST)
|
212
|
+
|
213
|
+
response = client.get("http://testserver/home")
|
214
|
+
assert response.status_code == 405
|
215
|
+
assert response.text == "Method Not Allowed"
|
216
|
+
|
217
|
+
|
218
|
+
class TestMiddleware:
|
219
|
+
def test_middleware_functionality(self, app, client):
|
220
|
+
process_request_called = False
|
221
|
+
process_response_called = False
|
222
|
+
|
223
|
+
class CallMiddlewareMethods(Middleware):
|
224
|
+
def __init__(self, app):
|
225
|
+
super().__init__(app)
|
226
|
+
|
227
|
+
def process_request(self, request):
|
228
|
+
nonlocal process_request_called
|
229
|
+
process_request_called = True
|
230
|
+
|
231
|
+
def process_response(self, request, response):
|
232
|
+
nonlocal process_response_called
|
233
|
+
process_response_called = True
|
234
|
+
|
235
|
+
app.add_middleware(CallMiddlewareMethods)
|
236
|
+
|
237
|
+
@app.route("/")
|
238
|
+
def home(request, response):
|
239
|
+
response.text = "Hello, World!"
|
240
|
+
|
241
|
+
client.get("http://testserver/")
|
242
|
+
|
243
|
+
assert process_request_called is True
|
244
|
+
assert process_response_called is True
|
245
|
+
|
246
|
+
|
247
|
+
class TestCustomResponses:
|
248
|
+
def test_json_response_helper(self, app, client):
|
249
|
+
@app.route("/json")
|
250
|
+
def json_response(request, response):
|
251
|
+
response.json = {"message": "Hello, JSON!"}
|
252
|
+
|
253
|
+
response = client.get("http://testserver/json")
|
254
|
+
|
255
|
+
assert response.status_code == 200
|
256
|
+
assert response.headers["Content-Type"] == "application/json"
|
257
|
+
assert response.json() == {"message": "Hello, JSON!"}
|
258
|
+
|
259
|
+
def test_text_response_helper(self, app, client):
|
260
|
+
@app.route("/text")
|
261
|
+
def text_response(request, response):
|
262
|
+
response.text = "Hello, Text!"
|
263
|
+
|
264
|
+
response = client.get("http://testserver/text")
|
265
|
+
|
266
|
+
assert response.status_code == 200
|
267
|
+
assert response.headers["Content-Type"] == "text/plain"
|
268
|
+
assert response.text == "Hello, Text!"
|
269
|
+
|
270
|
+
def test_manually_setting_body(self, app, client):
|
271
|
+
@app.route("/manual")
|
272
|
+
def manual_response(request, response):
|
273
|
+
response.body = b"Hello, Manual!"
|
274
|
+
response.content_type = "text/plain"
|
275
|
+
|
276
|
+
response = client.get("http://testserver/manual")
|
277
|
+
|
278
|
+
assert response.status_code == 200
|
279
|
+
assert response.headers["Content-Type"] == "text/plain"
|
280
|
+
assert response.text == "Hello, Manual!"
|
281
|
+
|
282
|
+
def test_custom_headers(self, app, client):
|
283
|
+
@app.route("/headers")
|
284
|
+
def custom_headers(request, response):
|
285
|
+
response.headers["X-Custom-Header"] = "CustomValue"
|
286
|
+
response.text = "Hello, Headers!"
|
287
|
+
|
288
|
+
response = client.get("http://testserver/headers")
|
289
|
+
|
290
|
+
assert response.status_code == 200
|
291
|
+
assert response.text == "Hello, Headers!"
|
292
|
+
assert response.headers["X-Custom-Header"] == "CustomValue"
|
293
|
+
assert response.headers["Content-Type"] == "text/plain"
|