bustapi 0.1.0__cp311-cp311-macosx_11_0_arm64.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.
Potentially problematic release.
This version of bustapi might be problematic. Click here for more details.
- bustapi/__init__.py +96 -0
- bustapi/app.py +552 -0
- bustapi/blueprints.py +500 -0
- bustapi/bustapi_core.cpython-311-darwin.so +0 -0
- bustapi/exceptions.py +438 -0
- bustapi/flask_compat.py +62 -0
- bustapi/helpers.py +370 -0
- bustapi/py.typed +0 -0
- bustapi/request.py +378 -0
- bustapi/response.py +406 -0
- bustapi/testing.py +375 -0
- bustapi-0.1.0.dist-info/METADATA +233 -0
- bustapi-0.1.0.dist-info/RECORD +16 -0
- bustapi-0.1.0.dist-info/WHEEL +4 -0
- bustapi-0.1.0.dist-info/entry_points.txt +2 -0
- bustapi-0.1.0.dist-info/licenses/LICENSE +21 -0
bustapi/request.py
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Request handling for BustAPI - Flask-compatible request object
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from contextvars import ContextVar
|
|
7
|
+
from typing import Any, Dict, Optional, Union
|
|
8
|
+
|
|
9
|
+
from werkzeug.datastructures import ImmutableMultiDict
|
|
10
|
+
|
|
11
|
+
# Thread-local request context
|
|
12
|
+
_request_ctx: ContextVar[Optional["Request"]] = ContextVar("request", default=None)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Request:
|
|
16
|
+
"""
|
|
17
|
+
Flask-compatible request object that wraps the Rust request data.
|
|
18
|
+
|
|
19
|
+
This object provides access to request data including headers, form data,
|
|
20
|
+
JSON data, files, and query parameters in a Flask-compatible way.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, rust_request=None):
|
|
24
|
+
"""
|
|
25
|
+
Initialize request object.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
rust_request: Rust PyRequest object from the backend
|
|
29
|
+
"""
|
|
30
|
+
self._rust_request = rust_request
|
|
31
|
+
self._json_cache = None
|
|
32
|
+
self._form_cache = None
|
|
33
|
+
self._files_cache = None
|
|
34
|
+
self._args_cache = None
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def _from_rust_request(cls, rust_request) -> "Request":
|
|
38
|
+
"""Create Request instance from Rust request object."""
|
|
39
|
+
return cls(rust_request)
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def method(self) -> str:
|
|
43
|
+
"""HTTP method (GET, POST, etc.)"""
|
|
44
|
+
if self._rust_request:
|
|
45
|
+
return self._rust_request.method
|
|
46
|
+
return "GET"
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def url(self) -> str:
|
|
50
|
+
"""Complete URL including query string."""
|
|
51
|
+
if self._rust_request:
|
|
52
|
+
return (
|
|
53
|
+
f"{self.path}?{self.query_string}" if self.query_string else self.path
|
|
54
|
+
)
|
|
55
|
+
return "/"
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def base_url(self) -> str:
|
|
59
|
+
"""Base URL without query string."""
|
|
60
|
+
return self.path
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def path(self) -> str:
|
|
64
|
+
"""URL path component."""
|
|
65
|
+
if self._rust_request:
|
|
66
|
+
return self._rust_request.path
|
|
67
|
+
return "/"
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def query_string(self) -> bytes:
|
|
71
|
+
"""Raw query string as bytes."""
|
|
72
|
+
if self._rust_request:
|
|
73
|
+
return self._rust_request.query_string.encode("utf-8")
|
|
74
|
+
return b""
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def args(self) -> ImmutableMultiDict:
|
|
78
|
+
"""Query parameters as ImmutableMultiDict."""
|
|
79
|
+
if self._args_cache is None:
|
|
80
|
+
if self._rust_request:
|
|
81
|
+
args_dict = self._rust_request.args
|
|
82
|
+
# Convert to ImmutableMultiDict for Flask compatibility
|
|
83
|
+
items = [(k, v) for k, v in args_dict.items()]
|
|
84
|
+
else:
|
|
85
|
+
items = []
|
|
86
|
+
self._args_cache = ImmutableMultiDict(items)
|
|
87
|
+
return self._args_cache
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def form(self) -> ImmutableMultiDict:
|
|
91
|
+
"""Form data as ImmutableMultiDict."""
|
|
92
|
+
if self._form_cache is None:
|
|
93
|
+
if self._rust_request:
|
|
94
|
+
form_dict = self._rust_request.form()
|
|
95
|
+
# Convert to ImmutableMultiDict for Flask compatibility
|
|
96
|
+
items = [(k, v) for k, v in form_dict.items()]
|
|
97
|
+
else:
|
|
98
|
+
items = []
|
|
99
|
+
self._form_cache = ImmutableMultiDict(items)
|
|
100
|
+
return self._form_cache
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def files(self) -> ImmutableMultiDict:
|
|
104
|
+
"""Uploaded files as ImmutableMultiDict."""
|
|
105
|
+
if self._files_cache is None:
|
|
106
|
+
# TODO: Implement file upload handling in Rust backend
|
|
107
|
+
# For now, return empty dict
|
|
108
|
+
self._files_cache = ImmutableMultiDict([])
|
|
109
|
+
return self._files_cache
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def values(self) -> ImmutableMultiDict:
|
|
113
|
+
"""Combined args and form data."""
|
|
114
|
+
combined = []
|
|
115
|
+
combined.extend(self.args.items(multi=True))
|
|
116
|
+
combined.extend(self.form.items(multi=True))
|
|
117
|
+
return ImmutableMultiDict(combined)
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def json(self) -> Optional[Any]:
|
|
121
|
+
"""Request body parsed as JSON."""
|
|
122
|
+
if self._json_cache is None:
|
|
123
|
+
if self._rust_request:
|
|
124
|
+
try:
|
|
125
|
+
self._json_cache = self._rust_request.json()
|
|
126
|
+
except Exception:
|
|
127
|
+
self._json_cache = None
|
|
128
|
+
else:
|
|
129
|
+
self._json_cache = None
|
|
130
|
+
return self._json_cache
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def data(self) -> bytes:
|
|
134
|
+
"""Raw request body as bytes."""
|
|
135
|
+
if self._rust_request:
|
|
136
|
+
return self._rust_request.get_data()
|
|
137
|
+
return b""
|
|
138
|
+
|
|
139
|
+
def get_data(
|
|
140
|
+
self, cache: bool = True, as_text: bool = False, parse_form_data: bool = False
|
|
141
|
+
) -> Union[bytes, str]:
|
|
142
|
+
"""
|
|
143
|
+
Get request body data.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
cache: Whether to cache the result
|
|
147
|
+
as_text: Return as text instead of bytes
|
|
148
|
+
parse_form_data: Parse form data
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Request body as bytes or text
|
|
152
|
+
"""
|
|
153
|
+
data = self.data
|
|
154
|
+
if as_text:
|
|
155
|
+
return data.decode("utf-8", errors="replace")
|
|
156
|
+
return data
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def headers(self) -> "EnvironHeaders":
|
|
160
|
+
"""Request headers."""
|
|
161
|
+
if self._rust_request:
|
|
162
|
+
headers_dict = self._rust_request.headers
|
|
163
|
+
return EnvironHeaders(headers_dict)
|
|
164
|
+
return EnvironHeaders({})
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def cookies(self) -> Dict[str, str]:
|
|
168
|
+
"""Request cookies."""
|
|
169
|
+
if self._rust_request:
|
|
170
|
+
return self._rust_request.cookies()
|
|
171
|
+
return {}
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def environ(self) -> Dict[str, Any]:
|
|
175
|
+
"""WSGI environ dictionary (placeholder)."""
|
|
176
|
+
# TODO: Implement WSGI environ compatibility
|
|
177
|
+
return {}
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def remote_addr(self) -> Optional[str]:
|
|
181
|
+
"""Client IP address."""
|
|
182
|
+
# Try common headers for reverse proxy setups
|
|
183
|
+
forwarded_for = self.headers.get("X-Forwarded-For")
|
|
184
|
+
if forwarded_for:
|
|
185
|
+
# Get first IP from comma-separated list
|
|
186
|
+
return forwarded_for.split(",")[0].strip()
|
|
187
|
+
|
|
188
|
+
real_ip = self.headers.get("X-Real-IP")
|
|
189
|
+
if real_ip:
|
|
190
|
+
return real_ip
|
|
191
|
+
|
|
192
|
+
# TODO: Get from connection info in Rust backend
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def user_agent(self) -> Optional[str]:
|
|
197
|
+
"""User agent string."""
|
|
198
|
+
return self.headers.get("User-Agent")
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def referrer(self) -> Optional[str]:
|
|
202
|
+
"""HTTP referrer."""
|
|
203
|
+
return self.headers.get("Referer")
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def is_secure(self) -> bool:
|
|
207
|
+
"""Whether request was made over HTTPS."""
|
|
208
|
+
if self._rust_request:
|
|
209
|
+
return (
|
|
210
|
+
self._rust_request.is_secure
|
|
211
|
+
if hasattr(self._rust_request, "is_secure")
|
|
212
|
+
else False
|
|
213
|
+
)
|
|
214
|
+
return False
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def is_json(self) -> bool:
|
|
218
|
+
"""Whether request has JSON content type."""
|
|
219
|
+
if self._rust_request:
|
|
220
|
+
return self._rust_request.is_json()
|
|
221
|
+
return False
|
|
222
|
+
|
|
223
|
+
def get_json(
|
|
224
|
+
self, force: bool = False, silent: bool = False, cache: bool = True
|
|
225
|
+
) -> Optional[Any]:
|
|
226
|
+
"""
|
|
227
|
+
Parse request body as JSON.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
force: Force parsing even without JSON content type
|
|
231
|
+
silent: Don't raise exception on parse error
|
|
232
|
+
cache: Cache the result
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
Parsed JSON data or None
|
|
236
|
+
"""
|
|
237
|
+
if not force and not self.is_json:
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
if cache and self._json_cache is not None:
|
|
241
|
+
return self._json_cache
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
data = self.get_data(as_text=True)
|
|
245
|
+
if not data:
|
|
246
|
+
return None
|
|
247
|
+
result = json.loads(data)
|
|
248
|
+
if cache:
|
|
249
|
+
self._json_cache = result
|
|
250
|
+
return result
|
|
251
|
+
except (ValueError, TypeError):
|
|
252
|
+
if not silent:
|
|
253
|
+
raise
|
|
254
|
+
return None
|
|
255
|
+
|
|
256
|
+
# Flask compatibility methods
|
|
257
|
+
def wants_json(self) -> bool:
|
|
258
|
+
"""Check if client prefers JSON response."""
|
|
259
|
+
accept = self.headers.get("Accept", "")
|
|
260
|
+
return "application/json" in accept and "text/html" not in accept
|
|
261
|
+
|
|
262
|
+
def is_xhr(self) -> bool:
|
|
263
|
+
"""Check if request was made via XMLHttpRequest."""
|
|
264
|
+
return self.headers.get("X-Requested-With", "").lower() == "xmlhttprequest"
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class EnvironHeaders:
|
|
268
|
+
"""
|
|
269
|
+
Flask-compatible headers object.
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
def __init__(self, headers_dict: Dict[str, str]):
|
|
273
|
+
self._headers = headers_dict
|
|
274
|
+
|
|
275
|
+
def get(
|
|
276
|
+
self,
|
|
277
|
+
key: str,
|
|
278
|
+
default: Optional[str] = None,
|
|
279
|
+
type_func: Optional[callable] = None,
|
|
280
|
+
) -> Any:
|
|
281
|
+
"""
|
|
282
|
+
Get header value.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
key: Header name
|
|
286
|
+
default: Default value if header not found
|
|
287
|
+
type_func: Function to convert value
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
Header value, converted if type_func provided
|
|
291
|
+
"""
|
|
292
|
+
# Case-insensitive header lookup
|
|
293
|
+
value = None
|
|
294
|
+
key_lower = key.lower()
|
|
295
|
+
for header_key, header_value in self._headers.items():
|
|
296
|
+
if header_key.lower() == key_lower:
|
|
297
|
+
value = header_value
|
|
298
|
+
break
|
|
299
|
+
|
|
300
|
+
if value is None:
|
|
301
|
+
return default
|
|
302
|
+
|
|
303
|
+
if type_func:
|
|
304
|
+
try:
|
|
305
|
+
return type_func(value)
|
|
306
|
+
except (ValueError, TypeError):
|
|
307
|
+
return default
|
|
308
|
+
|
|
309
|
+
return value
|
|
310
|
+
|
|
311
|
+
def getlist(self, key: str) -> list:
|
|
312
|
+
"""Get list of values for header (for multiple headers with same name)."""
|
|
313
|
+
# Simplified implementation - assumes single value per header
|
|
314
|
+
value = self.get(key)
|
|
315
|
+
return [value] if value is not None else []
|
|
316
|
+
|
|
317
|
+
def __getitem__(self, key: str) -> str:
|
|
318
|
+
"""Get header value, raise KeyError if not found."""
|
|
319
|
+
value = self.get(key)
|
|
320
|
+
if value is None:
|
|
321
|
+
raise KeyError(key)
|
|
322
|
+
return value
|
|
323
|
+
|
|
324
|
+
def __contains__(self, key: str) -> bool:
|
|
325
|
+
"""Check if header exists."""
|
|
326
|
+
return self.get(key) is not None
|
|
327
|
+
|
|
328
|
+
def __iter__(self):
|
|
329
|
+
"""Iterate over header names."""
|
|
330
|
+
return iter(self._headers.keys())
|
|
331
|
+
|
|
332
|
+
def items(self):
|
|
333
|
+
"""Iterate over header (name, value) pairs."""
|
|
334
|
+
return self._headers.items()
|
|
335
|
+
|
|
336
|
+
def keys(self):
|
|
337
|
+
"""Get header names."""
|
|
338
|
+
return self._headers.keys()
|
|
339
|
+
|
|
340
|
+
def values(self):
|
|
341
|
+
"""Get header values."""
|
|
342
|
+
return self._headers.values()
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
# Global request proxy object for Flask compatibility
|
|
346
|
+
class _RequestProxy:
|
|
347
|
+
"""Proxy object that provides access to the current request."""
|
|
348
|
+
|
|
349
|
+
def __getattr__(self, name: str) -> Any:
|
|
350
|
+
req = _request_ctx.get()
|
|
351
|
+
if req is None:
|
|
352
|
+
raise RuntimeError(
|
|
353
|
+
"Working outside of request context. This typically happens when "
|
|
354
|
+
"you are trying to access the request object from outside a view "
|
|
355
|
+
"function or from a thread that wasn't started by Flask."
|
|
356
|
+
)
|
|
357
|
+
return getattr(req, name)
|
|
358
|
+
|
|
359
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
360
|
+
req = _request_ctx.get()
|
|
361
|
+
if req is None:
|
|
362
|
+
raise RuntimeError("Working outside of request context")
|
|
363
|
+
setattr(req, name, value)
|
|
364
|
+
|
|
365
|
+
def __repr__(self) -> str:
|
|
366
|
+
req = _request_ctx.get()
|
|
367
|
+
if req is None:
|
|
368
|
+
return "<RequestProxy: no request context>"
|
|
369
|
+
return f"<RequestProxy: {req.method} {req.path}>"
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
# Global request object (Flask-compatible)
|
|
373
|
+
request = _RequestProxy()
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def has_request_context() -> bool:
|
|
377
|
+
"""Check if we're currently in a request context."""
|
|
378
|
+
return _request_ctx.get() is not None
|