fixturify 0.1.9__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.
Files changed (71) hide show
  1. fixturify/__init__.py +21 -0
  2. fixturify/_utils/__init__.py +7 -0
  3. fixturify/_utils/_constants.py +10 -0
  4. fixturify/_utils/_fixture_discovery.py +165 -0
  5. fixturify/_utils/_path_resolver.py +135 -0
  6. fixturify/http_d/__init__.py +80 -0
  7. fixturify/http_d/_config.py +214 -0
  8. fixturify/http_d/_decorator.py +267 -0
  9. fixturify/http_d/_exceptions.py +153 -0
  10. fixturify/http_d/_fixture_discovery.py +33 -0
  11. fixturify/http_d/_matcher.py +372 -0
  12. fixturify/http_d/_mock_context.py +154 -0
  13. fixturify/http_d/_models.py +205 -0
  14. fixturify/http_d/_patcher.py +524 -0
  15. fixturify/http_d/_player.py +222 -0
  16. fixturify/http_d/_recorder.py +1350 -0
  17. fixturify/http_d/_stubs/__init__.py +8 -0
  18. fixturify/http_d/_stubs/_aiohttp.py +220 -0
  19. fixturify/http_d/_stubs/_connection.py +478 -0
  20. fixturify/http_d/_stubs/_httpcore.py +269 -0
  21. fixturify/http_d/_stubs/_tornado.py +95 -0
  22. fixturify/http_d/_utils.py +194 -0
  23. fixturify/json_assert/__init__.py +13 -0
  24. fixturify/json_assert/_actual_saver.py +67 -0
  25. fixturify/json_assert/_assert.py +173 -0
  26. fixturify/json_assert/_comparator.py +183 -0
  27. fixturify/json_assert/_diff_formatter.py +265 -0
  28. fixturify/json_assert/_normalizer.py +83 -0
  29. fixturify/object_mapper/__init__.py +5 -0
  30. fixturify/object_mapper/_deserializers/__init__.py +19 -0
  31. fixturify/object_mapper/_deserializers/_base.py +186 -0
  32. fixturify/object_mapper/_deserializers/_dataclass.py +52 -0
  33. fixturify/object_mapper/_deserializers/_plain.py +55 -0
  34. fixturify/object_mapper/_deserializers/_pydantic_v1.py +38 -0
  35. fixturify/object_mapper/_deserializers/_pydantic_v2.py +41 -0
  36. fixturify/object_mapper/_deserializers/_sqlalchemy.py +72 -0
  37. fixturify/object_mapper/_deserializers/_sqlmodel.py +43 -0
  38. fixturify/object_mapper/_detectors/__init__.py +5 -0
  39. fixturify/object_mapper/_detectors/_type_detector.py +186 -0
  40. fixturify/object_mapper/_serializers/__init__.py +19 -0
  41. fixturify/object_mapper/_serializers/_base.py +260 -0
  42. fixturify/object_mapper/_serializers/_dataclass.py +55 -0
  43. fixturify/object_mapper/_serializers/_plain.py +49 -0
  44. fixturify/object_mapper/_serializers/_pydantic_v1.py +49 -0
  45. fixturify/object_mapper/_serializers/_pydantic_v2.py +49 -0
  46. fixturify/object_mapper/_serializers/_sqlalchemy.py +70 -0
  47. fixturify/object_mapper/_serializers/_sqlmodel.py +54 -0
  48. fixturify/object_mapper/mapper.py +256 -0
  49. fixturify/read_d/__init__.py +5 -0
  50. fixturify/read_d/_decorator.py +193 -0
  51. fixturify/read_d/_fixture_loader.py +88 -0
  52. fixturify/sql_d/__init__.py +7 -0
  53. fixturify/sql_d/_config.py +30 -0
  54. fixturify/sql_d/_decorator.py +373 -0
  55. fixturify/sql_d/_driver_registry.py +133 -0
  56. fixturify/sql_d/_executor.py +82 -0
  57. fixturify/sql_d/_fixture_discovery.py +55 -0
  58. fixturify/sql_d/_phase.py +10 -0
  59. fixturify/sql_d/_strategies/__init__.py +11 -0
  60. fixturify/sql_d/_strategies/_aiomysql.py +63 -0
  61. fixturify/sql_d/_strategies/_aiosqlite.py +29 -0
  62. fixturify/sql_d/_strategies/_asyncpg.py +34 -0
  63. fixturify/sql_d/_strategies/_base.py +118 -0
  64. fixturify/sql_d/_strategies/_mysql.py +70 -0
  65. fixturify/sql_d/_strategies/_psycopg.py +35 -0
  66. fixturify/sql_d/_strategies/_psycopg2.py +40 -0
  67. fixturify/sql_d/_strategies/_registry.py +109 -0
  68. fixturify/sql_d/_strategies/_sqlite.py +33 -0
  69. fixturify-0.1.9.dist-info/METADATA +122 -0
  70. fixturify-0.1.9.dist-info/RECORD +71 -0
  71. fixturify-0.1.9.dist-info/WHEEL +4 -0
