bustapi 0.1.0__cp311-cp311-win_amd64.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/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