bamboo-framework 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.
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bamboo-framework
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight, readable Python API framework. Simple by design.
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/myaxiomai/bambooapi
|
|
7
|
+
Project-URL: Repository, https://github.com/myaxiomai/bambooapi
|
|
8
|
+
Keywords: api,framework,asgi,web,bamboo
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# Bamboo
|
|
22
|
+
|
|
23
|
+
A lightweight, readable Python API framework. Simple by design.
|
|
24
|
+
|
|
25
|
+
Bamboo is an ASGI micro-framework you can read in one sitting and fully understand.
|
|
26
|
+
No magic. No hidden complexity. Just clean, honest Python.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install bambooapi
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quickstart
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from bambooapi import Bamboo, Response
|
|
38
|
+
|
|
39
|
+
app = Bamboo()
|
|
40
|
+
|
|
41
|
+
@app.get("/")
|
|
42
|
+
async def home(request):
|
|
43
|
+
"""Welcome message."""
|
|
44
|
+
return {"message": "Hello from Bamboo"}
|
|
45
|
+
|
|
46
|
+
@app.post("/notes")
|
|
47
|
+
async def create_note(request):
|
|
48
|
+
"""Create a note."""
|
|
49
|
+
data = await request.json()
|
|
50
|
+
return Response({"note": data}, status=201)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Run it:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
uvicorn myapp:app
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Then visit `http://localhost:8000/docs` for the interactive API docs.
|
|
60
|
+
|
|
61
|
+
## Features
|
|
62
|
+
|
|
63
|
+
- GET, POST, PUT, DELETE routing
|
|
64
|
+
- Path parameters: `/notes/{note_id}`
|
|
65
|
+
- Async request body and JSON parsing
|
|
66
|
+
- Auto-generated OpenAPI spec at `/openapi.json`
|
|
67
|
+
- Interactive docs page at `/docs`
|
|
68
|
+
- Zero dependencies beyond an ASGI server
|
|
69
|
+
|
|
70
|
+
## Philosophy
|
|
71
|
+
|
|
72
|
+
The entire framework fits in one file you can read in an afternoon.
|
|
73
|
+
If you can read it, you can understand it. If you can understand it, you can trust it.
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
MIT
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
bambooapi/__init__.py,sha256=A2IC6g3a9RzQEBFGIkorhUhh9Z8isIVFEgdzdrCJSrs,6215
|
|
2
|
+
bamboo_framework-0.1.0.dist-info/METADATA,sha256=aRUcABtaIRhJJpKOhyFIx4DkLaCB0vEHzuJZlfL5Fg0,2007
|
|
3
|
+
bamboo_framework-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
4
|
+
bamboo_framework-0.1.0.dist-info/top_level.txt,sha256=OC2Ykq0zH09-CCQQ5ZCLGM41mPiAXAL-Bg2k-3ghCJ8,10
|
|
5
|
+
bamboo_framework-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
bambooapi
|
bambooapi/__init__.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Response:
|
|
6
|
+
def __init__(self, body, status=200, headers=None):
|
|
7
|
+
self.body = body
|
|
8
|
+
self.status = status
|
|
9
|
+
self.headers = headers or []
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Request:
|
|
13
|
+
def __init__(self, scope, receive):
|
|
14
|
+
self.scope = scope
|
|
15
|
+
self.receive = receive
|
|
16
|
+
self.method = scope["method"]
|
|
17
|
+
self.path = scope["path"]
|
|
18
|
+
|
|
19
|
+
async def body(self):
|
|
20
|
+
chunks = []
|
|
21
|
+
more = True
|
|
22
|
+
while more:
|
|
23
|
+
event = await self.receive()
|
|
24
|
+
chunks.append(event.get("body", b""))
|
|
25
|
+
more = event.get("more_body", False)
|
|
26
|
+
return b"".join(chunks)
|
|
27
|
+
|
|
28
|
+
async def json(self):
|
|
29
|
+
raw = await self.body()
|
|
30
|
+
if not raw:
|
|
31
|
+
return None
|
|
32
|
+
return json.loads(raw)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
SWAGGER_UI_HTML = """<!DOCTYPE html>
|
|
36
|
+
<html lang="en">
|
|
37
|
+
<head>
|
|
38
|
+
<meta charset="utf-8" />
|
|
39
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
40
|
+
<title>{title} - API docs</title>
|
|
41
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css" />
|
|
42
|
+
</head>
|
|
43
|
+
<body>
|
|
44
|
+
<div id="swagger-ui"></div>
|
|
45
|
+
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
|
|
46
|
+
<script>
|
|
47
|
+
window.onload = function () {{
|
|
48
|
+
window.ui = SwaggerUIBundle({{
|
|
49
|
+
url: "/openapi.json",
|
|
50
|
+
dom_id: "#swagger-ui"
|
|
51
|
+
}});
|
|
52
|
+
}};
|
|
53
|
+
</script>
|
|
54
|
+
</body>
|
|
55
|
+
</html>
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Bamboo:
|
|
60
|
+
def __init__(self, title="Bamboo", version="0.1.0"):
|
|
61
|
+
self.title = title
|
|
62
|
+
self.version = version
|
|
63
|
+
self.routes = []
|
|
64
|
+
|
|
65
|
+
def route(self, method, path):
|
|
66
|
+
pattern = self.compile_path(path)
|
|
67
|
+
def decorator(func):
|
|
68
|
+
self.routes.append((method.upper(), path, pattern, func))
|
|
69
|
+
return func
|
|
70
|
+
return decorator
|
|
71
|
+
|
|
72
|
+
def get(self, path):
|
|
73
|
+
return self.route("GET", path)
|
|
74
|
+
|
|
75
|
+
def post(self, path):
|
|
76
|
+
return self.route("POST", path)
|
|
77
|
+
|
|
78
|
+
def put(self, path):
|
|
79
|
+
return self.route("PUT", path)
|
|
80
|
+
|
|
81
|
+
def delete(self, path):
|
|
82
|
+
return self.route("DELETE", path)
|
|
83
|
+
|
|
84
|
+
def compile_path(self, path):
|
|
85
|
+
segments = [s for s in path.split("/") if s != ""]
|
|
86
|
+
regex_parts = []
|
|
87
|
+
for segment in segments:
|
|
88
|
+
if segment.startswith("{") and segment.endswith("}"):
|
|
89
|
+
name = segment[1:-1]
|
|
90
|
+
regex_parts.append(r"(?P<%s>[^/]+)" % name)
|
|
91
|
+
else:
|
|
92
|
+
regex_parts.append(re.escape(segment))
|
|
93
|
+
if regex_parts:
|
|
94
|
+
regex = "^/" + "/".join(regex_parts) + "/?$"
|
|
95
|
+
else:
|
|
96
|
+
regex = "^/?$"
|
|
97
|
+
return re.compile(regex)
|
|
98
|
+
|
|
99
|
+
def openapi(self):
|
|
100
|
+
paths = {}
|
|
101
|
+
for method, path, pattern, handler in self.routes:
|
|
102
|
+
param_names = re.findall(r"{([^}]+)}", path)
|
|
103
|
+
parameters = [
|
|
104
|
+
{
|
|
105
|
+
"name": name,
|
|
106
|
+
"in": "path",
|
|
107
|
+
"required": True,
|
|
108
|
+
"schema": {"type": "string"},
|
|
109
|
+
}
|
|
110
|
+
for name in param_names
|
|
111
|
+
]
|
|
112
|
+
doc = (handler.__doc__ or "").strip()
|
|
113
|
+
summary = doc.splitlines()[0] if doc else handler.__name__
|
|
114
|
+
operation = {
|
|
115
|
+
"summary": summary,
|
|
116
|
+
"responses": {"200": {"description": "Successful Response"}},
|
|
117
|
+
}
|
|
118
|
+
if parameters:
|
|
119
|
+
operation["parameters"] = parameters
|
|
120
|
+
paths.setdefault(path, {})[method.lower()] = operation
|
|
121
|
+
return {
|
|
122
|
+
"openapi": "3.0.0",
|
|
123
|
+
"info": {"title": self.title, "version": self.version},
|
|
124
|
+
"paths": paths,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async def __call__(self, scope, receive, send):
|
|
128
|
+
if scope["type"] != "http":
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
request = Request(scope, receive)
|
|
132
|
+
method = request.method
|
|
133
|
+
path = request.path
|
|
134
|
+
|
|
135
|
+
if method == "GET" and path == "/openapi.json":
|
|
136
|
+
await self.send_json(send, self.openapi())
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
if method == "GET" and path == "/docs":
|
|
140
|
+
await self.send_html(send, SWAGGER_UI_HTML.format(title=self.title))
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
for route_method, route_path, pattern, handler in self.routes:
|
|
144
|
+
if route_method != method:
|
|
145
|
+
continue
|
|
146
|
+
match = pattern.match(path)
|
|
147
|
+
if match:
|
|
148
|
+
params = match.groupdict()
|
|
149
|
+
try:
|
|
150
|
+
result = await handler(request, **params)
|
|
151
|
+
if isinstance(result, Response):
|
|
152
|
+
await self.send_response(send, result)
|
|
153
|
+
else:
|
|
154
|
+
await self.send_json(send, result)
|
|
155
|
+
except Exception:
|
|
156
|
+
await self.send_json(send, {"error": "internal server error"}, 500)
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
await self.send_json(send, {"error": "not found"}, 404)
|
|
160
|
+
|
|
161
|
+
async def send_json(self, send, data, status=200):
|
|
162
|
+
body = json.dumps(data).encode("utf-8")
|
|
163
|
+
await send({
|
|
164
|
+
"type": "http.response.start",
|
|
165
|
+
"status": status,
|
|
166
|
+
"headers": [
|
|
167
|
+
(b"content-type", b"application/json; charset=utf-8"),
|
|
168
|
+
(b"content-length", str(len(body)).encode()),
|
|
169
|
+
],
|
|
170
|
+
})
|
|
171
|
+
await send({"type": "http.response.body", "body": body})
|
|
172
|
+
|
|
173
|
+
async def send_html(self, send, html, status=200):
|
|
174
|
+
body = html.encode("utf-8")
|
|
175
|
+
await send({
|
|
176
|
+
"type": "http.response.start",
|
|
177
|
+
"status": status,
|
|
178
|
+
"headers": [
|
|
179
|
+
(b"content-type", b"text/html; charset=utf-8"),
|
|
180
|
+
(b"content-length", str(len(body)).encode()),
|
|
181
|
+
],
|
|
182
|
+
})
|
|
183
|
+
await send({"type": "http.response.body", "body": body})
|
|
184
|
+
|
|
185
|
+
async def send_response(self, send, response):
|
|
186
|
+
body = json.dumps(response.body).encode("utf-8")
|
|
187
|
+
headers = [
|
|
188
|
+
(b"content-type", b"application/json; charset=utf-8"),
|
|
189
|
+
(b"content-length", str(len(body)).encode()),
|
|
190
|
+
]
|
|
191
|
+
headers.extend(response.headers)
|
|
192
|
+
await send({
|
|
193
|
+
"type": "http.response.start",
|
|
194
|
+
"status": response.status,
|
|
195
|
+
"headers": headers,
|
|
196
|
+
})
|
|
197
|
+
await send({"type": "http.response.body", "body": body})
|