serpy-rest 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.
serpy/__init__.py
ADDED
@@ -0,0 +1,190 @@
|
|
1
|
+
import json
|
2
|
+
from urllib.parse import parse_qs
|
3
|
+
|
4
|
+
class Request:
|
5
|
+
def __init__(self, scope, receive):
|
6
|
+
self.scope = scope
|
7
|
+
self._query_params = None
|
8
|
+
self.receive = receive
|
9
|
+
self._headers = None
|
10
|
+
|
11
|
+
async def json(self):
|
12
|
+
body = b''
|
13
|
+
more_body = True
|
14
|
+
while more_body:
|
15
|
+
message = await self.receive()
|
16
|
+
body += message.get('body', b'')
|
17
|
+
more_body = message.get('more_body', False)
|
18
|
+
|
19
|
+
if not body:
|
20
|
+
return None
|
21
|
+
return json.loads(body)
|
22
|
+
|
23
|
+
@property
|
24
|
+
def path(self):
|
25
|
+
return self.scope['path']
|
26
|
+
|
27
|
+
|
28
|
+
@property
|
29
|
+
def method(self):
|
30
|
+
return self.scope['method']
|
31
|
+
|
32
|
+
|
33
|
+
@property
|
34
|
+
def headers(self):
|
35
|
+
if self._headers is None:
|
36
|
+
self._headers = {
|
37
|
+
key.decode('utf-8'): value.decode('utf-8')
|
38
|
+
for key, value in self.scope['headers']
|
39
|
+
}
|
40
|
+
return self._headers
|
41
|
+
|
42
|
+
|
43
|
+
@property
|
44
|
+
def query(self):
|
45
|
+
if self._query_params is None:
|
46
|
+
query_string = self.scope['query_string'].decode("utf-8")
|
47
|
+
self._query_params = QueryParams(query_string)
|
48
|
+
return self._query_params
|
49
|
+
|
50
|
+
|
51
|
+
|
52
|
+
class QueryParams:
|
53
|
+
def __init__(self, query_string):
|
54
|
+
self._params = parse_qs(query_string)
|
55
|
+
|
56
|
+
def get(self, key, default=None):
|
57
|
+
values = self._params.get(key, [])
|
58
|
+
return values[0] if values else default
|
59
|
+
|
60
|
+
def getall(self, key, default=None):
|
61
|
+
return self._params.get(key, default or [])
|
62
|
+
|
63
|
+
|
64
|
+
|
65
|
+
class Response:
|
66
|
+
def __init__(self, content, status_code=200):
|
67
|
+
self.content = content
|
68
|
+
self.status_code = status_code
|
69
|
+
self.headers = {}
|
70
|
+
|
71
|
+
async def send_to_asgi(self, send):
|
72
|
+
body = json.dumps(self.content).encode("utf-8")
|
73
|
+
|
74
|
+
headers_list = [[b"content-type", b"application/json"]]
|
75
|
+
for key, value in self.headers.items():
|
76
|
+
headers_list.append([key.encode("utf-8"), value.encode("utf-8")])
|
77
|
+
|
78
|
+
await send({
|
79
|
+
"type": "http.response.start",
|
80
|
+
"status": self.status_code,
|
81
|
+
"headers": headers_list,
|
82
|
+
})
|
83
|
+
await send({"type": "http.response.body", "body": body})
|
84
|
+
|
85
|
+
|
86
|
+
|
87
|
+
class Router:
|
88
|
+
def __init__(self):
|
89
|
+
self.routes = {}
|
90
|
+
|
91
|
+
def route(self, path, methods=None):
|
92
|
+
if methods is None: methods = ["GET"]
|
93
|
+
def wrapper(handler):
|
94
|
+
for method in methods:
|
95
|
+
self.routes[(method.upper(), path)] = handler
|
96
|
+
return handler
|
97
|
+
return wrapper
|
98
|
+
|
99
|
+
|
100
|
+
|
101
|
+
class MiddlewareWrapper:
|
102
|
+
def __init__(self, app, middleware_func):
|
103
|
+
self.app = app
|
104
|
+
self.middleware_func = middleware_func
|
105
|
+
|
106
|
+
async def __call__(self, scope, receive):
|
107
|
+
async def call_next(request):
|
108
|
+
response = await self.app(scope, receive)
|
109
|
+
return response
|
110
|
+
|
111
|
+
request = Request(scope, receive)
|
112
|
+
response = await self.middleware_func(request, call_next)
|
113
|
+
return response
|
114
|
+
|
115
|
+
|
116
|
+
|
117
|
+
class SerPy:
|
118
|
+
def __init__(self):
|
119
|
+
self.routes = {}
|
120
|
+
self.app = self._dispatch_request
|
121
|
+
|
122
|
+
def add_middleware(self, middleware_func):
|
123
|
+
self.app = MiddlewareWrapper(self.app, middleware_func)
|
124
|
+
|
125
|
+
def include_router(self, router: Router, prefix=""):
|
126
|
+
for (method, path), handler in router.routes.items():
|
127
|
+
self.routes[(method, prefix + path)] = handler
|
128
|
+
|
129
|
+
def route(self, path, methods=None, middleware=None):
|
130
|
+
if methods is None: methods = ["GET"]
|
131
|
+
if middleware is None: middleware = []
|
132
|
+
|
133
|
+
def wrapper(handler):
|
134
|
+
for method in methods:
|
135
|
+
self.routes[(method.upper(), path)] = (handler, middleware)
|
136
|
+
return handler
|
137
|
+
return wrapper
|
138
|
+
|
139
|
+
def find_route(self, method, path):
|
140
|
+
for (route_method, route_path), (handler, middleware) in self.routes.items():
|
141
|
+
|
142
|
+
if route_method != method: continue
|
143
|
+
|
144
|
+
route_parts = route_path.split("/")
|
145
|
+
path_parts = path.split("/")
|
146
|
+
|
147
|
+
if len(route_parts) != len(path_parts): continue
|
148
|
+
|
149
|
+
params = {}
|
150
|
+
match = True
|
151
|
+
|
152
|
+
for route_part, path_part in zip(route_parts, path_parts):
|
153
|
+
if route_part.startswith('{') and route_part.endswith('}'):
|
154
|
+
param_name = route_part[1:-1]
|
155
|
+
params[param_name] = path_part
|
156
|
+
elif route_part != path_part:
|
157
|
+
match = False
|
158
|
+
break
|
159
|
+
if match:
|
160
|
+
return handler, params, middleware
|
161
|
+
return None, None, []
|
162
|
+
|
163
|
+
async def _dispatch_request(self, scope, receive):
|
164
|
+
|
165
|
+
request = Request(scope, receive)
|
166
|
+
handler, params, route_middleware = self.find_route(request.method, request.path)
|
167
|
+
|
168
|
+
if not handler:
|
169
|
+
return Response(content={"error": "Route Not Found"}, status_code=404)
|
170
|
+
|
171
|
+
async def handler_call(req):
|
172
|
+
return await handler(req, **params)
|
173
|
+
|
174
|
+
chain = handler_call
|
175
|
+
for mw in reversed(route_middleware):
|
176
|
+
chain = self._wrap_middleware_for_dispatch(mw, chain)
|
177
|
+
|
178
|
+
response = await chain(request)
|
179
|
+
return response
|
180
|
+
|
181
|
+
def _wrap_middleware_for_dispatch(self, middleware_func, next_in_chain):
|
182
|
+
async def new_chain_link(request):
|
183
|
+
return await middleware_func(request, next_in_chain)
|
184
|
+
return new_chain_link
|
185
|
+
|
186
|
+
|
187
|
+
async def __call__(self, scope, receive, send):
|
188
|
+
if scope["type"] != "http": return
|
189
|
+
response = await self.app(scope, receive)
|
190
|
+
await response.send_to_asgi(send)
|
@@ -0,0 +1,187 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: serpy-rest
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: A minimal ASGI web framework for Python.
|
5
|
+
Home-page: https://github.com/ragav2005/SerPy
|
6
|
+
Author: Ragav Vignes
|
7
|
+
Author-email: ragavvignesviswanathan@gmail.com
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
10
|
+
Classifier: Operating System :: OS Independent
|
11
|
+
Classifier: Framework :: AsyncIO
|
12
|
+
Requires-Python: >=3.7
|
13
|
+
Description-Content-Type: text/markdown
|
14
|
+
Dynamic: author
|
15
|
+
Dynamic: author-email
|
16
|
+
Dynamic: classifier
|
17
|
+
Dynamic: description
|
18
|
+
Dynamic: description-content-type
|
19
|
+
Dynamic: home-page
|
20
|
+
Dynamic: requires-python
|
21
|
+
Dynamic: summary
|
22
|
+
|
23
|
+
# SerPy
|
24
|
+
|
25
|
+
SerPy is a minimal, modern, and lightning-fast ASGI web framework for Python. It is designed for simplicity, flexibility, and performance, making it ideal for building APIs, microservices, and web applications with minimal overhead.
|
26
|
+
|
27
|
+
---
|
28
|
+
|
29
|
+
## Table of Contents
|
30
|
+
|
31
|
+
- [Features](#features)
|
32
|
+
- [Installation](#installation)
|
33
|
+
- [Quick Start](#quick-start)
|
34
|
+
- [Example Project](#example-project)
|
35
|
+
- [Middleware](#middleware)
|
36
|
+
- [Advanced Routing](#advanced-routing)
|
37
|
+
- [Contributing](#contributing)
|
38
|
+
- [Running Locally](#running-locally)
|
39
|
+
- [License](#license)
|
40
|
+
- [Contact](#contact)
|
41
|
+
|
42
|
+
---
|
43
|
+
|
44
|
+
## Features
|
45
|
+
|
46
|
+
- **ASGI compatible**: Works seamlessly with ASGI servers like Uvicorn and Hypercorn
|
47
|
+
- **Simple routing**: Path parameters, method-based routing, and easy route registration
|
48
|
+
- **Middleware support**: Add custom middleware for logging, authentication, CORS, etc.
|
49
|
+
- **Request/Response abstraction**: Clean API for handling requests and responses
|
50
|
+
- **Modular**: Use routers to organize your application
|
51
|
+
- **Type hints**: Modern Pythonic code with type hints
|
52
|
+
|
53
|
+
---
|
54
|
+
|
55
|
+
## Installation
|
56
|
+
|
57
|
+
Install the latest release from PyPI:
|
58
|
+
|
59
|
+
```bash
|
60
|
+
pip install serpy-asgi
|
61
|
+
```
|
62
|
+
|
63
|
+
Or, to use the latest development version, clone this repository:
|
64
|
+
|
65
|
+
```bash
|
66
|
+
git clone https://github.com/ragav2005/SerPy.git
|
67
|
+
cd SerPy
|
68
|
+
pip install .
|
69
|
+
```
|
70
|
+
|
71
|
+
---
|
72
|
+
|
73
|
+
## Quick Start
|
74
|
+
|
75
|
+
Create a file called `main.py`:
|
76
|
+
|
77
|
+
```python
|
78
|
+
from serpy import SerPy, Response
|
79
|
+
|
80
|
+
app = SerPy()
|
81
|
+
|
82
|
+
@app.route("/hello/{name}", methods=["GET"])
|
83
|
+
async def hello(request, name):
|
84
|
+
return Response({"message": f"Hello, {name}!"})
|
85
|
+
|
86
|
+
# Run with: uvicorn main:app
|
87
|
+
```
|
88
|
+
|
89
|
+
Start the server:
|
90
|
+
|
91
|
+
```bash
|
92
|
+
uvicorn main:app --reload
|
93
|
+
```
|
94
|
+
|
95
|
+
---
|
96
|
+
|
97
|
+
## Example Project Structure
|
98
|
+
|
99
|
+
```
|
100
|
+
myproject/
|
101
|
+
├── main.py
|
102
|
+
├── requirements.txt
|
103
|
+
└── ...
|
104
|
+
```
|
105
|
+
|
106
|
+
---
|
107
|
+
|
108
|
+
## Middleware
|
109
|
+
|
110
|
+
Add global middleware to your app:
|
111
|
+
|
112
|
+
```python
|
113
|
+
async def log_middleware(request, call_next):
|
114
|
+
print(f"Request: {request.method} {request.path}")
|
115
|
+
response = await call_next(request)
|
116
|
+
return response
|
117
|
+
|
118
|
+
app.add_middleware(log_middleware)
|
119
|
+
```
|
120
|
+
|
121
|
+
---
|
122
|
+
|
123
|
+
## Advanced Routing
|
124
|
+
|
125
|
+
Use routers to organize endpoints:
|
126
|
+
|
127
|
+
```python
|
128
|
+
from serpy import Router
|
129
|
+
|
130
|
+
user_router = Router()
|
131
|
+
|
132
|
+
@user_router.route("/users/{user_id}", methods=["GET"])
|
133
|
+
async def get_user(request, user_id):
|
134
|
+
# Fetch user logic here
|
135
|
+
return Response({"user_id": user_id})
|
136
|
+
|
137
|
+
app.include_router(user_router, prefix="/api")
|
138
|
+
```
|
139
|
+
|
140
|
+
---
|
141
|
+
|
142
|
+
## Contributing
|
143
|
+
|
144
|
+
Contributions are welcome! To contribute:
|
145
|
+
|
146
|
+
1. Fork the repository
|
147
|
+
2. Create a new branch (`git checkout -b feature/your-feature`)
|
148
|
+
3. Commit your changes
|
149
|
+
4. Push to your fork and submit a pull request
|
150
|
+
|
151
|
+
Please ensure your code follows PEP8 and includes tests where appropriate.
|
152
|
+
|
153
|
+
---
|
154
|
+
|
155
|
+
## Running Locally (Development)
|
156
|
+
|
157
|
+
1. Clone the repository:
|
158
|
+
```bash
|
159
|
+
git clone https://github.com/ragav2005/SerPy.git
|
160
|
+
cd SerPy
|
161
|
+
```
|
162
|
+
2. (Optional) Create a virtual environment:
|
163
|
+
```bash
|
164
|
+
python3 -m venv .venv
|
165
|
+
source .venv/bin/activate
|
166
|
+
```
|
167
|
+
3. Install dependencies:
|
168
|
+
```bash
|
169
|
+
pip install -r requirements.txt
|
170
|
+
```
|
171
|
+
4. Run the example app or your own app with Uvicorn:
|
172
|
+
```bash
|
173
|
+
uvicorn main:app --reload
|
174
|
+
```
|
175
|
+
|
176
|
+
---
|
177
|
+
|
178
|
+
## License
|
179
|
+
|
180
|
+
This project is licensed under the MIT License.
|
181
|
+
|
182
|
+
---
|
183
|
+
|
184
|
+
## Contact
|
185
|
+
|
186
|
+
Author: [ragav2005](https://github.com/ragav2005)
|
187
|
+
For questions, suggestions, or issues, please open an issue on GitHub.
|
@@ -0,0 +1,5 @@
|
|
1
|
+
serpy/__init__.py,sha256=Wl08BQXRIsHSGJ85OqC05o2UqRdmItRBtiKbXHj-rUg,5616
|
2
|
+
serpy_rest-0.1.0.dist-info/METADATA,sha256=Eb3r-BDvv9Mf3MYB_8QQn1cnzTbRkWs_j0uKlSghDDo,3918
|
3
|
+
serpy_rest-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
4
|
+
serpy_rest-0.1.0.dist-info/top_level.txt,sha256=taqeUQycKG1R6VbUW9mD8vWeuHPE6UouxvPjgquT3tk,6
|
5
|
+
serpy_rest-0.1.0.dist-info/RECORD,,
|
@@ -0,0 +1 @@
|
|
1
|
+
serpy
|