skyflow-flowvault-python 1.0.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.
Files changed (202) hide show
  1. common/.coveragerc +4 -0
  2. common/__init__.py +3 -0
  3. common/client/__init__.py +0 -0
  4. common/client/base_skyflow.py +345 -0
  5. common/client/utils/__init__.py +1 -0
  6. common/client/utils/_utils.py +127 -0
  7. common/errors/__init__.py +1 -0
  8. common/errors/_skyflow_error.py +17 -0
  9. common/generated/__init__.py +0 -0
  10. common/generated/rest/__init__.py +24 -0
  11. common/generated/rest/authentication/__init__.py +4 -0
  12. common/generated/rest/authentication/client.py +181 -0
  13. common/generated/rest/authentication/raw_client.py +241 -0
  14. common/generated/rest/client.py +153 -0
  15. common/generated/rest/core/__init__.py +52 -0
  16. common/generated/rest/core/api_error.py +23 -0
  17. common/generated/rest/core/client_wrapper.py +86 -0
  18. common/generated/rest/core/datetime_utils.py +28 -0
  19. common/generated/rest/core/file.py +67 -0
  20. common/generated/rest/core/force_multipart.py +16 -0
  21. common/generated/rest/core/http_client.py +543 -0
  22. common/generated/rest/core/http_response.py +55 -0
  23. common/generated/rest/core/jsonable_encoder.py +100 -0
  24. common/generated/rest/core/pydantic_utilities.py +255 -0
  25. common/generated/rest/core/query_encoder.py +58 -0
  26. common/generated/rest/core/remove_none_from_dict.py +11 -0
  27. common/generated/rest/core/request_options.py +35 -0
  28. common/generated/rest/core/serialization.py +276 -0
  29. common/generated/rest/environment.py +8 -0
  30. common/generated/rest/errors/__init__.py +9 -0
  31. common/generated/rest/errors/bad_request_error.py +14 -0
  32. common/generated/rest/errors/not_found_error.py +14 -0
  33. common/generated/rest/errors/unauthorized_error.py +14 -0
  34. common/generated/rest/py.typed +0 -0
  35. common/generated/rest/types/__init__.py +9 -0
  36. common/generated/rest/types/googlerpc_status.py +22 -0
  37. common/generated/rest/types/protobuf_any.py +21 -0
  38. common/generated/rest/types/v_1_get_auth_token_response.py +33 -0
  39. common/generated/rest/version.py +6 -0
  40. common/service_account/__init__.py +1 -0
  41. common/service_account/_utils.py +248 -0
  42. common/service_account/client/__init__.py +0 -0
  43. common/service_account/client/auth_client.py +13 -0
  44. common/utils/__init__.py +4 -0
  45. common/utils/_helpers.py +18 -0
  46. common/utils/_skyflow_messages.py +445 -0
  47. common/utils/_utils.py +50 -0
  48. common/utils/constants.py +291 -0
  49. common/utils/enums/__init__.py +12 -0
  50. common/utils/enums/content_types.py +9 -0
  51. common/utils/enums/detect_entities.py +73 -0
  52. common/utils/enums/detect_output_transcriptions.py +8 -0
  53. common/utils/enums/env.py +13 -0
  54. common/utils/enums/log_level.py +8 -0
  55. common/utils/enums/masking_method.py +5 -0
  56. common/utils/enums/redaction_type.py +7 -0
  57. common/utils/enums/request_method.py +8 -0
  58. common/utils/enums/token_mode.py +6 -0
  59. common/utils/enums/token_type.py +6 -0
  60. common/utils/logger/__init__.py +2 -0
  61. common/utils/logger/_log_helpers.py +47 -0
  62. common/utils/logger/_logger.py +50 -0
  63. common/utils/validations/__init__.py +11 -0
  64. common/utils/validations/_validations.py +267 -0
  65. common/vault/base_vault_client.py +122 -0
  66. common/vault/base_vault_controller.py +58 -0
  67. common/vault/data/__init__.py +2 -0
  68. common/vault/data/_base_insert_request.py +7 -0
  69. common/vault/data/_base_insert_response.py +11 -0
  70. skyflow/__init__.py +2 -0
  71. skyflow/client/__init__.py +1 -0
  72. skyflow/client/_http_config_builder.py +45 -0
  73. skyflow/client/skyflow.py +15 -0
  74. skyflow/error/__init__.py +3 -0
  75. skyflow/generated/__init__.py +0 -0
  76. skyflow/generated/rest/__init__.py +164 -0
  77. skyflow/generated/rest/_default_clients.py +32 -0
  78. skyflow/generated/rest/client.py +385 -0
  79. skyflow/generated/rest/core/__init__.py +127 -0
  80. skyflow/generated/rest/core/api_error.py +23 -0
  81. skyflow/generated/rest/core/client_wrapper.py +148 -0
  82. skyflow/generated/rest/core/datetime_utils.py +70 -0
  83. skyflow/generated/rest/core/file.py +67 -0
  84. skyflow/generated/rest/core/force_multipart.py +18 -0
  85. skyflow/generated/rest/core/http_client.py +940 -0
  86. skyflow/generated/rest/core/http_response.py +63 -0
  87. skyflow/generated/rest/core/http_sse/__init__.py +42 -0
  88. skyflow/generated/rest/core/http_sse/_api.py +455 -0
  89. skyflow/generated/rest/core/http_sse/_decoders.py +74 -0
  90. skyflow/generated/rest/core/http_sse/_exceptions.py +7 -0
  91. skyflow/generated/rest/core/http_sse/_models.py +17 -0
  92. skyflow/generated/rest/core/jsonable_encoder.py +133 -0
  93. skyflow/generated/rest/core/logging.py +107 -0
  94. skyflow/generated/rest/core/parse_error.py +36 -0
  95. skyflow/generated/rest/core/pydantic_utilities.py +486 -0
  96. skyflow/generated/rest/core/query_encoder.py +58 -0
  97. skyflow/generated/rest/core/remove_none_from_dict.py +11 -0
  98. skyflow/generated/rest/core/request_options.py +40 -0
  99. skyflow/generated/rest/core/serialization.py +347 -0
  100. skyflow/generated/rest/environment.py +8 -0
  101. skyflow/generated/rest/errors/__init__.py +53 -0
  102. skyflow/generated/rest/errors/bad_request_error.py +11 -0
  103. skyflow/generated/rest/errors/forbidden_error.py +11 -0
  104. skyflow/generated/rest/errors/internal_server_error.py +11 -0
  105. skyflow/generated/rest/errors/not_found_error.py +11 -0
  106. skyflow/generated/rest/errors/too_many_requests_error.py +11 -0
  107. skyflow/generated/rest/errors/unauthorized_error.py +11 -0
  108. skyflow/generated/rest/py.typed +0 -0
  109. skyflow/generated/rest/query/__init__.py +4 -0
  110. skyflow/generated/rest/query/client.py +139 -0
  111. skyflow/generated/rest/query/raw_client.py +239 -0
  112. skyflow/generated/rest/raw_client.py +86 -0
  113. skyflow/generated/rest/records/__init__.py +34 -0
  114. skyflow/generated/rest/records/client.py +685 -0
  115. skyflow/generated/rest/records/raw_client.py +988 -0
  116. skyflow/generated/rest/records/types/__init__.py +34 -0
  117. skyflow/generated/rest/records/types/update_request_update_type.py +5 -0
  118. skyflow/generated/rest/tokens/__init__.py +4 -0
  119. skyflow/generated/rest/tokens/client.py +246 -0
  120. skyflow/generated/rest/tokens/raw_client.py +507 -0
  121. skyflow/generated/rest/types/__init__.py +116 -0
  122. skyflow/generated/rest/types/column_redactions.py +33 -0
  123. skyflow/generated/rest/types/delete_response.py +23 -0
  124. skyflow/generated/rest/types/delete_response_object.py +42 -0
  125. skyflow/generated/rest/types/detokenize_response.py +23 -0
  126. skyflow/generated/rest/types/detokenize_response_object.py +54 -0
  127. skyflow/generated/rest/types/error_response.py +20 -0
  128. skyflow/generated/rest/types/error_response_error.py +28 -0
  129. skyflow/generated/rest/types/execute_query_record_response.py +22 -0
  130. skyflow/generated/rest/types/execute_query_response.py +26 -0
  131. skyflow/generated/rest/types/execute_query_response_metadata.py +26 -0
  132. skyflow/generated/rest/types/get_request_data.py +60 -0
  133. skyflow/generated/rest/types/get_response.py +23 -0
  134. skyflow/generated/rest/types/get_tokens_from_values_request_object.py +30 -0
  135. skyflow/generated/rest/types/get_tokens_from_values_response.py +23 -0
  136. skyflow/generated/rest/types/google_protobuf_value.py +8 -0
  137. skyflow/generated/rest/types/http_code.py +6 -0
  138. skyflow/generated/rest/types/insert_record_data.py +36 -0
  139. skyflow/generated/rest/types/insert_response.py +23 -0
  140. skyflow/generated/rest/types/record_response_object.py +70 -0
  141. skyflow/generated/rest/types/token_group_redactions.py +33 -0
  142. skyflow/generated/rest/types/tokenize_response_object.py +49 -0
  143. skyflow/generated/rest/types/unique_value.py +22 -0
  144. skyflow/generated/rest/types/update_record_data.py +52 -0
  145. skyflow/generated/rest/types/update_record_data_update_type.py +5 -0
  146. skyflow/generated/rest/types/update_response.py +23 -0
  147. skyflow/generated/rest/types/upsert.py +45 -0
  148. skyflow/generated/rest/types/upsert_update_type.py +5 -0
  149. skyflow/generated/rest/version.py +6 -0
  150. skyflow/service_account/__init__.py +15 -0
  151. skyflow/utils/__init__.py +9 -0
  152. skyflow/utils/_http_config.py +36 -0
  153. skyflow/utils/_response_parsing.py +66 -0
  154. skyflow/utils/_retry.py +97 -0
  155. skyflow/utils/_skyflow_messages.py +114 -0
  156. skyflow/utils/_utils.py +54 -0
  157. skyflow/utils/_version.py +1 -0
  158. skyflow/utils/enums/__init__.py +3 -0
  159. skyflow/utils/enums/_custom_header_key.py +10 -0
  160. skyflow/utils/enums/_env_urls.py +9 -0
  161. skyflow/utils/enums/_upsert_type.py +7 -0
  162. skyflow/utils/validations/__init__.py +9 -0
  163. skyflow/utils/validations/_validations.py +195 -0
  164. skyflow/vault/__init__.py +0 -0
  165. skyflow/vault/client/__init__.py +0 -0
  166. skyflow/vault/client/client.py +102 -0
  167. skyflow/vault/controller/__init__.py +1 -0
  168. skyflow/vault/controller/_vault.py +475 -0
  169. skyflow/vault/data/__init__.py +30 -0
  170. skyflow/vault/data/_column_redactions.py +4 -0
  171. skyflow/vault/data/_delete_options.py +8 -0
  172. skyflow/vault/data/_delete_request.py +5 -0
  173. skyflow/vault/data/_delete_response.py +9 -0
  174. skyflow/vault/data/_delete_response_record.py +9 -0
  175. skyflow/vault/data/_detokenize_options.py +8 -0
  176. skyflow/vault/data/_detokenize_request.py +9 -0
  177. skyflow/vault/data/_detokenize_response.py +9 -0
  178. skyflow/vault/data/_detokenize_response_record.py +16 -0
  179. skyflow/vault/data/_detokenize_response_record_metadata.py +10 -0
  180. skyflow/vault/data/_get_options.py +8 -0
  181. skyflow/vault/data/_get_request.py +17 -0
  182. skyflow/vault/data/_get_request_record.py +13 -0
  183. skyflow/vault/data/_get_response.py +9 -0
  184. skyflow/vault/data/_get_response_record.py +17 -0
  185. skyflow/vault/data/_insert_options.py +8 -0
  186. skyflow/vault/data/_insert_request.py +11 -0
  187. skyflow/vault/data/_insert_request_record.py +9 -0
  188. skyflow/vault/data/_insert_response.py +9 -0
  189. skyflow/vault/data/_insert_response_record.py +17 -0
  190. skyflow/vault/data/_request_context.py +16 -0
  191. skyflow/vault/data/_token.py +8 -0
  192. skyflow/vault/data/_token_group_redactions.py +4 -0
  193. skyflow/vault/data/_update_options.py +8 -0
  194. skyflow/vault/data/_update_request.py +11 -0
  195. skyflow/vault/data/_update_request_record.py +6 -0
  196. skyflow/vault/data/_update_response.py +9 -0
  197. skyflow/vault/data/_update_response_record.py +17 -0
  198. skyflow/vault/data/_upsert_options.py +7 -0
  199. skyflow_flowvault_python-1.0.0.dist-info/METADATA +896 -0
  200. skyflow_flowvault_python-1.0.0.dist-info/RECORD +202 -0
  201. skyflow_flowvault_python-1.0.0.dist-info/WHEEL +5 -0
  202. skyflow_flowvault_python-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,940 @@