@@ -0,0 +1,1350 @@
1
+ """Recording logic for capturing HTTP interactions."""
2
+
3
+ import base64
4
+ import hashlib
5
+ import json
6
+ import mimetypes
7
+ from pathlib import Path
8
+ from typing import Any, Dict, List, Optional, Tuple, Union
9
+ from urllib.parse import parse_qs, urlparse
10
+
11
+ from fixturify._utils import ENCODING
12
+
13
+ from fixturify.http_d._models import HttpMapping, HttpMappings, HttpRequest, HttpResponse
14
+
15
+
16
+ # Content types that should be stored as separate binary files
17
+ BINARY_CONTENT_TYPES = (
18
+ "image/",
19
+ "audio/",
20
+ "video/",
21
+ "application/octet-stream",
22
+ "application/pdf",
23
+ "application/zip",
24
+ "application/gzip",
25
+ "application/x-tar",
26
+ "application/x-rar",
27
+ "application/x-7z-compressed",
28
+ "application/vnd.ms-excel",
29
+ "application/vnd.openxmlformats",
30
+ "application/msword",
31
+ "font/",
32
+ )
33
+
34
+
35
+ def _normalize_content_type(content_type: Union[str, bytes, None]) -> str:
36
+ """Normalize content type to string."""
37
+ if content_type is None:
38
+ return ""
39
+ if isinstance(content_type, bytes):
40
+ return content_type.decode("utf-8", errors="ignore")
41
+ return content_type
42
+
43
+
44
+ def _is_text_content_type(content_type: Union[str, bytes]) -> bool:
45
+ """Check if content type indicates text content."""
46
+ text_types = (
47
+ "text/",
48
+ "application/json",
49
+ "application/xml",
50
+ "application/javascript",
51
+ "application/x-www-form-urlencoded",
52
+ )
53
+ content_str = _normalize_content_type(content_type)
54
+ return any(t in content_str.lower() for t in text_types)
55
+
56
+
57
+ def _is_json_content_type(content_type: Union[str, bytes]) -> bool:
58
+ """Check if content type indicates JSON content."""
59
+ content_str = _normalize_content_type(content_type)
60
+ return "application/json" in content_str.lower()
61
+
62
+
63
+ def _is_binary_content_type(content_type: Union[str, bytes]) -> bool:
64
+ """Check if content type indicates binary content that should be stored as file."""
65
+ content_str = _normalize_content_type(content_type)
66
+ content_lower = content_str.lower()
67
+ return any(t in content_lower for t in BINARY_CONTENT_TYPES)
68
+
69
+
70
+ def _try_parse_json(content: str) -> Any:
71
+ """Try to parse string as JSON, return parsed or original string."""
72
+ try:
73
+ return json.loads(content)
74
+ except (json.JSONDecodeError, TypeError):
75
+ return content
76
+
77
+
78
+ def _get_file_extension(content_type: Union[str, bytes]) -> str:
79
+ """Get appropriate file extension for a content type."""
80
+ # Common mappings
81
+ extension_map = {
82
+ "application/json": ".json",
83
+ "application/pdf": ".pdf",
84
+ "application/zip": ".zip",
85
+ "application/gzip": ".gz",
86
+ "application/xml": ".xml",
87
+ "text/html": ".html",
88
+ "text/plain": ".txt",
89
+ "text/css": ".css",
90
+ "text/javascript": ".js",
91
+ "image/jpeg": ".jpg",
92
+ "image/png": ".png",
93
+ "image/gif": ".gif",
94
+ "image/webp": ".webp",
95
+ "image/svg+xml": ".svg",
96
+ "audio/mpeg": ".mp3",
97
+ "audio/wav": ".wav",
98
+ "video/mp4": ".mp4",
99
+ "video/webm": ".webm",
100
+ }
101
+
102
+ # Normalize content_type to string
103
+ content_str = _normalize_content_type(content_type)
104
+
105
+ # Check our map first
106
+ content_lower = content_str.lower().split(";")[0].strip()
107
+ if content_lower in extension_map:
108
+ return extension_map[content_lower]
109
+
110
+ # Fall back to mimetypes
111
+ ext = mimetypes.guess_extension(content_lower)
112
+ if ext:
113
+ return ext
114
+
115
+ # Default to .bin for unknown binary
116
+ return ".bin"
117
+
118
+
119
+ def _serialize_body(
120
+ body: Optional[Union[bytes, str]],
121
+ content_type: Union[str, bytes] = "",
122
+ ) -> Tuple[Any, Optional[str], bool]:
123
+ """
124
+ Serialize body for storage.
125
+
126
+ For JSON content: returns parsed JSON object (not stringified)
127
+ For text content: returns string
128
+ For binary content: returns raw bytes and flag to save as file
129
+
130
+ Args:
131
+ body: Request or response body (bytes, string, or BytesIO)
132
+ content_type: Content-Type header value (str or bytes)
133
+
134
+ Returns:
135
+ Tuple of (body_value, encoding, is_binary_file)
136
+ - For text/JSON: (value, None, False)
137
+ - For binary: (bytes, None, True)
138
+ """
139
+ if body is None:
140
+ return None, None, False
141
+
142
+ # Handle BytesIO and other file-like objects
143
+ if hasattr(body, "read"):
144
+ file_obj = body
145
+ body = file_obj.read()
146
+ # Reset position if possible for re-reading
147
+ if hasattr(file_obj, "seek"):
148
+ file_obj.seek(0)
149
+
150
+ # Normalize content_type to string
151
+ content_type = _normalize_content_type(content_type)
152
+
153
+ # Check if this should be stored as a binary file
154
+ if _is_binary_content_type(content_type):
155
+ if isinstance(body, bytes):
156
+ return body, None, True
157
+ elif isinstance(body, str):
158
+ return body.encode("utf-8"), None, True
159
+ else:
160
+ # Unknown type - try to convert
161
+ return str(body).encode("utf-8"), None, True
162
+
163
+ # Convert bytes to string if needed
164
+ if isinstance(body, bytes):
165
+ if _is_text_content_type(content_type) or not content_type:
166
+ try:
167
+ body_str = body.decode("utf-8")
168
+ except UnicodeDecodeError:
169
+ # Binary content - return as bytes for file storage
170
+ return body, None, True
171
+ else:
172
+ # Unknown content type with bytes - try text first
173
+ try:
174
+ body_str = body.decode("utf-8")
175
+ except UnicodeDecodeError:
176
+ return body, None, True
177
+ else:
178
+ body_str = body
179
+
180
+ # Empty body
181
+ if not body_str:
182
+ return None, None, False
183
+
184
+ # For JSON content, parse and store as native JSON
185
+ if _is_json_content_type(content_type):
186
+ return _try_parse_json(body_str), None, False
187
+
188
+ # Return as string for non-JSON text content
189
+ return body_str, None, False
190
+
191
+
192
+ class HttpRecorder:
193
+ """
194
+ Records HTTP interactions for later playback.
195
+
196
+ Captures request/response pairs and saves them to a JSON file
197
+ in WireMock-compatible format. Binary responses are stored as
198
+ separate files in a __files subdirectory.
199
+ """
200
+
201
+ # Directory name for binary response files (WireMock convention)
202
+ FILES_DIR = "__files"
203
+
204
+ def __init__(
205
+ self,
206
+ file_path: Path,
207
+ exclude_request_headers: Optional[set] = None,
208
+ exclude_response_headers: Optional[set] = None,
209
+ redact_request_body: Optional[list[str]] = None,
210
+ redact_response_body: Optional[list[str]] = None,
211
+ ):
212
+ """
213
+ Initialize the recorder.
214
+
215
+ Args:
216
+ file_path: Path where recordings will be saved.
217
+ exclude_request_headers: Set of header names (lowercase) to exclude
218
+ from request recordings.
219
+ exclude_response_headers: Set of header names (lowercase) to exclude
220
+ from response recordings.
221
+ """
222
+ self.file_path = file_path
223
+ self.mappings: HttpMappings = HttpMappings()
224
+ # Track binary files to save: list of (filename, bytes)
225
+ self._binary_files: List[Tuple[str, bytes]] = []
226
+ # Counter for generating unique filenames
227
+ self._file_counter = 0
228
+ # Headers to exclude from recordings
229
+ self._exclude_request_headers = exclude_request_headers or set()
230
+ self._exclude_response_headers = exclude_response_headers or set()
231
+ self._redact_request_body = {f for f in (redact_request_body or []) if f}
232
+ self._redact_response_body = {f for f in (redact_response_body or []) if f}
233
+
234
+ def _redact_json_fields(self, value: Any) -> Any:
235
+ """Redact configured JSON fields in dict/list structures."""
236
+ if not self._redact_request_body:
237
+ return value
238
+ if isinstance(value, dict):
239
+ redacted = {}
240
+ for key, val in value.items():
241
+ if key in self._redact_request_body:
242
+ redacted[key] = "*******"
243
+ else:
244
+ redacted[key] = self._redact_json_fields(val)
245
+ return redacted
246
+ if isinstance(value, list):
247
+ return [self._redact_json_fields(item) for item in value]
248
+ return value
249
+
250
+ def _redact_json_fields_response(self, value: Any) -> Any:
251
+ """Redact configured JSON fields in dict/list structures (response)."""
252
+ if not self._redact_response_body:
253
+ return value
254
+ if isinstance(value, dict):
255
+ redacted = {}
256
+ for key, val in value.items():
257
+ if key in self._redact_response_body:
258
+ redacted[key] = "*******"
259
+ else:
260
+ redacted[key] = self._redact_json_fields_response(val)
261
+ return redacted
262
+ if isinstance(value, list):
263
+ return [self._redact_json_fields_response(item) for item in value]
264
+ return value
265
+
266
+ def record(
267
+ self,
268
+ request: HttpRequest,
269
+ response: HttpResponse,
270
+ ) -> None:
271
+ """
272
+ Record a request/response pair.
273
+
274
+ Args:
275
+ request: The HTTP request.
276
+ response: The HTTP response.
277
+ """
278
+ mapping = HttpMapping(request=request, response=response)
279
+ self.mappings.add(mapping)
280
+
281
+ def save(self) -> None:
282
+ """
283
+ Save all recorded mappings to the JSON file.
284
+
285
+ Creates parent directories if they don't exist.
286
+ Binary response bodies are saved to a __files subdirectory.
287
+ """
288
+ # Ensure parent directory exists
289
+ self.file_path.parent.mkdir(parents=True, exist_ok=True)
290
+
291
+ # Convert to dict with native JSON bodies (and save binary files)
292
+ data = self._to_dict_with_json_bodies()
293
+
294
+ # Write JSON file
295
+ with open(self.file_path, "w", encoding=ENCODING) as f:
296
+ json.dump(data, f, indent=2, ensure_ascii=False)
297
+
298
+ # Save binary files
299
+ if self._binary_files:
300
+ files_dir = self.file_path.parent / self.FILES_DIR
301
+ files_dir.mkdir(exist_ok=True)
302
+
303
+ for filename, content in self._binary_files:
304
+ file_path = files_dir / filename
305
+ with open(file_path, "wb") as f:
306
+ f.write(content)
307
+
308
+ def _generate_filename(self, content_type: str, content: bytes) -> str:
309
+ """
310
+ Generate a unique filename for binary content.
311
+
312
+ Uses a hash of the content to enable deduplication.
313
+
314
+ Args:
315
+ content_type: Content-Type header
316
+ content: Binary content
317
+
318
+ Returns:
319
+ Filename like "response_0_a1b2c3d4.png"
320
+ """
321
+ self._file_counter += 1
322
+ ext = _get_file_extension(content_type)
323
+ # Use content hash for deduplication and uniqueness
324
+ content_hash = hashlib.sha256(content).hexdigest()[:8]
325
+ return f"response_{self._file_counter}_{content_hash}{ext}"
326
+
327
+ def _filter_headers(
328
+ self,
329
+ headers: Dict[str, str],
330
+ exclude_set: set,
331
+ ) -> Dict[str, str]:
332
+ """
333
+ Filter headers, removing excluded ones.
334
+
335
+ Args:
336
+ headers: Original headers dict.
337
+ exclude_set: Set of lowercase header names to exclude.
338
+
339
+ Returns:
340
+ Filtered headers dict.
341
+ """
342
+ if not exclude_set:
343
+ return headers
344
+ return {k: v for k, v in headers.items() if k.lower() not in exclude_set}
345
+
346
+ def _to_dict_with_json_bodies(self) -> Dict[str, Any]:
347
+ """
348
+ Convert mappings to dict, storing bodies as native JSON.
349
+
350
+ Binary response bodies are saved to files and referenced via bodyFileName.
351
+ Excluded headers are filtered out from the recording.
352
+
353
+ Returns:
354
+ Dict ready for JSON serialization.
355
+ """
356
+ mappings_list = []
357
+ self._binary_files.clear()
358
+ self._file_counter = 0
359
+
360
+ for mapping in self.mappings:
361
+ # Filter headers
362
+ req_headers = self._filter_headers(
363
+ mapping.request.headers,
364
+ self._exclude_request_headers,
365
+ )
366
+ resp_headers = self._filter_headers(
367
+ mapping.response.headers,
368
+ self._exclude_response_headers,
369
+ )
370
+
371
+ # Get content types for body serialization
372
+ req_content_type = req_headers.get(
373
+ "Content-Type",
374
+ req_headers.get("content-type", ""),
375
+ )
376
+ resp_content_type = resp_headers.get(
377
+ "Content-Type",
378
+ resp_headers.get("content-type", ""),
379
+ )
380
+
381
+ # Serialize request body
382
+ req_body = mapping.request.body
383
+ if isinstance(req_body, str) and _is_json_content_type(req_content_type):
384
+ req_body = _try_parse_json(req_body)
385
+
386
+ # Build request dict
387
+ if isinstance(req_body, (dict, list)):
388
+ req_body = self._redact_json_fields(req_body)
389
+ request_dict: Dict[str, Any] = {
390
+ "method": mapping.request.method.upper(),
391
+ "url": mapping.request.url,
392
+ "headers": req_headers,
393
+ "body": req_body,
394
+ }
395
+ if mapping.request.bodyEncoding:
396
+ request_dict["bodyEncoding"] = mapping.request.bodyEncoding
397
+
398
+ # Build response dict
399
+ response_dict: Dict[str, Any] = {
400
+ "status": mapping.response.status,
401
+ "headers": resp_headers,
402
+ }
403
+
404
+ # Check if response has a binary file reference
405
+ if mapping.response.bodyFileName:
406
+ response_dict["bodyFileName"] = mapping.response.bodyFileName
407
+ elif mapping.response._body_bytes is not None:
408
+ # Binary content that needs to be saved as file
409
+ filename = self._generate_filename(
410
+ resp_content_type,
411
+ mapping.response._body_bytes,
412
+ )
413
+ self._binary_files.append((filename, mapping.response._body_bytes))
414
+ response_dict["bodyFileName"] = filename
415
+ elif _is_binary_content_type(resp_content_type) and mapping.response.body:
416
+ # Response body is base64-encoded binary - decode and save as file
417
+ try:
418
+ content = base64.b64decode(mapping.response.body)
419
+ filename = self._generate_filename(resp_content_type, content)
420
+ self._binary_files.append((filename, content))
421
+ response_dict["bodyFileName"] = filename
422
+ except Exception:
423
+ # Fall back to inline body
424
+ resp_body = mapping.response.body
425
+ if _is_json_content_type(resp_content_type):
426
+ resp_body = _try_parse_json(resp_body)
427
+ response_dict["body"] = resp_body
428
+ else:
429
+ # Text/JSON content - store inline
430
+ resp_body = mapping.response.body
431
+ if isinstance(resp_body, str) and _is_json_content_type(
432
+ resp_content_type
433
+ ):
434
+ resp_body = _try_parse_json(resp_body)
435
+ if isinstance(resp_body, (dict, list)):
436
+ resp_body = self._redact_json_fields_response(resp_body)
437
+ response_dict["body"] = resp_body if resp_body else None
438
+
439
+ if mapping.response.bodyEncoding and "bodyFileName" not in response_dict:
440
+ response_dict["bodyEncoding"] = mapping.response.bodyEncoding
441
+
442
+ mappings_list.append(
443
+ {
444
+ "request": request_dict,
445
+ "response": response_dict,
446
+ }
447
+ )
448
+
449
+ return {"mappings": mappings_list}
450
+
451
+ def __len__(self) -> int:
452
+ """Return number of recorded mappings."""
453
+ return len(self.mappings)
454
+
455
+
456
+ def create_request_from_httpx(
457
+ request: Any, # httpx.Request
458
+ ) -> HttpRequest:
459
+ """
460
+ Create HttpRequest from an httpx Request object.
461
+
462
+ Args:
463
+ request: httpx.Request object
464
+
465
+ Returns:
466
+ HttpRequest model
467
+ """
468
+ # Get URL and extract query parameters
469
+ url_str = str(request.url)
470
+ parsed = urlparse(url_str)
471
+
472
+ # Extract query parameters
473
+ query_params: Dict[str, str] = {}
474
+ if parsed.query:
475
+ for key, values in parse_qs(parsed.query).items():
476
+ query_params[key] = values[0] if values else ""
477
+
478
+ # Get headers as dict
479
+ headers = dict(request.headers)
480
+
481
+ # Get body
482
+ content_type = headers.get("content-type", headers.get("Content-Type", ""))
483
+ body_bytes = request.content if hasattr(request, "content") else None
484
+ body, body_encoding, _ = _serialize_body(body_bytes, content_type)
485
+
486
+ # Convert body back to string for storage in model (will be parsed when saving)
487
+ body_str = None
488
+ if body is not None:
489
+ if body_encoding == "base64":
490
+ body_str = body
491
+ elif isinstance(body, str):
492
+ body_str = body
493
+ else:
494
+ body_str = json.dumps(body, ensure_ascii=False)
495
+
496
+ return HttpRequest(
497
+ method=request.method,
498
+ url=url_str,
499
+ headers=headers,
500
+ queryParameters=query_params,
501
+ body=body_str,
502
+ bodyEncoding=body_encoding,
503
+ )
504
+
505
+
506
+ def create_response_from_httpx(
507
+ response: Any, # httpx.Response
508
+ ) -> HttpResponse:
509
+ """
510
+ Create HttpResponse from an httpx Response object.
511
+
512
+ Args:
513
+ response: httpx.Response object
514
+
515
+ Returns:
516
+ HttpResponse model
517
+ """
518
+ headers = dict(response.headers)
519
+ content_type = headers.get("content-type", headers.get("Content-Type", ""))
520
+
521
+ # Get response body
522
+ body_bytes = response.content
523
+ body, body_encoding, is_binary = _serialize_body(body_bytes, content_type)
524
+
525
+ if is_binary:
526
+ # Binary content - store bytes for file saving
527
+ resp = HttpResponse(
528
+ status=response.status_code,
529
+ headers=headers,
530
+ body=None,
531
+ )
532
+ resp._body_bytes = body if isinstance(body, bytes) else body_bytes
533
+ return resp
534
+
535
+ # Convert body to string for storage in model
536
+ body_str = ""
537
+ if body is not None:
538
+ if isinstance(body, str):
539
+ body_str = body
540
+ else:
541
+ body_str = json.dumps(body, ensure_ascii=False)
542
+
543
+ return HttpResponse(
544
+ status=response.status_code,
545
+ headers=headers,
546
+ body=body_str,
547
+ bodyEncoding=body_encoding,
548
+ )
549
+
550
+
551
+ def create_request_from_requests(
552
+ prepared_request: Any, # requests.PreparedRequest
553
+ ) -> HttpRequest:
554
+ """
555
+ Create HttpRequest from a requests PreparedRequest object.
556
+
557
+ Args:
558
+ prepared_request: requests.PreparedRequest object
559
+
560
+ Returns:
561
+ HttpRequest model
562
+ """
563
+ url_str = prepared_request.url or ""
564
+ parsed = urlparse(url_str)
565
+
566
+ # Extract query parameters
567
+ query_params: Dict[str, str] = {}
568
+ if parsed.query:
569
+ for key, values in parse_qs(parsed.query).items():
570
+ query_params[key] = values[0] if values else ""
571
+
572
+ # Get headers as dict
573
+ headers = dict(prepared_request.headers) if prepared_request.headers else {}
574
+
575
+ # Get body
576
+ content_type = headers.get("content-type", headers.get("Content-Type", ""))
577
+ body_raw = prepared_request.body
578
+ body, body_encoding, _ = _serialize_body(body_raw, content_type)
579
+
580
+ # Convert body back to string for storage
581
+ body_str = None
582
+ if body is not None:
583
+ if body_encoding == "base64":
584
+ body_str = body
585
+ elif isinstance(body, str):
586
+ body_str = body
587
+ else:
588
+ body_str = json.dumps(body, ensure_ascii=False)
589
+
590
+ return HttpRequest(
591
+ method=prepared_request.method or "GET",
592
+ url=url_str,
593
+ headers=headers,
594
+ queryParameters=query_params,
595
+ body=body_str,
596
+ bodyEncoding=body_encoding,
597
+ )
598
+
599
+
600
+ def create_response_from_requests(
601
+ response: Any, # requests.Response
602
+ ) -> HttpResponse:
603
+ """
604
+ Create HttpResponse from a requests Response object.
605
+
606
+ Args:
607
+ response: requests.Response object
608
+
609
+ Returns:
610
+ HttpResponse model
611
+ """
612
+ headers = dict(response.headers) if response.headers else {}
613
+ content_type = headers.get("content-type", headers.get("Content-Type", ""))
614
+
615
+ # Get response body
616
+ body_bytes = response.content
617
+ body, body_encoding, is_binary = _serialize_body(body_bytes, content_type)
618
+
619
+ if is_binary:
620
+ # Binary content - store bytes for file saving
621
+ resp = HttpResponse(
622
+ status=response.status_code,
623
+ headers=headers,
624
+ body=None,
625
+ )
626
+ resp._body_bytes = body if isinstance(body, bytes) else body_bytes
627
+ return resp
628
+
629
+ # Convert body to string for storage
630
+ body_str = ""
631
+ if body is not None:
632
+ if isinstance(body, str):
633
+ body_str = body
634
+ else:
635
+ body_str = json.dumps(body, ensure_ascii=False)
636
+
637
+ return HttpResponse(
638
+ status=response.status_code,
639
+ headers=headers,
640
+ body=body_str,
641
+ bodyEncoding=body_encoding,
642
+ )
643
+
644
+
645
+ # ============================================================================
646
+ # urllib3 functions
647
+ # ============================================================================
648
+
649
+
650
+ def create_request_from_urllib3(
651
+ method: str,
652
+ url: str,
653
+ body: Any = None,
654
+ headers: Any = None,
655
+ ) -> HttpRequest:
656
+ """
657
+ Create HttpRequest from urllib3 request parameters.
658
+
659
+ Args:
660
+ method: HTTP method
661
+ url: Full URL string
662
+ body: Request body
663
+ headers: Request headers
664
+
665
+ Returns:
666
+ HttpRequest model
667
+ """
668
+ parsed = urlparse(url)
669
+
670
+ query_params: Dict[str, str] = {}
671
+ if parsed.query:
672
+ for key, values in parse_qs(parsed.query).items():
673
+ query_params[key] = values[0] if values else ""
674
+
675
+ # Normalize headers - may contain bytes values from botocore
676
+ headers_dict: Dict[str, str] = {}
677
+ if headers:
678
+ for k, v in (headers.items() if hasattr(headers, "items") else headers):
679
+ key = k.decode("utf-8") if isinstance(k, bytes) else str(k)
680
+ val = v.decode("utf-8") if isinstance(v, bytes) else str(v)
681
+ headers_dict[key] = val
682
+
683
+ content_type = headers_dict.get("content-type", headers_dict.get("Content-Type", ""))
684
+
685
+ body_value, body_encoding, _ = _serialize_body(body, content_type)
686
+
687
+ body_str = None
688
+ if body_value is not None:
689
+ if body_encoding == "base64":
690
+ body_str = body_value
691
+ elif isinstance(body_value, str):
692
+ body_str = body_value
693
+ else:
694
+ body_str = json.dumps(body_value, ensure_ascii=False)
695
+
696
+ return HttpRequest(
697
+ method=method,
698
+ url=url,
699
+ headers=headers_dict,
700
+ queryParameters=query_params,
701
+ body=body_str,
702
+ bodyEncoding=body_encoding,
703
+ )
704
+
705
+
706
+ def create_response_from_urllib3(
707
+ response: Any, # urllib3.HTTPResponse
708
+ ) -> HttpResponse:
709
+ """
710
+ Create HttpResponse from a urllib3 HTTPResponse object.
711
+
712
+ Args:
713
+ response: urllib3.HTTPResponse object
714
+
715
+ Returns:
716
+ HttpResponse model
717
+ """
718
+ headers = dict(response.headers) if response.headers else {}
719
+ content_type = headers.get("content-type", headers.get("Content-Type", ""))
720
+
721
+ body_bytes = response.data
722
+ body, body_encoding, is_binary = _serialize_body(body_bytes, content_type)
723
+
724
+ if is_binary:
725
+ resp = HttpResponse(
726
+ status=response.status,
727
+ headers=headers,
728
+ body=None,
729
+ )
730
+ resp._body_bytes = body if isinstance(body, bytes) else body_bytes
731
+ return resp
732
+
733
+ body_str = ""
734
+ if body is not None:
735
+ if isinstance(body, str):
736
+ body_str = body
737
+ else:
738
+ body_str = json.dumps(body, ensure_ascii=False)
739
+
740
+ return HttpResponse(
741
+ status=response.status,
742
+ headers=headers,
743
+ body=body_str,
744
+ bodyEncoding=body_encoding,
745
+ )
746
+
747
+
748
+ # ============================================================================
749
+ # http.client functions
750
+ # ============================================================================
751
+
752
+
753
+ def create_request_from_http_client(
754
+ method: str,
755
+ url: str,
756
+ body: Any = None,
757
+ headers: Dict[str, str] = None,
758
+ ) -> HttpRequest:
759
+ """
760
+ Create HttpRequest from http.client request parameters.
761
+
762
+ Args:
763
+ method: HTTP method
764
+ url: Full URL string
765
+ body: Request body
766
+ headers: Request headers dict
767
+
768
+ Returns:
769
+ HttpRequest model
770
+ """
771
+ parsed = urlparse(url)
772
+
773
+ query_params: Dict[str, str] = {}
774
+ if parsed.query:
775
+ for key, values in parse_qs(parsed.query).items():
776
+ query_params[key] = values[0] if values else ""
777
+
778
+ headers_dict = headers or {}
779
+ content_type = headers_dict.get("content-type", headers_dict.get("Content-Type", ""))
780
+
781
+ body_value, body_encoding, _ = _serialize_body(body, content_type)
782
+
783
+ body_str = None
784
+ if body_value is not None:
785
+ if body_encoding == "base64":
786
+ body_str = body_value
787
+ elif isinstance(body_value, str):
788
+ body_str = body_value
789
+ else:
790
+ body_str = json.dumps(body_value, ensure_ascii=False)
791
+
792
+ return HttpRequest(
793
+ method=method,
794
+ url=url,
795
+ headers=headers_dict,
796
+ queryParameters=query_params,
797
+ body=body_str,
798
+ bodyEncoding=body_encoding,
799
+ )
800
+
801
+
802
+ def create_response_from_http_client(
803
+ response: Any, # http.client.HTTPResponse
804
+ ) -> HttpResponse:
805
+ """
806
+ Create HttpResponse from an http.client HTTPResponse object.
807
+
808
+ Args:
809
+ response: http.client.HTTPResponse object
810
+
811
+ Returns:
812
+ HttpResponse model
813
+ """
814
+ headers = dict(response.getheaders()) if hasattr(response, "getheaders") else {}
815
+ content_type = headers.get("content-type", headers.get("Content-Type", ""))
816
+
817
+ body_bytes = response.read()
818
+ body, body_encoding, is_binary = _serialize_body(body_bytes, content_type)
819
+
820
+ if is_binary:
821
+ resp = HttpResponse(
822
+ status=response.status,
823
+ headers=headers,
824
+ body=None,
825
+ )
826
+ resp._body_bytes = body if isinstance(body, bytes) else body_bytes
827
+ return resp
828
+
829
+ body_str = ""
830
+ if body is not None:
831
+ if isinstance(body, str):
832
+ body_str = body
833
+ else:
834
+ body_str = json.dumps(body, ensure_ascii=False)
835
+
836
+ return HttpResponse(
837
+ status=response.status,
838
+ headers=headers,
839
+ body=body_str,
840
+ bodyEncoding=body_encoding,
841
+ )
842
+
843
+
844
+ # ============================================================================
845
+ # aiohttp functions
846
+ # ============================================================================
847
+
848
+
849
+ def create_request_from_aiohttp(
850
+ method: str,
851
+ url: str,
852
+ data: Any = None,
853
+ headers: Any = None,
854
+ ) -> HttpRequest:
855
+ """
856
+ Create HttpRequest from aiohttp request parameters.
857
+
858
+ Args:
859
+ method: HTTP method
860
+ url: URL string
861
+ data: Request body data
862
+ headers: Request headers
863
+
864
+ Returns:
865
+ HttpRequest model
866
+ """
867
+ parsed = urlparse(url)
868
+
869
+ query_params: Dict[str, str] = {}
870
+ if parsed.query:
871
+ for key, values in parse_qs(parsed.query).items():
872
+ query_params[key] = values[0] if values else ""
873
+
874
+ headers_dict = dict(headers) if headers else {}
875
+ content_type = headers_dict.get("content-type", headers_dict.get("Content-Type", ""))
876
+
877
+ body_value, body_encoding, _ = _serialize_body(data, content_type)
878
+
879
+ body_str = None
880
+ if body_value is not None:
881
+ if body_encoding == "base64":
882
+ body_str = body_value
883
+ elif isinstance(body_value, str):
884
+ body_str = body_value
885
+ else:
886
+ body_str = json.dumps(body_value, ensure_ascii=False)
887
+
888
+ return HttpRequest(
889
+ method=method.upper(),
890
+ url=url,
891
+ headers=headers_dict,
892
+ queryParameters=query_params,
893
+ body=body_str,
894
+ bodyEncoding=body_encoding,
895
+ )
896
+
897
+
898
+ def create_response_from_aiohttp(
899
+ response: Any, # aiohttp.ClientResponse
900
+ body_bytes: bytes,
901
+ ) -> HttpResponse:
902
+ """
903
+ Create HttpResponse from an aiohttp ClientResponse object.
904
+
905
+ Args:
906
+ response: aiohttp.ClientResponse object
907
+ body_bytes: Already-read response body bytes
908
+
909
+ Returns:
910
+ HttpResponse model
911
+ """
912
+ headers = dict(response.headers) if response.headers else {}
913
+ content_type = headers.get("content-type", headers.get("Content-Type", ""))
914
+
915
+ body, body_encoding, is_binary = _serialize_body(body_bytes, content_type)
916
+
917
+ if is_binary:
918
+ resp = HttpResponse(
919
+ status=response.status,
920
+ headers=headers,
921
+ body=None,
922
+ )
923
+ resp._body_bytes = body if isinstance(body, bytes) else body_bytes
924
+ return resp
925
+
926
+ body_str = ""
927
+ if body is not None:
928
+ if isinstance(body, str):
929
+ body_str = body
930
+ else:
931
+ body_str = json.dumps(body, ensure_ascii=False)
932
+
933
+ return HttpResponse(
934
+ status=response.status,
935
+ headers=headers,
936
+ body=body_str,
937
+ bodyEncoding=body_encoding,
938
+ )
939
+
940
+
941
+ # ============================================================================
942
+ # httplib2 functions
943
+ # ============================================================================
944
+
945
+
946
+ def create_request_from_httplib2(
947
+ uri: str,
948
+ method: str = "GET",
949
+ body: Any = None,
950
+ headers: Any = None,
951
+ ) -> HttpRequest:
952
+ """
953
+ Create HttpRequest from httplib2 request parameters.
954
+
955
+ Args:
956
+ uri: Request URI
957
+ method: HTTP method
958
+ body: Request body
959
+ headers: Request headers
960
+
961
+ Returns:
962
+ HttpRequest model
963
+ """
964
+ parsed = urlparse(uri)
965
+
966
+ query_params: Dict[str, str] = {}
967
+ if parsed.query:
968
+ for key, values in parse_qs(parsed.query).items():
969
+ query_params[key] = values[0] if values else ""
970
+
971
+ headers_dict = dict(headers) if headers else {}
972
+ content_type = headers_dict.get("content-type", headers_dict.get("Content-Type", ""))
973
+
974
+ body_value, body_encoding, _ = _serialize_body(body, content_type)
975
+
976
+ body_str = None
977
+ if body_value is not None:
978
+ if body_encoding == "base64":
979
+ body_str = body_value
980
+ elif isinstance(body_value, str):
981
+ body_str = body_value
982
+ else:
983
+ body_str = json.dumps(body_value, ensure_ascii=False)
984
+
985
+ return HttpRequest(
986
+ method=method.upper(),
987
+ url=uri,
988
+ headers=headers_dict,
989
+ queryParameters=query_params,
990
+ body=body_str,
991
+ bodyEncoding=body_encoding,
992
+ )
993
+
994
+
995
+ def create_response_from_httplib2(
996
+ response: Any, # httplib2.Response
997
+ content: bytes,
998
+ ) -> HttpResponse:
999
+ """
1000
+ Create HttpResponse from an httplib2 Response object.
1001
+
1002
+ Args:
1003
+ response: httplib2.Response object
1004
+ content: Response body bytes
1005
+
1006
+ Returns:
1007
+ HttpResponse model
1008
+ """
1009
+ headers = dict(response) if response else {}
1010
+ content_type = headers.get("content-type", headers.get("Content-Type", ""))
1011
+
1012
+ body, body_encoding, is_binary = _serialize_body(content, content_type)
1013
+
1014
+ if is_binary:
1015
+ resp = HttpResponse(
1016
+ status=response.status,
1017
+ headers=headers,
1018
+ body=None,
1019
+ )
1020
+ resp._body_bytes = body if isinstance(body, bytes) else content
1021
+ return resp
1022
+
1023
+ body_str = ""
1024
+ if body is not None:
1025
+ if isinstance(body, str):
1026
+ body_str = body
1027
+ else:
1028
+ body_str = json.dumps(body, ensure_ascii=False)
1029
+
1030
+ return HttpResponse(
1031
+ status=response.status,
1032
+ headers=headers,
1033
+ body=body_str,
1034
+ bodyEncoding=body_encoding,
1035
+ )
1036
+
1037
+
1038
+ # ============================================================================
1039
+ # tornado functions
1040
+ # ============================================================================
1041
+
1042
+
1043
+ def create_request_from_tornado(
1044
+ request: Any, # tornado.httpclient.HTTPRequest
1045
+ ) -> HttpRequest:
1046
+ """
1047
+ Create HttpRequest from a tornado HTTPRequest object.
1048
+
1049
+ Args:
1050
+ request: tornado.httpclient.HTTPRequest object
1051
+
1052
+ Returns:
1053
+ HttpRequest model
1054
+ """
1055
+ url = request.url
1056
+ parsed = urlparse(url)
1057
+
1058
+ query_params: Dict[str, str] = {}
1059
+ if parsed.query:
1060
+ for key, values in parse_qs(parsed.query).items():
1061
+ query_params[key] = values[0] if values else ""
1062
+
1063
+ headers_dict = dict(request.headers) if request.headers else {}
1064
+ content_type = headers_dict.get("content-type", headers_dict.get("Content-Type", ""))
1065
+
1066
+ body = request.body
1067
+ body_value, body_encoding, _ = _serialize_body(body, content_type)
1068
+
1069
+ body_str = None
1070
+ if body_value is not None:
1071
+ if body_encoding == "base64":
1072
+ body_str = body_value
1073
+ elif isinstance(body_value, str):
1074
+ body_str = body_value
1075
+ else:
1076
+ body_str = json.dumps(body_value, ensure_ascii=False)
1077
+
1078
+ return HttpRequest(
1079
+ method=request.method or "GET",
1080
+ url=url,
1081
+ headers=headers_dict,
1082
+ queryParameters=query_params,
1083
+ body=body_str,
1084
+ bodyEncoding=body_encoding,
1085
+ )
1086
+
1087
+
1088
+ def create_response_from_tornado(
1089
+ response: Any, # tornado.httpclient.HTTPResponse
1090
+ ) -> HttpResponse:
1091
+ """
1092
+ Create HttpResponse from a tornado HTTPResponse object.
1093
+
1094
+ Args:
1095
+ response: tornado.httpclient.HTTPResponse object
1096
+
1097
+ Returns:
1098
+ HttpResponse model
1099
+ """
1100
+ headers = dict(response.headers) if response.headers else {}
1101
+ content_type = headers.get("content-type", headers.get("Content-Type", ""))
1102
+
1103
+ body_bytes = response.body or b""
1104
+ body, body_encoding, is_binary = _serialize_body(body_bytes, content_type)
1105
+
1106
+ if is_binary:
1107
+ resp = HttpResponse(
1108
+ status=response.code,
1109
+ headers=headers,
1110
+ body=None,
1111
+ )
1112
+ resp._body_bytes = body if isinstance(body, bytes) else body_bytes
1113
+ return resp
1114
+
1115
+ body_str = ""
1116
+ if body is not None:
1117
+ if isinstance(body, str):
1118
+ body_str = body
1119
+ else:
1120
+ body_str = json.dumps(body, ensure_ascii=False)
1121
+
1122
+ return HttpResponse(
1123
+ status=response.code,
1124
+ headers=headers,
1125
+ body=body_str,
1126
+ bodyEncoding=body_encoding,
1127
+ )
1128
+
1129
+
1130
+ # ============================================================================
1131
+ # botocore functions
1132
+ # ============================================================================
1133
+
1134
+
1135
+ def create_request_from_botocore(
1136
+ request: Any, # botocore.awsrequest.AWSRequest
1137
+ ) -> HttpRequest:
1138
+ """
1139
+ Create HttpRequest from a botocore AWSRequest object.
1140
+
1141
+ Args:
1142
+ request: botocore.awsrequest.AWSRequest object
1143
+
1144
+ Returns:
1145
+ HttpRequest model
1146
+ """
1147
+ url = request.url
1148
+ parsed = urlparse(url)
1149
+
1150
+ query_params: Dict[str, str] = {}
1151
+ if parsed.query:
1152
+ for key, values in parse_qs(parsed.query).items():
1153
+ query_params[key] = values[0] if values else ""
1154
+
1155
+ # Normalize headers - botocore can have bytes values
1156
+ headers_dict: Dict[str, str] = {}
1157
+ if request.headers:
1158
+ for k, v in request.headers.items():
1159
+ key = k.decode("utf-8") if isinstance(k, bytes) else k
1160
+ val = v.decode("utf-8") if isinstance(v, bytes) else v
1161
+ headers_dict[key] = val
1162
+
1163
+ content_type = headers_dict.get("content-type", headers_dict.get("Content-Type", ""))
1164
+
1165
+ body = request.body
1166
+ body_value, body_encoding, _ = _serialize_body(body, content_type)
1167
+
1168
+ body_str = None
1169
+ if body_value is not None:
1170
+ if body_encoding == "base64":
1171
+ body_str = body_value
1172
+ elif isinstance(body_value, str):
1173
+ body_str = body_value
1174
+ else:
1175
+ body_str = json.dumps(body_value, ensure_ascii=False)
1176
+
1177
+ return HttpRequest(
1178
+ method=request.method or "GET",
1179
+ url=url,
1180
+ headers=headers_dict,
1181
+ queryParameters=query_params,
1182
+ body=body_str,
1183
+ bodyEncoding=body_encoding,
1184
+ )
1185
+
1186
+
1187
+ def create_response_from_botocore(
1188
+ response: Any, # botocore.awsrequest.AWSResponse
1189
+ ) -> HttpResponse:
1190
+ """
1191
+ Create HttpResponse from a botocore AWSResponse object.
1192
+
1193
+ Args:
1194
+ response: botocore.awsrequest.AWSResponse object
1195
+
1196
+ Returns:
1197
+ HttpResponse model
1198
+ """
1199
+ # AWSResponse has headers as HTTPHeaderDict
1200
+ headers = dict(response.headers) if response.headers else {}
1201
+ content_type = headers.get("content-type", headers.get("Content-Type", ""))
1202
+
1203
+ # AWSResponse has content property for body bytes
1204
+ body_bytes = response.content
1205
+ body, body_encoding, is_binary = _serialize_body(body_bytes, content_type)
1206
+
1207
+ # AWSResponse uses status_code, not status
1208
+ status = response.status_code
1209
+
1210
+ if is_binary:
1211
+ resp = HttpResponse(
1212
+ status=status,
1213
+ headers=headers,
1214
+ body=None,
1215
+ )
1216
+ resp._body_bytes = body if isinstance(body, bytes) else body_bytes
1217
+ return resp
1218
+
1219
+ body_str = ""
1220
+ if body is not None:
1221
+ if isinstance(body, str):
1222
+ body_str = body
1223
+ else:
1224
+ body_str = json.dumps(body, ensure_ascii=False)
1225
+
1226
+ return HttpResponse(
1227
+ status=status,
1228
+ headers=headers,
1229
+ body=body_str,
1230
+ bodyEncoding=body_encoding,
1231
+ )
1232
+
1233
+
1234
+ # ============================================================================
1235
+ # httpcore functions
1236
+ # ============================================================================
1237
+
1238
+
1239
+ def create_request_from_httpcore(
1240
+ method: str,
1241
+ url: str,
1242
+ content: Any = None,
1243
+ headers: Any = None,
1244
+ ) -> HttpRequest:
1245
+ """
1246
+ Create HttpRequest from httpcore request parameters.
1247
+
1248
+ Args:
1249
+ method: HTTP method
1250
+ url: URL string
1251
+ content: Request body content
1252
+ headers: Request headers (list of tuples or bytes pairs)
1253
+
1254
+ Returns:
1255
+ HttpRequest model
1256
+ """
1257
+ parsed = urlparse(url)
1258
+
1259
+ query_params: Dict[str, str] = {}
1260
+ if parsed.query:
1261
+ for key, values in parse_qs(parsed.query).items():
1262
+ query_params[key] = values[0] if values else ""
1263
+
1264
+ # Convert headers from list of tuples to dict
1265
+ headers_dict: Dict[str, str] = {}
1266
+ if headers:
1267
+ for k, v in headers:
1268
+ key = k.decode() if isinstance(k, bytes) else k
1269
+ val = v.decode() if isinstance(v, bytes) else v
1270
+ headers_dict[key] = val
1271
+
1272
+ content_type = headers_dict.get("content-type", headers_dict.get("Content-Type", ""))
1273
+
1274
+ # Handle content
1275
+ if isinstance(content, bytes):
1276
+ body_data = content
1277
+ elif hasattr(content, "read"):
1278
+ body_data = content.read()
1279
+ else:
1280
+ body_data = content
1281
+
1282
+ body_value, body_encoding, _ = _serialize_body(body_data, content_type)
1283
+
1284
+ body_str = None
1285
+ if body_value is not None:
1286
+ if body_encoding == "base64":
1287
+ body_str = body_value
1288
+ elif isinstance(body_value, str):
1289
+ body_str = body_value
1290
+ else:
1291
+ body_str = json.dumps(body_value, ensure_ascii=False)
1292
+
1293
+ return HttpRequest(
1294
+ method=method.upper(),
1295
+ url=url,
1296
+ headers=headers_dict,
1297
+ queryParameters=query_params,
1298
+ body=body_str,
1299
+ bodyEncoding=body_encoding,
1300
+ )
1301
+
1302
+
1303
+ def create_response_from_httpcore(
1304
+ response: Any, # httpcore.Response
1305
+ ) -> HttpResponse:
1306
+ """
1307
+ Create HttpResponse from an httpcore Response object.
1308
+
1309
+ Args:
1310
+ response: httpcore.Response object
1311
+
1312
+ Returns:
1313
+ HttpResponse model
1314
+ """
1315
+ # Convert headers from list of tuples to dict
1316
+ headers_dict: Dict[str, str] = {}
1317
+ if response.headers:
1318
+ for k, v in response.headers:
1319
+ key = k.decode() if isinstance(k, bytes) else k
1320
+ val = v.decode() if isinstance(v, bytes) else v
1321
+ headers_dict[key] = val
1322
+
1323
+ content_type = headers_dict.get("content-type", headers_dict.get("Content-Type", ""))
1324
+
1325
+ # Get body content
1326
+ body_bytes = response.content
1327
+ body, body_encoding, is_binary = _serialize_body(body_bytes, content_type)
1328
+
1329
+ if is_binary:
1330
+ resp = HttpResponse(
1331
+ status=response.status,
1332
+ headers=headers_dict,
1333
+ body=None,
1334
+ )
1335
+ resp._body_bytes = body if isinstance(body, bytes) else body_bytes
1336
+ return resp
1337
+
1338
+ body_str = ""
1339
+ if body is not None:
1340
+ if isinstance(body, str):
1341
+ body_str = body
1342
+ else:
1343
+ body_str = json.dumps(body, ensure_ascii=False)
1344
+
1345
+ return HttpResponse(
1346
+ status=response.status,
1347
+ headers=headers_dict,
1348
+ body=body_str,
1349
+ bodyEncoding=body_encoding,
1350
+ )