menzoapi 0.0.2__tar.gz → 1.0.0__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.
- menzoapi-1.0.0/PKG-INFO +175 -0
- menzoapi-1.0.0/README.md +143 -0
- menzoapi-1.0.0/menzoapi/__init__.py +32 -0
- menzoapi-1.0.0/menzoapi/app.py +290 -0
- menzoapi-1.0.0/menzoapi/exceptions.py +38 -0
- menzoapi-1.0.0/menzoapi/middleware.py +54 -0
- menzoapi-1.0.0/menzoapi/server.py +167 -0
- menzoapi-1.0.0/menzoapi/static.py +37 -0
- menzoapi-1.0.0/menzoapi/types.py +46 -0
- menzoapi-1.0.0/menzoapi.egg-info/PKG-INFO +175 -0
- {menzoapi-0.0.2 → menzoapi-1.0.0}/menzoapi.egg-info/SOURCES.txt +5 -0
- menzoapi-1.0.0/menzoapi.egg-info/requires.txt +7 -0
- menzoapi-1.0.0/setup.py +38 -0
- menzoapi-0.0.2/PKG-INFO +0 -7
- menzoapi-0.0.2/README.md +0 -90
- menzoapi-0.0.2/menzoapi/__init__.py +0 -9
- menzoapi-0.0.2/menzoapi/app.py +0 -39
- menzoapi-0.0.2/menzoapi/server.py +0 -179
- menzoapi-0.0.2/menzoapi.egg-info/PKG-INFO +0 -7
- menzoapi-0.0.2/setup.py +0 -9
- {menzoapi-0.0.2 → menzoapi-1.0.0}/menzoapi.egg-info/dependency_links.txt +0 -0
- {menzoapi-0.0.2 → menzoapi-1.0.0}/menzoapi.egg-info/top_level.txt +0 -0
- {menzoapi-0.0.2 → menzoapi-1.0.0}/setup.cfg +0 -0
menzoapi-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: menzoapi
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A lightweight FastAPI-inspired framework
|
|
5
|
+
Home-page: https://github.com/menzoapi/menzoapi
|
|
6
|
+
Author: MenzoAPI Contributors
|
|
7
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
20
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
21
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
22
|
+
Requires-Dist: flake8>=6.0.0; extra == "dev"
|
|
23
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
24
|
+
Dynamic: author
|
|
25
|
+
Dynamic: classifier
|
|
26
|
+
Dynamic: description
|
|
27
|
+
Dynamic: description-content-type
|
|
28
|
+
Dynamic: home-page
|
|
29
|
+
Dynamic: provides-extra
|
|
30
|
+
Dynamic: requires-python
|
|
31
|
+
Dynamic: summary
|
|
32
|
+
|
|
33
|
+
# README.md
|
|
34
|
+
# MenzoAPI v1.0.0
|
|
35
|
+
|
|
36
|
+
A lightweight FastAPI-inspired framework for Python 3.10+ with zero external dependencies.
|
|
37
|
+
|
|
38
|
+
## Features
|
|
39
|
+
|
|
40
|
+
- **Routing**: GET, POST, PUT, DELETE, PATCH
|
|
41
|
+
- **Route Parameters**: `/users/{id}`
|
|
42
|
+
- **Query Parameters**: `/search?q=test&page=1`
|
|
43
|
+
- **Request Object**: headers, body, json, params, query, method, path
|
|
44
|
+
- **Response Object**: status, body, headers
|
|
45
|
+
- **JSON Responses**: Automatic serialization
|
|
46
|
+
- **Middleware System**: Multiple middleware support
|
|
47
|
+
- **Exception Handlers**: Custom exception handling
|
|
48
|
+
- **Built-in Docs**: `/docs` endpoint with auto-listed routes
|
|
49
|
+
- **Async Support**: Full async endpoint support
|
|
50
|
+
- **Thread-Safe Routing**: Safe for concurrent requests
|
|
51
|
+
- **Automatic JSON Parsing**: Request body parsing
|
|
52
|
+
- **Automatic Status Codes**: Smart response status detection
|
|
53
|
+
- **Static File Serving**: Serve static files
|
|
54
|
+
- **CORS Support**: Cross-Origin Resource Sharing
|
|
55
|
+
- **Logging**: Built-in logging
|
|
56
|
+
- **Type Hints**: Complete type annotations everywhere
|
|
57
|
+
- **Zero Dependencies**: Pure Python standard library
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install menzoapi
|
|
63
|
+
```
|
|
64
|
+
Quick Start
|
|
65
|
+
```python
|
|
66
|
+
from menzoapi import MenzoAPI, Server
|
|
67
|
+
|
|
68
|
+
app = MenzoAPI()
|
|
69
|
+
|
|
70
|
+
@app.get("/")
|
|
71
|
+
def home(request):
|
|
72
|
+
return {"message": "Hello, MenzoAPI!"}
|
|
73
|
+
|
|
74
|
+
@app.get("/users/{id}")
|
|
75
|
+
def get_user(request):
|
|
76
|
+
user_id = request.params["id"]
|
|
77
|
+
return {"id": user_id, "name": "User " + user_id}
|
|
78
|
+
|
|
79
|
+
@app.post("/users")
|
|
80
|
+
async def create_user(request):
|
|
81
|
+
data = await request.json
|
|
82
|
+
return {"created": True, "data": data}
|
|
83
|
+
|
|
84
|
+
Server(app).run()
|
|
85
|
+
```
|
|
86
|
+
Advanced Usage
|
|
87
|
+
Middleware
|
|
88
|
+
```python
|
|
89
|
+
from menzoapi import MenzoAPI
|
|
90
|
+
from menzoapi.middleware import LoggerMiddleware, CORSMiddleware
|
|
91
|
+
|
|
92
|
+
app = MenzoAPI()
|
|
93
|
+
app.use(LoggerMiddleware())
|
|
94
|
+
app.use(CORSMiddleware(origins=["*"]))
|
|
95
|
+
```
|
|
96
|
+
Exception Handling
|
|
97
|
+
```python
|
|
98
|
+
from menzoapi.exceptions import NotFoundException
|
|
99
|
+
|
|
100
|
+
@app.exception_handler(NotFoundException)
|
|
101
|
+
def handle_not_found(exc):
|
|
102
|
+
return {"error": str(exc)}, 404
|
|
103
|
+
```
|
|
104
|
+
Static Files
|
|
105
|
+
```python
|
|
106
|
+
app.static("/static", "./public")
|
|
107
|
+
```
|
|
108
|
+
CORS
|
|
109
|
+
```python
|
|
110
|
+
app.enable_cors(
|
|
111
|
+
origins=["https://example.com"],
|
|
112
|
+
methods=["GET", "POST"],
|
|
113
|
+
headers=["Content-Type"]
|
|
114
|
+
)
|
|
115
|
+
```
|
|
116
|
+
API Reference
|
|
117
|
+
MenzoAPI
|
|
118
|
+
```python
|
|
119
|
+
@app.get(path) - GET route
|
|
120
|
+
|
|
121
|
+
@app.post(path) - POST route
|
|
122
|
+
|
|
123
|
+
@app.put(path) - PUT route
|
|
124
|
+
|
|
125
|
+
@app.delete(path) - DELETE route
|
|
126
|
+
|
|
127
|
+
@app.patch(path) - PATCH route
|
|
128
|
+
|
|
129
|
+
app.use(middleware) - Add middleware
|
|
130
|
+
|
|
131
|
+
app.exception_handler(type) - Add exception handler
|
|
132
|
+
|
|
133
|
+
app.enable_cors() - Enable CORS
|
|
134
|
+
|
|
135
|
+
app.static(path, directory) - Serve static files
|
|
136
|
+
Request
|
|
137
|
+
request.method - HTTP method
|
|
138
|
+
|
|
139
|
+
request.path - Request path
|
|
140
|
+
|
|
141
|
+
request.headers - Headers dict
|
|
142
|
+
|
|
143
|
+
request.query - Query parameters
|
|
144
|
+
|
|
145
|
+
request.params - Route parameters
|
|
146
|
+
|
|
147
|
+
await request.body - Raw body
|
|
148
|
+
|
|
149
|
+
await request.json - JSON body
|
|
150
|
+
|
|
151
|
+
Response
|
|
152
|
+
Response(status, body, headers) - Create response
|
|
153
|
+
|
|
154
|
+
response.json(data, status) - JSON response
|
|
155
|
+
```
|
|
156
|
+
Server
|
|
157
|
+
```python
|
|
158
|
+
from menzoapi import Server
|
|
159
|
+
|
|
160
|
+
Server(app, host="0.0.0.0", port=8000, workers=4).run()
|
|
161
|
+
host - Bind address (default: 127.0.0.1)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
# ENDING PART
|
|
165
|
+
port - Bind port (default: 8000)
|
|
166
|
+
|
|
167
|
+
workers - Number of worker processes (default: 1)
|
|
168
|
+
Documentation
|
|
169
|
+
Visit /docs endpoint in your browser to see auto-generated API documentation.
|
|
170
|
+
|
|
171
|
+
License
|
|
172
|
+
MIT
|
|
173
|
+
---
|
|
174
|
+
<h1> By Sarthak Kamat </h1>
|
|
175
|
+
---
|
menzoapi-1.0.0/README.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# README.md
|
|
2
|
+
# MenzoAPI v1.0.0
|
|
3
|
+
|
|
4
|
+
A lightweight FastAPI-inspired framework for Python 3.10+ with zero external dependencies.
|
|
5
|
+
|
|
6
|
+
## Features
|
|
7
|
+
|
|
8
|
+
- **Routing**: GET, POST, PUT, DELETE, PATCH
|
|
9
|
+
- **Route Parameters**: `/users/{id}`
|
|
10
|
+
- **Query Parameters**: `/search?q=test&page=1`
|
|
11
|
+
- **Request Object**: headers, body, json, params, query, method, path
|
|
12
|
+
- **Response Object**: status, body, headers
|
|
13
|
+
- **JSON Responses**: Automatic serialization
|
|
14
|
+
- **Middleware System**: Multiple middleware support
|
|
15
|
+
- **Exception Handlers**: Custom exception handling
|
|
16
|
+
- **Built-in Docs**: `/docs` endpoint with auto-listed routes
|
|
17
|
+
- **Async Support**: Full async endpoint support
|
|
18
|
+
- **Thread-Safe Routing**: Safe for concurrent requests
|
|
19
|
+
- **Automatic JSON Parsing**: Request body parsing
|
|
20
|
+
- **Automatic Status Codes**: Smart response status detection
|
|
21
|
+
- **Static File Serving**: Serve static files
|
|
22
|
+
- **CORS Support**: Cross-Origin Resource Sharing
|
|
23
|
+
- **Logging**: Built-in logging
|
|
24
|
+
- **Type Hints**: Complete type annotations everywhere
|
|
25
|
+
- **Zero Dependencies**: Pure Python standard library
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install menzoapi
|
|
31
|
+
```
|
|
32
|
+
Quick Start
|
|
33
|
+
```python
|
|
34
|
+
from menzoapi import MenzoAPI, Server
|
|
35
|
+
|
|
36
|
+
app = MenzoAPI()
|
|
37
|
+
|
|
38
|
+
@app.get("/")
|
|
39
|
+
def home(request):
|
|
40
|
+
return {"message": "Hello, MenzoAPI!"}
|
|
41
|
+
|
|
42
|
+
@app.get("/users/{id}")
|
|
43
|
+
def get_user(request):
|
|
44
|
+
user_id = request.params["id"]
|
|
45
|
+
return {"id": user_id, "name": "User " + user_id}
|
|
46
|
+
|
|
47
|
+
@app.post("/users")
|
|
48
|
+
async def create_user(request):
|
|
49
|
+
data = await request.json
|
|
50
|
+
return {"created": True, "data": data}
|
|
51
|
+
|
|
52
|
+
Server(app).run()
|
|
53
|
+
```
|
|
54
|
+
Advanced Usage
|
|
55
|
+
Middleware
|
|
56
|
+
```python
|
|
57
|
+
from menzoapi import MenzoAPI
|
|
58
|
+
from menzoapi.middleware import LoggerMiddleware, CORSMiddleware
|
|
59
|
+
|
|
60
|
+
app = MenzoAPI()
|
|
61
|
+
app.use(LoggerMiddleware())
|
|
62
|
+
app.use(CORSMiddleware(origins=["*"]))
|
|
63
|
+
```
|
|
64
|
+
Exception Handling
|
|
65
|
+
```python
|
|
66
|
+
from menzoapi.exceptions import NotFoundException
|
|
67
|
+
|
|
68
|
+
@app.exception_handler(NotFoundException)
|
|
69
|
+
def handle_not_found(exc):
|
|
70
|
+
return {"error": str(exc)}, 404
|
|
71
|
+
```
|
|
72
|
+
Static Files
|
|
73
|
+
```python
|
|
74
|
+
app.static("/static", "./public")
|
|
75
|
+
```
|
|
76
|
+
CORS
|
|
77
|
+
```python
|
|
78
|
+
app.enable_cors(
|
|
79
|
+
origins=["https://example.com"],
|
|
80
|
+
methods=["GET", "POST"],
|
|
81
|
+
headers=["Content-Type"]
|
|
82
|
+
)
|
|
83
|
+
```
|
|
84
|
+
API Reference
|
|
85
|
+
MenzoAPI
|
|
86
|
+
```python
|
|
87
|
+
@app.get(path) - GET route
|
|
88
|
+
|
|
89
|
+
@app.post(path) - POST route
|
|
90
|
+
|
|
91
|
+
@app.put(path) - PUT route
|
|
92
|
+
|
|
93
|
+
@app.delete(path) - DELETE route
|
|
94
|
+
|
|
95
|
+
@app.patch(path) - PATCH route
|
|
96
|
+
|
|
97
|
+
app.use(middleware) - Add middleware
|
|
98
|
+
|
|
99
|
+
app.exception_handler(type) - Add exception handler
|
|
100
|
+
|
|
101
|
+
app.enable_cors() - Enable CORS
|
|
102
|
+
|
|
103
|
+
app.static(path, directory) - Serve static files
|
|
104
|
+
Request
|
|
105
|
+
request.method - HTTP method
|
|
106
|
+
|
|
107
|
+
request.path - Request path
|
|
108
|
+
|
|
109
|
+
request.headers - Headers dict
|
|
110
|
+
|
|
111
|
+
request.query - Query parameters
|
|
112
|
+
|
|
113
|
+
request.params - Route parameters
|
|
114
|
+
|
|
115
|
+
await request.body - Raw body
|
|
116
|
+
|
|
117
|
+
await request.json - JSON body
|
|
118
|
+
|
|
119
|
+
Response
|
|
120
|
+
Response(status, body, headers) - Create response
|
|
121
|
+
|
|
122
|
+
response.json(data, status) - JSON response
|
|
123
|
+
```
|
|
124
|
+
Server
|
|
125
|
+
```python
|
|
126
|
+
from menzoapi import Server
|
|
127
|
+
|
|
128
|
+
Server(app, host="0.0.0.0", port=8000, workers=4).run()
|
|
129
|
+
host - Bind address (default: 127.0.0.1)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
# ENDING PART
|
|
133
|
+
port - Bind port (default: 8000)
|
|
134
|
+
|
|
135
|
+
workers - Number of worker processes (default: 1)
|
|
136
|
+
Documentation
|
|
137
|
+
Visit /docs endpoint in your browser to see auto-generated API documentation.
|
|
138
|
+
|
|
139
|
+
License
|
|
140
|
+
MIT
|
|
141
|
+
---
|
|
142
|
+
<h1> By Sarthak Kamat </h1>
|
|
143
|
+
---
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# menzoapi/__init__.py
|
|
2
|
+
from menzoapi.app import MenzoAPI
|
|
3
|
+
from menzoapi.server import Server
|
|
4
|
+
from menzoapi.types import Request, Response
|
|
5
|
+
from menzoapi.middleware import Middleware, LoggerMiddleware, CORSMiddleware
|
|
6
|
+
from menzoapi.exceptions import (
|
|
7
|
+
HTTPException,
|
|
8
|
+
NotFoundException,
|
|
9
|
+
MethodNotAllowedException,
|
|
10
|
+
BadRequestException,
|
|
11
|
+
UnauthorizedException,
|
|
12
|
+
ForbiddenException,
|
|
13
|
+
InternalServerErrorException,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__version__ = "1.0.0"
|
|
17
|
+
__all__ = [
|
|
18
|
+
"MenzoAPI",
|
|
19
|
+
"Server",
|
|
20
|
+
"Request",
|
|
21
|
+
"Response",
|
|
22
|
+
"Middleware",
|
|
23
|
+
"LoggerMiddleware",
|
|
24
|
+
"CORSMiddleware",
|
|
25
|
+
"HTTPException",
|
|
26
|
+
"NotFoundException",
|
|
27
|
+
"MethodNotAllowedException",
|
|
28
|
+
"BadRequestException",
|
|
29
|
+
"UnauthorizedException",
|
|
30
|
+
"ForbiddenException",
|
|
31
|
+
"InternalServerErrorException",
|
|
32
|
+
]
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
# menzoapi/app.py
|
|
2
|
+
import asyncio
|
|
3
|
+
import inspect
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
|
7
|
+
from urllib.parse import parse_qs
|
|
8
|
+
|
|
9
|
+
from menzoapi.types import Request, Response
|
|
10
|
+
from menzoapi.middleware import Middleware
|
|
11
|
+
from menzoapi.exceptions import HTTPException, NotFoundException, MethodNotAllowedException
|
|
12
|
+
from menzoapi.static import StaticFileHandler
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MenzoAPI:
|
|
18
|
+
__slots__ = (
|
|
19
|
+
"_routes",
|
|
20
|
+
"_middlewares",
|
|
21
|
+
"_exception_handlers",
|
|
22
|
+
"_static_handlers",
|
|
23
|
+
"_cors_enabled",
|
|
24
|
+
"_cors_origins",
|
|
25
|
+
"_cors_methods",
|
|
26
|
+
"_cors_headers",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def __init__(self) -> None:
|
|
30
|
+
self._routes: Dict[str, Dict[str, Callable]] = {}
|
|
31
|
+
self._middlewares: List[Middleware] = []
|
|
32
|
+
self._exception_handlers: Dict[type, Callable] = {}
|
|
33
|
+
self._static_handlers: List[StaticFileHandler] = []
|
|
34
|
+
self._cors_enabled: bool = False
|
|
35
|
+
self._cors_origins: List[str] = []
|
|
36
|
+
self._cors_methods: List[str] = []
|
|
37
|
+
self._cors_headers: List[str] = []
|
|
38
|
+
|
|
39
|
+
def _add_route(self, path: str, method: str, handler: Callable) -> None:
|
|
40
|
+
self._routes.setdefault(path, {})[method] = handler
|
|
41
|
+
|
|
42
|
+
def get(self, path: str) -> Callable:
|
|
43
|
+
def decorator(handler: Callable) -> Callable:
|
|
44
|
+
self._add_route(path, "GET", handler)
|
|
45
|
+
return handler
|
|
46
|
+
return decorator
|
|
47
|
+
|
|
48
|
+
def post(self, path: str) -> Callable:
|
|
49
|
+
def decorator(handler: Callable) -> Callable:
|
|
50
|
+
self._add_route(path, "POST", handler)
|
|
51
|
+
return handler
|
|
52
|
+
return decorator
|
|
53
|
+
|
|
54
|
+
def put(self, path: str) -> Callable:
|
|
55
|
+
def decorator(handler: Callable) -> Callable:
|
|
56
|
+
self._add_route(path, "PUT", handler)
|
|
57
|
+
return handler
|
|
58
|
+
return decorator
|
|
59
|
+
|
|
60
|
+
def delete(self, path: str) -> Callable:
|
|
61
|
+
def decorator(handler: Callable) -> Callable:
|
|
62
|
+
self._add_route(path, "DELETE", handler)
|
|
63
|
+
return handler
|
|
64
|
+
return decorator
|
|
65
|
+
|
|
66
|
+
def patch(self, path: str) -> Callable:
|
|
67
|
+
def decorator(handler: Callable) -> Callable:
|
|
68
|
+
self._add_route(path, "PATCH", handler)
|
|
69
|
+
return handler
|
|
70
|
+
return decorator
|
|
71
|
+
|
|
72
|
+
def use(self, middleware: Middleware) -> None:
|
|
73
|
+
self._middlewares.append(middleware)
|
|
74
|
+
|
|
75
|
+
def exception_handler(self, exception_type: type) -> Callable:
|
|
76
|
+
def decorator(handler: Callable) -> Callable:
|
|
77
|
+
self._exception_handlers[exception_type] = handler
|
|
78
|
+
return handler
|
|
79
|
+
return decorator
|
|
80
|
+
|
|
81
|
+
def enable_cors(
|
|
82
|
+
self,
|
|
83
|
+
origins: Optional[List[str]] = None,
|
|
84
|
+
methods: Optional[List[str]] = None,
|
|
85
|
+
headers: Optional[List[str]] = None,
|
|
86
|
+
) -> None:
|
|
87
|
+
self._cors_enabled = True
|
|
88
|
+
self._cors_origins = origins or ["*"]
|
|
89
|
+
self._cors_methods = methods or ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
|
90
|
+
self._cors_headers = headers or ["Content-Type", "Authorization"]
|
|
91
|
+
|
|
92
|
+
def static(self, path: str, directory: str) -> None:
|
|
93
|
+
self._static_handlers.append(StaticFileHandler(path, directory))
|
|
94
|
+
|
|
95
|
+
def _parse_path_params(self, route_path: str, actual_path: str) -> Dict[str, str]:
|
|
96
|
+
route_parts = route_path.strip("/").split("/")
|
|
97
|
+
actual_parts = actual_path.strip("/").split("/")
|
|
98
|
+
if len(route_parts) != len(actual_parts):
|
|
99
|
+
return {}
|
|
100
|
+
|
|
101
|
+
params = {}
|
|
102
|
+
for rp, ap in zip(route_parts, actual_parts):
|
|
103
|
+
if rp.startswith("{") and rp.endswith("}"):
|
|
104
|
+
params[rp[1:-1]] = ap
|
|
105
|
+
elif rp != ap:
|
|
106
|
+
return {}
|
|
107
|
+
return params
|
|
108
|
+
|
|
109
|
+
def _match_route(self, path: str, method: str) -> Tuple[Optional[Callable], Dict[str, str]]:
|
|
110
|
+
for route_path, methods in self._routes.items():
|
|
111
|
+
if route_path == path:
|
|
112
|
+
return (methods.get(method), {}) if method in methods else (None, {})
|
|
113
|
+
if "{" in route_path:
|
|
114
|
+
params = self._parse_path_params(route_path, path)
|
|
115
|
+
if params and method in methods:
|
|
116
|
+
return methods[method], params
|
|
117
|
+
return None, {}
|
|
118
|
+
|
|
119
|
+
def _handle_exception(self, exc: Exception) -> Response:
|
|
120
|
+
for exc_type, handler in self._exception_handlers.items():
|
|
121
|
+
if isinstance(exc, exc_type):
|
|
122
|
+
result = handler(exc)
|
|
123
|
+
if inspect.iscoroutine(result):
|
|
124
|
+
loop = asyncio.new_event_loop()
|
|
125
|
+
asyncio.set_event_loop(loop)
|
|
126
|
+
result = loop.run_until_complete(result)
|
|
127
|
+
loop.close()
|
|
128
|
+
return result if isinstance(result, Response) else self._normalize_response(result)
|
|
129
|
+
|
|
130
|
+
if isinstance(exc, NotFoundException):
|
|
131
|
+
return Response(404, json.dumps({"error": "Not Found"}), {"Content-Type": "application/json"})
|
|
132
|
+
if isinstance(exc, MethodNotAllowedException):
|
|
133
|
+
return Response(405, json.dumps({"error": "Method Not Allowed"}), {"Content-Type": "application/json"})
|
|
134
|
+
if isinstance(exc, HTTPException):
|
|
135
|
+
return Response(exc.status_code, json.dumps({"error": exc.detail}), {"Content-Type": "application/json"})
|
|
136
|
+
|
|
137
|
+
logger.error(f"Unhandled exception: {exc}", exc_info=True)
|
|
138
|
+
return Response(500, json.dumps({"error": "Internal Server Error"}), {"Content-Type": "application/json"})
|
|
139
|
+
|
|
140
|
+
def _cors_headers(self) -> Dict[str, str]:
|
|
141
|
+
if not self._cors_enabled:
|
|
142
|
+
return {}
|
|
143
|
+
return {
|
|
144
|
+
"Access-Control-Allow-Origin": "*" if "*" in self._cors_origins else ", ".join(self._cors_origins),
|
|
145
|
+
"Access-Control-Allow-Methods": ", ".join(self._cors_methods),
|
|
146
|
+
"Access-Control-Allow-Headers": ", ".join(self._cors_headers),
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
def _prepare_request(self, scope: Dict[str, Any], receive: Callable) -> Request:
|
|
150
|
+
query_string = scope.get("query_string", b"").decode()
|
|
151
|
+
headers = {}
|
|
152
|
+
for k, v in scope.get("headers", []):
|
|
153
|
+
headers[k.decode().lower()] = v.decode()
|
|
154
|
+
|
|
155
|
+
return Request(
|
|
156
|
+
method=scope.get("method", "GET"),
|
|
157
|
+
path=scope.get("path", "/"),
|
|
158
|
+
headers=headers,
|
|
159
|
+
query=parse_qs(query_string),
|
|
160
|
+
scope=scope,
|
|
161
|
+
receive=receive,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
async def _apply_middleware(self, request: Request, handler: Callable) -> Response:
|
|
165
|
+
async def process(idx: int) -> Response:
|
|
166
|
+
if idx >= len(self._middlewares):
|
|
167
|
+
return await self._call_handler(handler, request)
|
|
168
|
+
return await self._middlewares[idx].process(request, lambda req: process(idx + 1))
|
|
169
|
+
return await process(0)
|
|
170
|
+
|
|
171
|
+
async def _call_handler(self, handler: Callable, request: Request) -> Response:
|
|
172
|
+
result = await handler(request) if inspect.iscoroutinefunction(handler) else handler(request)
|
|
173
|
+
return result if isinstance(result, Response) else self._normalize_response(result)
|
|
174
|
+
|
|
175
|
+
def _normalize_response(self, result: Any) -> Response:
|
|
176
|
+
if result is None:
|
|
177
|
+
return Response(204, b"")
|
|
178
|
+
if isinstance(result, dict):
|
|
179
|
+
return Response(200, json.dumps(result), {"Content-Type": "application/json"})
|
|
180
|
+
if isinstance(result, (str, bytes)):
|
|
181
|
+
content_type = "text/plain" if isinstance(result, str) else "application/octet-stream"
|
|
182
|
+
return Response(200, result.encode() if isinstance(result, str) else result, {"Content-Type": content_type})
|
|
183
|
+
return Response(200, json.dumps({"result": str(result)}), {"Content-Type": "application/json"})
|
|
184
|
+
|
|
185
|
+
def _docs_routes(self) -> List[Dict[str, Any]]:
|
|
186
|
+
return [
|
|
187
|
+
{"path": p, "method": m, "handler": h.__name__}
|
|
188
|
+
for p, methods in self._routes.items()
|
|
189
|
+
for m, h in methods.items()
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
async def _serve_static(self, path: str) -> Optional[Response]:
|
|
193
|
+
for handler in self._static_handlers:
|
|
194
|
+
if path.startswith(handler.path_prefix):
|
|
195
|
+
return await handler.serve(path)
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
async def _handle_request(self, scope: Dict[str, Any], receive: Callable) -> Response:
|
|
199
|
+
path = scope.get("path", "/")
|
|
200
|
+
method = scope.get("method", "GET")
|
|
201
|
+
|
|
202
|
+
if method == "OPTIONS" and self._cors_enabled:
|
|
203
|
+
return Response(200, b"", self._cors_headers())
|
|
204
|
+
|
|
205
|
+
if path == "/docs":
|
|
206
|
+
return Response(200, self._docs_html().encode(), {"Content-Type": "text/html"})
|
|
207
|
+
|
|
208
|
+
static_resp = await self._serve_static(path)
|
|
209
|
+
if static_resp:
|
|
210
|
+
return static_resp
|
|
211
|
+
|
|
212
|
+
request = self._prepare_request(scope, receive)
|
|
213
|
+
handler, path_params = self._match_route(path, method)
|
|
214
|
+
|
|
215
|
+
if not handler:
|
|
216
|
+
if path in self._routes:
|
|
217
|
+
raise MethodNotAllowedException(f"Method {method} not allowed for {path}")
|
|
218
|
+
raise NotFoundException(f"Route {path} not found")
|
|
219
|
+
|
|
220
|
+
request.params = path_params
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
response = await self._apply_middleware(request, handler)
|
|
224
|
+
if self._cors_enabled:
|
|
225
|
+
response.headers.update(self._cors_headers())
|
|
226
|
+
return response
|
|
227
|
+
except Exception as e:
|
|
228
|
+
return self._handle_exception(e)
|
|
229
|
+
|
|
230
|
+
def _docs_html(self) -> str:
|
|
231
|
+
routes = self._docs_routes()
|
|
232
|
+
rows = "".join(
|
|
233
|
+
f"""
|
|
234
|
+
<tr>
|
|
235
|
+
<td><span class="method method-{r['method'].lower()}">{r['method']}</span></td>
|
|
236
|
+
<td><code>{r['path']}</code></td>
|
|
237
|
+
<td>{r['handler']}</td>
|
|
238
|
+
</tr>"""
|
|
239
|
+
for r in routes
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
return f"""
|
|
243
|
+
<!DOCTYPE html>
|
|
244
|
+
<html>
|
|
245
|
+
<head>
|
|
246
|
+
<meta charset="utf-8">
|
|
247
|
+
<title>MenzoAPI Documentation</title>
|
|
248
|
+
<style>
|
|
249
|
+
body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; max-width: 1200px; margin: 40px auto; padding: 0 20px; }}
|
|
250
|
+
h1 {{ color: #333; border-bottom: 2px solid #eee; padding-bottom: 10px; }}
|
|
251
|
+
table {{ width: 100%; border-collapse: collapse; margin-top: 20px; }}
|
|
252
|
+
th {{ text-align: left; padding: 12px; background: #f5f5f5; font-weight: 600; }}
|
|
253
|
+
td {{ padding: 12px; border-bottom: 1px solid #eee; }}
|
|
254
|
+
.method {{ display: inline-block; padding: 4px 12px; border-radius: 4px; font-weight: 600; font-size: 14px; }}
|
|
255
|
+
.method-get {{ background: #61affe; color: white; }}
|
|
256
|
+
.method-post {{ background: #49cc90; color: white; }}
|
|
257
|
+
.method-put {{ background: #fca130; color: white; }}
|
|
258
|
+
.method-delete {{ background: #f93e3e; color: white; }}
|
|
259
|
+
.method-patch {{ background: #50e3c2; color: white; }}
|
|
260
|
+
code {{ background: #f5f5f5; padding: 2px 6px; border-radius: 3px; font-size: 14px; }}
|
|
261
|
+
.info {{ margin-top: 20px; padding: 15px; background: #f8f9fa; border-radius: 4px; }}
|
|
262
|
+
</style>
|
|
263
|
+
</head>
|
|
264
|
+
<body>
|
|
265
|
+
<h1>MenzoAPI v1.0.0 Documentation</h1>
|
|
266
|
+
<div class="info"><strong>Total Routes:</strong> {len(routes)}</div>
|
|
267
|
+
<table>
|
|
268
|
+
<thead><tr><th>Method</th><th>Path</th><th>Handler</th></tr></thead>
|
|
269
|
+
<tbody>{rows}</tbody>
|
|
270
|
+
</table>
|
|
271
|
+
</body>
|
|
272
|
+
</html>
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
async def __call__(self, scope: Dict[str, Any], receive: Callable, send: Callable) -> None:
|
|
276
|
+
if scope["type"] != "http":
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
response = await self._handle_request(scope, receive)
|
|
281
|
+
headers = [(k.encode(), v.encode()) for k, v in response.headers.items()]
|
|
282
|
+
|
|
283
|
+
await send({"type": "http.response.start", "status": response.status, "headers": headers})
|
|
284
|
+
body = response.body if isinstance(response.body, bytes) else response.body.encode()
|
|
285
|
+
await send({"type": "http.response.body", "body": body})
|
|
286
|
+
|
|
287
|
+
except Exception as e:
|
|
288
|
+
logger.error(f"Error processing request: {e}", exc_info=True)
|
|
289
|
+
await send({"type": "http.response.start", "status": 500, "headers": [(b"content-type", b"application/json")]})
|
|
290
|
+
await send({"type": "http.response.body", "body": b'{"error": "Internal Server Error"}'})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# menzoapi/exceptions.py
|
|
2
|
+
class HTTPException(Exception):
|
|
3
|
+
__slots__ = ("status_code", "detail")
|
|
4
|
+
|
|
5
|
+
def __init__(self, status_code: int, detail: str = "") -> None:
|
|
6
|
+
self.status_code = status_code
|
|
7
|
+
self.detail = detail
|
|
8
|
+
super().__init__(detail or f"HTTP {status_code}")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class NotFoundException(HTTPException):
|
|
12
|
+
def __init__(self, detail: str = "Not Found") -> None:
|
|
13
|
+
super().__init__(404, detail)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MethodNotAllowedException(HTTPException):
|
|
17
|
+
def __init__(self, detail: str = "Method Not Allowed") -> None:
|
|
18
|
+
super().__init__(405, detail)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BadRequestException(HTTPException):
|
|
22
|
+
def __init__(self, detail: str = "Bad Request") -> None:
|
|
23
|
+
super().__init__(400, detail)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class UnauthorizedException(HTTPException):
|
|
27
|
+
def __init__(self, detail: str = "Unauthorized") -> None:
|
|
28
|
+
super().__init__(401, detail)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ForbiddenException(HTTPException):
|
|
32
|
+
def __init__(self, detail: str = "Forbidden") -> None:
|
|
33
|
+
super().__init__(403, detail)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class InternalServerErrorException(HTTPException):
|
|
37
|
+
def __init__(self, detail: str = "Internal Server Error") -> None:
|
|
38
|
+
super().__init__(500, detail)
|