1
+ # This file was auto-generated by Fern from our API Definition.
2
+
3
+ import asyncio
4
+ import email.utils
5
+ import re
6
+ import socket
7
+ import time
8
+ import typing
9
+ from contextlib import asynccontextmanager, contextmanager
10
+ from random import random
11
+
12
+ import httpx
13
+ from .file import File, convert_file_dict_to_httpx_tuples
14
+ from .force_multipart import FORCE_MULTIPART
15
+ from .jsonable_encoder import jsonable_encoder
16
+ from .logging import LogConfig, Logger, create_logger
17
+ from .query_encoder import encode_query
18
+ from .remove_none_from_dict import remove_none_from_dict as remove_none_from_dict
19
+ from .request_options import RequestOptions
20
+ from httpx._types import RequestFiles
21
+
22
+ INITIAL_RETRY_DELAY_SECONDS = 1.0
23
+ MAX_RETRY_DELAY_SECONDS = 60.0
24
+ JITTER_FACTOR = 0.2 # 20% random jitter
25
+
26
+
27
+ def get_keepalive_socket_options(
28
+ idle: int = 60,
29
+ intvl: int = 30,
30
+ cnt: int = 5,
31
+ ) -> typing.List[typing.Tuple[int, int, int]]:
32
+ """
33
+ Build TCP keepalive socket options for the current platform.
34
+
35
+ Keepalive probes keep otherwise-idle connections alive so that long,
36
+ non-streaming requests survive idle-connection reaping by a firewall,
37
+ load balancer, or NAT. The available socket constants are OS-dependent,
38
+ so each option is guarded and only emitted when the platform defines it:
39
+
40
+ - ``SO_KEEPALIVE`` is portable (Linux/macOS/Windows).
41
+ - The idle-before-first-probe knob is ``TCP_KEEPIDLE`` on Linux and modern
42
+ Windows, but ``TCP_KEEPALIVE`` on macOS.
43
+ - ``TCP_KEEPINTVL`` / ``TCP_KEEPCNT`` exist on Linux/macOS/modern Windows.
44
+
45
+ Passing these tuples to ``httpx.HTTPTransport(socket_options=...)`` /
46
+ ``httpx.AsyncHTTPTransport(socket_options=...)`` applies them to every
47
+ connection the transport opens.
48
+ """
49
+ opts: typing.List[typing.Tuple[int, int, int]] = [(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)]
50
+ idle_const = getattr(socket, "TCP_KEEPIDLE", None) or getattr(socket, "TCP_KEEPALIVE", None)
51
+ if idle_const:
52
+ opts.append((socket.IPPROTO_TCP, idle_const, idle))
53
+ if hasattr(socket, "TCP_KEEPINTVL"):
54
+ opts.append((socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, intvl))
55
+ if hasattr(socket, "TCP_KEEPCNT"):
56
+ opts.append((socket.IPPROTO_TCP, socket.TCP_KEEPCNT, cnt))
57
+ return opts
58
+
59
+
60
+ def _parse_retry_after(response_headers: httpx.Headers) -> typing.Optional[float]:
61
+ """
62
+ This function parses the `Retry-After` header in a HTTP response and returns the number of seconds to wait.
63
+
64
+ Inspired by the urllib3 retry implementation.
65
+ """
66
+ retry_after_ms = response_headers.get("retry-after-ms")
67
+ if retry_after_ms is not None:
68
+ try:
69
+ return int(retry_after_ms) / 1000 if retry_after_ms > 0 else 0
70
+ except Exception:
71
+ pass
72
+
73
+ retry_after = response_headers.get("retry-after")
74
+ if retry_after is None:
75
+ return None
76
+
77
+ # Attempt to parse the header as an int.
78
+ if re.match(r"^\s*[0-9]+\s*$", retry_after):
79
+ seconds = float(retry_after)
80
+ # Fallback to parsing it as a date.
81
+ else:
82
+ retry_date_tuple = email.utils.parsedate_tz(retry_after)
83
+ if retry_date_tuple is None:
84
+ return None
85
+ if retry_date_tuple[9] is None: # Python 2
86
+ # Assume UTC if no timezone was specified
87
+ # On Python2.7, parsedate_tz returns None for a timezone offset
88
+ # instead of 0 if no timezone is given, where mktime_tz treats
89
+ # a None timezone offset as local time.
90
+ retry_date_tuple = retry_date_tuple[:9] + (0,) + retry_date_tuple[10:]
91
+
92
+ retry_date = email.utils.mktime_tz(retry_date_tuple)
93
+ seconds = retry_date - time.time()
94
+
95
+ if seconds < 0:
96
+ seconds = 0
97
+
98
+ return seconds
99
+
100
+
101
+ def _add_positive_jitter(delay: float) -> float:
102
+ """Add positive jitter (0-20%) to prevent thundering herd."""
103
+ jitter_multiplier = 1 + random() * JITTER_FACTOR
104
+ return delay * jitter_multiplier
105
+
106
+
107
+ def _add_symmetric_jitter(delay: float) -> float:
108
+ """Add symmetric jitter (±10%) for exponential backoff."""
109
+ jitter_multiplier = 1 + (random() - 0.5) * JITTER_FACTOR
110
+ return delay * jitter_multiplier
111
+
112
+
113
+ def _parse_x_ratelimit_reset(response_headers: httpx.Headers) -> typing.Optional[float]:
114
+ """
115
+ Parse the X-RateLimit-Reset header (Unix timestamp in seconds).
116
+ Returns seconds to wait, or None if header is missing/invalid.
117
+ """
118
+ reset_time_str = response_headers.get("x-ratelimit-reset")
119
+ if reset_time_str is None:
120
+ return None
121
+
122
+ try:
123
+ reset_time = int(reset_time_str)
124
+ delay = reset_time - time.time()
125
+ if delay > 0:
126
+ return delay
127
+ except (ValueError, TypeError):
128
+ pass
129
+
130
+ return None
131
+
132
+
133
+ def _retry_timeout(response: httpx.Response, retries: int) -> float:
134
+ """
135
+ Determine the amount of time to wait before retrying a request.
136
+ This function begins by trying to parse a retry-after header from the response, and then proceeds to use exponential backoff
137
+ with a jitter to determine the number of seconds to wait.
138
+ """
139
+
140
+ # 1. Check Retry-After header first
141
+ retry_after = _parse_retry_after(response.headers)
142
+ if retry_after is not None and retry_after > 0:
143
+ return min(retry_after, MAX_RETRY_DELAY_SECONDS)
144
+
145
+ # 2. Check X-RateLimit-Reset header (with positive jitter)
146
+ ratelimit_reset = _parse_x_ratelimit_reset(response.headers)
147
+ if ratelimit_reset is not None:
148
+ return _add_positive_jitter(min(ratelimit_reset, MAX_RETRY_DELAY_SECONDS))
149
+
150
+ # 3. Fall back to exponential backoff (with symmetric jitter)
151
+ backoff = min(INITIAL_RETRY_DELAY_SECONDS * pow(2.0, retries), MAX_RETRY_DELAY_SECONDS)
152
+ return _add_symmetric_jitter(backoff)
153
+
154
+
155
+ def _retry_timeout_from_retries(retries: int) -> float:
156
+ """Determine retry timeout using exponential backoff when no response is available."""
157
+ backoff = min(INITIAL_RETRY_DELAY_SECONDS * pow(2.0, retries), MAX_RETRY_DELAY_SECONDS)
158
+ return _add_symmetric_jitter(backoff)
159
+
160
+
161
+ def _should_retry(response: httpx.Response) -> bool:
162
+ return response.status_code >= 500 or response.status_code in [429, 408, 409]
163
+
164
+
165
+ _SENSITIVE_HEADERS = frozenset(
166
+ {
167
+ "authorization",
168
+ "www-authenticate",
169
+ "x-api-key",
170
+ "api-key",
171
+ "apikey",
172
+ "x-api-token",
173
+ "x-auth-token",
174
+ "auth-token",
175
+ "cookie",
176
+ "set-cookie",
177
+ "proxy-authorization",
178
+ "proxy-authenticate",
179
+ "x-csrf-token",
180
+ "x-xsrf-token",
181
+ "x-session-token",
182
+ "x-access-token",
183
+ }
184
+ )
185
+
186
+
187
+ def _redact_headers(headers: typing.Dict[str, str]) -> typing.Dict[str, str]:
188
+ return {k: ("[REDACTED]" if k.lower() in _SENSITIVE_HEADERS else v) for k, v in headers.items()}
189
+
190
+
191
+ def _build_url(base_url: str, path: typing.Optional[str]) -> str:
192
+ """
193
+ Build a full URL by joining a base URL with a path.
194
+
195
+ This function correctly handles base URLs that contain path prefixes (e.g., tenant-based URLs)
196
+ by using string concatenation instead of urllib.parse.urljoin(), which would incorrectly
197
+ strip path components when the path starts with '/'.
198
+
199
+ Example:
200
+ >>> _build_url("https://cloud.example.com/org/tenant/api", "/users")
201
+ 'https://cloud.example.com/org/tenant/api/users'
202
+
203
+ Args:
204
+ base_url: The base URL, which may contain path prefixes.
205
+ path: The path to append. Can be None or empty string.
206
+
207
+ Returns:
208
+ The full URL with base_url and path properly joined.
209
+ """
210
+ if not path:
211
+ return base_url
212
+ return f"{base_url.rstrip('/')}/{path.lstrip('/')}"
213
+
214
+
215
+ def _maybe_filter_none_from_multipart_data(
216
+ data: typing.Optional[typing.Any],
217
+ request_files: typing.Optional[RequestFiles],
218
+ force_multipart: typing.Optional[bool],
219
+ ) -> typing.Optional[typing.Any]:
220
+ """
221
+ Filter None values from data body for multipart/form requests.
222
+ This prevents httpx from converting None to empty strings in multipart encoding.
223
+ Only applies when files are present or force_multipart is True.
224
+ """
225
+ if data is not None and isinstance(data, typing.Mapping) and (request_files or force_multipart):
226
+ return remove_none_from_dict(data)
227
+ return data
228
+
229
+
230
+ def remove_omit_from_dict(
231
+ original: typing.Dict[str, typing.Optional[typing.Any]],
232
+ omit: typing.Optional[typing.Any],
233
+ ) -> typing.Dict[str, typing.Any]:
234
+ if omit is None:
235
+ return original
236
+ new: typing.Dict[str, typing.Any] = {}
237
+ for key, value in original.items():
238
+ if value is not omit:
239
+ new[key] = value
240
+ return new
241
+
242
+
243
+ def maybe_filter_request_body(
244
+ data: typing.Optional[typing.Any],
245
+ request_options: typing.Optional[RequestOptions],
246
+ omit: typing.Optional[typing.Any],
247
+ ) -> typing.Optional[typing.Any]:
248
+ if data is None:
249
+ return (
250
+ jsonable_encoder(request_options.get("additional_body_parameters", {})) or {}
251
+ if request_options is not None
252
+ else None
253
+ )
254
+ elif not isinstance(data, typing.Mapping):
255
+ data_content = jsonable_encoder(data)
256
+ else:
257
+ data_content = {
258
+ **(jsonable_encoder(remove_omit_from_dict(data, omit))), # type: ignore
259
+ **(
260
+ jsonable_encoder(request_options.get("additional_body_parameters", {})) or {}
261
+ if request_options is not None
262
+ else {}
263
+ ),
264
+ }
265
+ return data_content
266
+
267
+
268
+ # Abstracted out for testing purposes
269
+ def get_request_body(
270
+ *,
271
+ json: typing.Optional[typing.Any],
272
+ data: typing.Optional[typing.Any],
273
+ request_options: typing.Optional[RequestOptions],
274
+ omit: typing.Optional[typing.Any],
275
+ optional_body: bool = False,
276
+ ) -> typing.Tuple[typing.Optional[typing.Any], typing.Optional[typing.Any]]:
277
+ # A whole body left at the sentinel was never passed by the caller, so it is absent
278
+ # rather than empty: the request carries no content and no `Content-Type`.
279
+ if omit is not None:
280
+ if json is omit:
281
+ json = None
282
+ if data is omit:
283
+ data = None
284
+
285
+ json_body = None
286
+ data_body = None
287
+ if data is not None:
288
+ data_body = maybe_filter_request_body(data, request_options, omit)
289
+ else:
290
+ # If both data and json are None, we send json data in the event extra properties are specified
291
+ json_body = maybe_filter_request_body(json, request_options, omit)
292
+
293
+ has_additional_body_parameters = bool(
294
+ request_options is not None and request_options.get("additional_body_parameters")
295
+ )
296
+
297
+ # Only collapse empty dict to None when the body was not explicitly provided
298
+ # and there are no additional body parameters. This preserves explicit empty
299
+ # bodies (e.g., when an endpoint has a request body type but all fields are optional).
300
+ # `optional_body` marks an endpoint whose body the API does not require, where a body
301
+ # that ends up empty means the caller passed none of its properties, so the request is
302
+ # sent with no content and no `Content-Type`.
303
+ if json_body == {} and (json is None or optional_body) and not has_additional_body_parameters:
304
+ json_body = None
305
+ if data_body == {} and (data is None or optional_body) and not has_additional_body_parameters:
306
+ data_body = None
307
+
308
+ return json_body, data_body
309
+
310
+
311
+ def drop_content_type_without_body(
312
+ headers: typing.Dict[str, typing.Any],
313
+ *,
314
+ json_body: typing.Optional[typing.Any],
315
+ data_body: typing.Optional[typing.Any],
316
+ optional_body: bool,
317
+ ) -> typing.Dict[str, typing.Any]:
318
+ """Strip ``Content-Type`` from a request that carries no body.
319
+
320
+ ``get_request_body`` drops the body of an ``optional_body`` endpoint when the caller
321
+ supplied none of it, but the endpoint still passes the content type it would have used.
322
+ A request that sends nothing must not advertise a media type, so a server that branches
323
+ on the header sees a bodyless call for what it is.
324
+ """
325
+ if not optional_body or json_body is not None or data_body is not None:
326
+ return headers
327
+ return {key: value for key, value in headers.items() if key.lower() != "content-type"}
328
+
329
+
330
+ class HttpClient:
331
+ def __init__(
332
+ self,
333
+ *,
334
+ httpx_client: httpx.Client,
335
+ base_timeout: typing.Callable[[], typing.Optional[float]],
336
+ base_headers: typing.Callable[[], typing.Dict[str, str]],
337
+ base_url: typing.Optional[typing.Callable[[], str]] = None,
338
+ base_max_retries: int = 2,
339
+ logging_config: typing.Optional[typing.Union[LogConfig, Logger]] = None,
340
+ ):
341
+ self.base_url = base_url
342
+ self.base_timeout = base_timeout
343
+ self.base_headers = base_headers
344
+ self.base_max_retries = base_max_retries
345
+ self.httpx_client = httpx_client
346
+ self.logger = create_logger(logging_config)
347
+
348
+ def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str:
349
+ base_url = maybe_base_url
350
+ if self.base_url is not None and base_url is None:
351
+ base_url = self.base_url()
352
+
353
+ if base_url is None:
354
+ raise ValueError("A base_url is required to make this request, please provide one and try again.")
355
+ return base_url
356
+
357
+ def request(
358
+ self,
359
+ path: typing.Optional[str] = None,
360
+ *,
361
+ method: str,
362
+ base_url: typing.Optional[str] = None,
363
+ params: typing.Optional[typing.Dict[str, typing.Any]] = None,
364
+ json: typing.Optional[typing.Any] = None,
365
+ data: typing.Optional[typing.Any] = None,
366
+ content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None,
367
+ files: typing.Optional[
368
+ typing.Union[
369
+ typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]],
370
+ typing.List[typing.Tuple[str, File]],
371
+ ]
372
+ ] = None,
373
+ headers: typing.Optional[typing.Dict[str, typing.Any]] = None,
374
+ request_options: typing.Optional[RequestOptions] = None,
375
+ retries: int = 0,
376
+ omit: typing.Optional[typing.Any] = None,
377
+ optional_body: bool = False,
378
+ force_multipart: typing.Optional[bool] = None,
379
+ ) -> httpx.Response:
380
+ base_url = self.get_base_url(base_url)
381
+ _timeout = (
382
+ request_options.get("timeout")
383
+ if request_options is not None and request_options.get("timeout") is not None
384
+ else request_options.get("timeout_in_seconds")
385
+ if request_options is not None and request_options.get("timeout_in_seconds") is not None
386
+ else self.base_timeout()
387
+ )
388
+ timeout = _timeout if _timeout is not None else httpx.USE_CLIENT_DEFAULT
389
+
390
+ json_body, data_body = get_request_body(
391
+ json=json, data=data, request_options=request_options, omit=omit, optional_body=optional_body
392
+ )
393
+
394
+ request_files: typing.Optional[RequestFiles] = (
395
+ convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit))
396
+ if (files is not None and files is not omit and isinstance(files, dict))
397
+ else None
398
+ )
399
+
400
+ if (request_files is None or len(request_files) == 0) and force_multipart:
401
+ request_files = FORCE_MULTIPART
402
+
403
+ data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart)
404
+
405
+ # Compute encoded params separately to avoid passing empty list to httpx
406
+ # (httpx strips existing query params from URL when params=[] is passed)
407
+ _encoded_params = encode_query(
408
+ jsonable_encoder(
409
+ remove_none_from_dict(
410
+ remove_omit_from_dict(
411
+ {
412
+ **(params if params is not None else {}),
413
+ **(
414
+ request_options.get("additional_query_parameters", {}) or {}
415
+ if request_options is not None
416
+ else {}
417
+ ),
418
+ },
419
+ omit,
420
+ )
421
+ )
422
+ )
423
+ )
424
+
425
+ _request_url = _build_url(base_url, path)
426
+ _request_headers = jsonable_encoder(
427
+ remove_none_from_dict(
428
+ {
429
+ **self.base_headers(),
430
+ **(headers if headers is not None else {}),
431
+ **(request_options.get("additional_headers", {}) or {} if request_options is not None else {}),
432
+ }
433
+ )
434
+ )
435
+ _request_headers = drop_content_type_without_body(
436
+ _request_headers, json_body=json_body, data_body=data_body, optional_body=optional_body
437
+ )
438
+
439
+ if self.logger.is_debug():
440
+ self.logger.debug(
441
+ "Making HTTP request",
442
+ method=method,
443
+ url=_request_url,
444
+ headers=_redact_headers(_request_headers),
445
+ has_body=json_body is not None or data_body is not None,
446
+ )
447
+
448
+ max_retries: int = (
449
+ request_options.get("max_retries", self.base_max_retries)
450
+ if request_options is not None
451
+ else self.base_max_retries
452
+ )
453
+
454
+ try:
455
+ response = self.httpx_client.request(
456
+ method=method,
457
+ url=_request_url,
458
+ headers=_request_headers,
459
+ params=_encoded_params if _encoded_params else None,
460
+ json=json_body,
461
+ data=data_body,
462
+ content=content,
463
+ files=request_files,
464
+ timeout=timeout,
465
+ )
466
+ except (httpx.ConnectError, httpx.RemoteProtocolError):
467
+ if retries < max_retries:
468
+ time.sleep(_retry_timeout_from_retries(retries=retries))
469
+ return self.request(
470
+ path=path,
471
+ method=method,
472
+ base_url=base_url,
473
+ params=params,
474
+ json=json,
475
+ data=data,
476
+ content=content,
477
+ files=files,
478
+ headers=headers,
479
+ request_options=request_options,
480
+ retries=retries + 1,
481
+ omit=omit,
482
+ force_multipart=force_multipart,
483
+ )
484
+ raise
485
+
486
+ if _should_retry(response=response):
487
+ if retries < max_retries:
488
+ time.sleep(_retry_timeout(response=response, retries=retries))
489
+ return self.request(
490
+ path=path,
491
+ method=method,
492
+ base_url=base_url,
493
+ params=params,
494
+ json=json,
495
+ data=data,
496
+ content=content,
497
+ files=files,
498
+ headers=headers,
499
+ request_options=request_options,
500
+ retries=retries + 1,
501
+ omit=omit,
502
+ force_multipart=force_multipart,
503
+ )
504
+
505
+ if self.logger.is_debug():
506
+ if 200 <= response.status_code < 400:
507
+ self.logger.debug(
508
+ "HTTP request succeeded",
509
+ method=method,
510
+ url=_request_url,
511
+ status_code=response.status_code,
512
+ )
513
+
514
+ if self.logger.is_error():
515
+ if response.status_code >= 400:
516
+ self.logger.error(
517
+ "HTTP request failed with error status",
518
+ method=method,
519
+ url=_request_url,
520
+ status_code=response.status_code,
521
+ )
522
+
523
+ return response
524
+
525
+ @contextmanager
526
+ def stream(
527
+ self,
528
+ path: typing.Optional[str] = None,
529
+ *,
530
+ method: str,
531
+ base_url: typing.Optional[str] = None,
532
+ params: typing.Optional[typing.Dict[str, typing.Any]] = None,
533
+ json: typing.Optional[typing.Any] = None,
534
+ data: typing.Optional[typing.Any] = None,
535
+ content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None,
536
+ files: typing.Optional[
537
+ typing.Union[
538
+ typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]],
539
+ typing.List[typing.Tuple[str, File]],
540
+ ]
541
+ ] = None,
542
+ headers: typing.Optional[typing.Dict[str, typing.Any]] = None,
543
+ request_options: typing.Optional[RequestOptions] = None,
544
+ retries: int = 0,
545
+ omit: typing.Optional[typing.Any] = None,
546
+ optional_body: bool = False,
547
+ force_multipart: typing.Optional[bool] = None,
548
+ ) -> typing.Iterator[httpx.Response]:
549
+ base_url = self.get_base_url(base_url)
550
+ _timeout = (
551
+ request_options.get("timeout")
552
+ if request_options is not None and request_options.get("timeout") is not None
553
+ else request_options.get("timeout_in_seconds")
554
+ if request_options is not None and request_options.get("timeout_in_seconds") is not None
555
+ else self.base_timeout()
556
+ )
557
+ timeout = _timeout if _timeout is not None else httpx.USE_CLIENT_DEFAULT
558
+
559
+ request_files: typing.Optional[RequestFiles] = (
560
+ convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit))
561
+ if (files is not None and files is not omit and isinstance(files, dict))
562
+ else None
563
+ )
564
+
565
+ if (request_files is None or len(request_files) == 0) and force_multipart:
566
+ request_files = FORCE_MULTIPART
567
+
568
+ json_body, data_body = get_request_body(
569
+ json=json, data=data, request_options=request_options, omit=omit, optional_body=optional_body
570
+ )
571
+
572
+ data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart)
573
+
574
+ # Compute encoded params separately to avoid passing empty list to httpx
575
+ # (httpx strips existing query params from URL when params=[] is passed)
576
+ _encoded_params = encode_query(
577
+ jsonable_encoder(
578
+ remove_none_from_dict(
579
+ remove_omit_from_dict(
580
+ {
581
+ **(params if params is not None else {}),
582
+ **(
583
+ request_options.get("additional_query_parameters", {})
584
+ if request_options is not None
585
+ else {}
586
+ ),
587
+ },
588
+ omit,
589
+ )
590
+ )
591
+ )
592
+ )
593
+
594
+ _request_url = _build_url(base_url, path)
595
+ _request_headers = jsonable_encoder(
596
+ remove_none_from_dict(
597
+ {
598
+ **self.base_headers(),
599
+ **(headers if headers is not None else {}),
600
+ **(request_options.get("additional_headers", {}) if request_options is not None else {}),
601
+ }
602
+ )
603
+ )
604
+ _request_headers = drop_content_type_without_body(
605
+ _request_headers, json_body=json_body, data_body=data_body, optional_body=optional_body
606
+ )
607
+
608
+ if self.logger.is_debug():
609
+ self.logger.debug(
610
+ "Making streaming HTTP request",
611
+ method=method,
612
+ url=_request_url,
613
+ headers=_redact_headers(_request_headers),
614
+ )
615
+
616
+ with self.httpx_client.stream(
617
+ method=method,
618
+ url=_request_url,
619
+ headers=_request_headers,
620
+ params=_encoded_params if _encoded_params else None,
621
+ json=json_body,
622
+ data=data_body,
623
+ content=content,
624
+ files=request_files,
625
+ timeout=timeout,
626
+ ) as stream:
627
+ yield stream
628
+
629
+
630
+ class AsyncHttpClient:
631
+ def __init__(
632
+ self,
633
+ *,
634
+ httpx_client: httpx.AsyncClient,
635
+ base_timeout: typing.Callable[[], typing.Optional[float]],
636
+ base_headers: typing.Callable[[], typing.Dict[str, str]],
637
+ base_url: typing.Optional[typing.Callable[[], str]] = None,
638
+ base_max_retries: int = 2,
639
+ async_base_headers: typing.Optional[typing.Callable[[], typing.Awaitable[typing.Dict[str, str]]]] = None,
640
+ logging_config: typing.Optional[typing.Union[LogConfig, Logger]] = None,
641
+ ):
642
+ self.base_url = base_url
643
+ self.base_timeout = base_timeout
644
+ self.base_headers = base_headers
645
+ self.base_max_retries = base_max_retries
646
+ self.async_base_headers = async_base_headers
647
+ self.httpx_client = httpx_client
648
+ self.logger = create_logger(logging_config)
649
+
650
+ async def _get_headers(self) -> typing.Dict[str, str]:
651
+ if self.async_base_headers is not None:
652
+ return await self.async_base_headers()
653
+ return self.base_headers()
654
+
655
+ def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str:
656
+ base_url = maybe_base_url
657
+ if self.base_url is not None and base_url is None:
658
+ base_url = self.base_url()
659
+
660
+ if base_url is None:
661
+ raise ValueError("A base_url is required to make this request, please provide one and try again.")
662
+ return base_url
663
+
664
+ async def request(
665
+ self,
666
+ path: typing.Optional[str] = None,
667
+ *,
668
+ method: str,
669
+ base_url: typing.Optional[str] = None,
670
+ params: typing.Optional[typing.Dict[str, typing.Any]] = None,
671
+ json: typing.Optional[typing.Any] = None,
672
+ data: typing.Optional[typing.Any] = None,
673
+ content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None,
674
+ files: typing.Optional[
675
+ typing.Union[
676
+ typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]],
677
+ typing.List[typing.Tuple[str, File]],
678
+ ]
679
+ ] = None,
680
+ headers: typing.Optional[typing.Dict[str, typing.Any]] = None,
681
+ request_options: typing.Optional[RequestOptions] = None,
682
+ retries: int = 0,
683
+ omit: typing.Optional[typing.Any] = None,
684
+ optional_body: bool = False,
685
+ force_multipart: typing.Optional[bool] = None,
686
+ ) -> httpx.Response:
687
+ base_url = self.get_base_url(base_url)
688
+ _timeout = (
689
+ request_options.get("timeout")
690
+ if request_options is not None and request_options.get("timeout") is not None
691
+ else request_options.get("timeout_in_seconds")
692
+ if request_options is not None and request_options.get("timeout_in_seconds") is not None
693
+ else self.base_timeout()
694
+ )
695
+ timeout = _timeout if _timeout is not None else httpx.USE_CLIENT_DEFAULT
696
+
697
+ request_files: typing.Optional[RequestFiles] = (
698
+ convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit))
699
+ if (files is not None and files is not omit and isinstance(files, dict))
700
+ else None
701
+ )
702
+
703
+ if (request_files is None or len(request_files) == 0) and force_multipart:
704
+ request_files = FORCE_MULTIPART
705
+
706
+ json_body, data_body = get_request_body(
707
+ json=json, data=data, request_options=request_options, omit=omit, optional_body=optional_body
708
+ )
709
+
710
+ data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart)
711
+
712
+ # Get headers (supports async token providers)
713
+ _headers = await self._get_headers()
714
+
715
+ # Compute encoded params separately to avoid passing empty list to httpx
716
+ # (httpx strips existing query params from URL when params=[] is passed)
717
+ _encoded_params = encode_query(
718
+ jsonable_encoder(
719
+ remove_none_from_dict(
720
+ remove_omit_from_dict(
721
+ {
722
+ **(params if params is not None else {}),
723
+ **(
724
+ request_options.get("additional_query_parameters", {}) or {}
725
+ if request_options is not None
726
+ else {}
727
+ ),
728
+ },
729
+ omit,
730
+ )
731
+ )
732
+ )
733
+ )
734
+
735
+ _request_url = _build_url(base_url, path)
736
+ _request_headers = jsonable_encoder(
737
+ remove_none_from_dict(
738
+ {
739
+ **_headers,
740
+ **(headers if headers is not None else {}),
741
+ **(request_options.get("additional_headers", {}) or {} if request_options is not None else {}),
742
+ }
743
+ )
744
+ )
745
+ _request_headers = drop_content_type_without_body(
746
+ _request_headers, json_body=json_body, data_body=data_body, optional_body=optional_body
747
+ )
748
+
749
+ if self.logger.is_debug():
750
+ self.logger.debug(
751
+ "Making HTTP request",
752
+ method=method,
753
+ url=_request_url,
754
+ headers=_redact_headers(_request_headers),
755
+ has_body=json_body is not None or data_body is not None,
756
+ )
757
+
758
+ max_retries: int = (
759
+ request_options.get("max_retries", self.base_max_retries)
760
+ if request_options is not None
761
+ else self.base_max_retries
762
+ )
763
+
764
+ try:
765
+ response = await self.httpx_client.request(
766
+ method=method,
767
+ url=_request_url,
768
+ headers=_request_headers,
769
+ params=_encoded_params if _encoded_params else None,
770
+ json=json_body,
771
+ data=data_body,
772
+ content=content,
773
+ files=request_files,
774
+ timeout=timeout,
775
+ )
776
+ except (httpx.ConnectError, httpx.RemoteProtocolError):
777
+ if retries < max_retries:
778
+ await asyncio.sleep(_retry_timeout_from_retries(retries=retries))
779
+ return await self.request(
780
+ path=path,
781
+ method=method,
782
+ base_url=base_url,
783
+ params=params,
784
+ json=json,
785
+ data=data,
786
+ content=content,
787
+ files=files,
788
+ headers=headers,
789
+ request_options=request_options,
790
+ retries=retries + 1,
791
+ omit=omit,
792
+ force_multipart=force_multipart,
793
+ )
794
+ raise
795
+
796
+ if _should_retry(response=response):
797
+ if retries < max_retries:
798
+ await asyncio.sleep(_retry_timeout(response=response, retries=retries))
799
+ return await self.request(
800
+ path=path,
801
+ method=method,
802
+ base_url=base_url,
803
+ params=params,
804
+ json=json,
805
+ data=data,
806
+ content=content,
807
+ files=files,
808
+ headers=headers,
809
+ request_options=request_options,
810
+ retries=retries + 1,
811
+ omit=omit,
812
+ force_multipart=force_multipart,
813
+ )
814
+
815
+ if self.logger.is_debug():
816
+ if 200 <= response.status_code < 400:
817
+ self.logger.debug(
818
+ "HTTP request succeeded",
819
+ method=method,
820
+ url=_request_url,
821
+ status_code=response.status_code,
822
+ )
823
+
824
+ if self.logger.is_error():
825
+ if response.status_code >= 400:
826
+ self.logger.error(
827
+ "HTTP request failed with error status",
828
+ method=method,
829
+ url=_request_url,
830
+ status_code=response.status_code,
831
+ )
832
+
833
+ return response
834
+
835
+ @asynccontextmanager
836
+ async def stream(
837
+ self,
838
+ path: typing.Optional[str] = None,
839
+ *,
840
+ method: str,
841
+ base_url: typing.Optional[str] = None,
842
+ params: typing.Optional[typing.Dict[str, typing.Any]] = None,
843
+ json: typing.Optional[typing.Any] = None,
844
+ data: typing.Optional[typing.Any] = None,
845
+ content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None,
846
+ files: typing.Optional[
847
+ typing.Union[
848
+ typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]],
849
+ typing.List[typing.Tuple[str, File]],
850
+ ]
851
+ ] = None,
852
+ headers: typing.Optional[typing.Dict[str, typing.Any]] = None,
853
+ request_options: typing.Optional[RequestOptions] = None,
854
+ retries: int = 0,
855
+ omit: typing.Optional[typing.Any] = None,
856
+ optional_body: bool = False,
857
+ force_multipart: typing.Optional[bool] = None,
858
+ ) -> typing.AsyncIterator[httpx.Response]:
859
+ base_url = self.get_base_url(base_url)
860
+ _timeout = (
861
+ request_options.get("timeout")
862
+ if request_options is not None and request_options.get("timeout") is not None
863
+ else request_options.get("timeout_in_seconds")
864
+ if request_options is not None and request_options.get("timeout_in_seconds") is not None
865
+ else self.base_timeout()
866
+ )
867
+ timeout = _timeout if _timeout is not None else httpx.USE_CLIENT_DEFAULT
868
+
869
+ request_files: typing.Optional[RequestFiles] = (
870
+ convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit))
871
+ if (files is not None and files is not omit and isinstance(files, dict))
872
+ else None
873
+ )
874
+
875
+ if (request_files is None or len(request_files) == 0) and force_multipart:
876
+ request_files = FORCE_MULTIPART
877
+
878
+ json_body, data_body = get_request_body(
879
+ json=json, data=data, request_options=request_options, omit=omit, optional_body=optional_body
880
+ )
881
+
882
+ data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart)
883
+
884
+ # Get headers (supports async token providers)
885
+ _headers = await self._get_headers()
886
+
887
+ # Compute encoded params separately to avoid passing empty list to httpx
888
+ # (httpx strips existing query params from URL when params=[] is passed)
889
+ _encoded_params = encode_query(
890
+ jsonable_encoder(
891
+ remove_none_from_dict(
892
+ remove_omit_from_dict(
893
+ {
894
+ **(params if params is not None else {}),
895
+ **(
896
+ request_options.get("additional_query_parameters", {})
897
+ if request_options is not None
898
+ else {}
899
+ ),
900
+ },
901
+ omit=omit,
902
+ )
903
+ )
904
+ )
905
+ )
906
+
907
+ _request_url = _build_url(base_url, path)
908
+ _request_headers = jsonable_encoder(
909
+ remove_none_from_dict(
910
+ {
911
+ **_headers,
912
+ **(headers if headers is not None else {}),
913
+ **(request_options.get("additional_headers", {}) if request_options is not None else {}),
914
+ }
915
+ )
916
+ )
917
+ _request_headers = drop_content_type_without_body(
918
+ _request_headers, json_body=json_body, data_body=data_body, optional_body=optional_body
919
+ )
920
+
921
+ if self.logger.is_debug():
922
+ self.logger.debug(
923
+ "Making streaming HTTP request",
924
+ method=method,
925
+ url=_request_url,
926
+ headers=_redact_headers(_request_headers),
927
+ )
928
+
929
+ async with self.httpx_client.stream(
930
+ method=method,
931
+ url=_request_url,
932
+ headers=_request_headers,
933
+ params=_encoded_params if _encoded_params else None,
934
+ json=json_body,
935
+ data=data_body,
936
+ content=content,
937
+ files=request_files,
938
+ timeout=timeout,
939
+ ) as stream:
940
+ yield stream