airbyte_cdk.sources.declarative.requesters
1# 2# Copyright (c) 2023 Airbyte, Inc., all rights reserved. 3# 4 5from airbyte_cdk.sources.declarative.requesters.http_requester import HttpRequester 6from airbyte_cdk.sources.declarative.requesters.request_option import RequestOption 7from airbyte_cdk.sources.declarative.requesters.requester import Requester 8 9__all__ = ["HttpRequester", "RequestOption", "Requester"]
37@dataclass 38class HttpRequester(Requester): 39 """ 40 Default implementation of a Requester 41 42 Attributes: 43 name (str): Name of the stream. Only used for request/response caching 44 url_base (Union[InterpolatedString, str]): Base url to send requests to 45 path (Union[InterpolatedString, str]): Path to send requests to 46 http_method (Union[str, HttpMethod]): HTTP method to use when sending requests 47 request_options_provider (Optional[InterpolatedRequestOptionsProvider]): request option provider defining the options to set on outgoing requests 48 authenticator (DeclarativeAuthenticator): Authenticator defining how to authenticate to the source 49 error_handler (Optional[ErrorHandler]): Error handler defining how to detect and handle errors 50 backoff_strategies (Optional[List[BackoffStrategy]]): List of backoff strategies to use when retrying requests 51 config (Config): The user-provided configuration as specified by the source's spec 52 use_cache (bool): Indicates that data should be cached for this stream 53 """ 54 55 name: str 56 config: Config 57 parameters: InitVar[Mapping[str, Any]] 58 59 url: Optional[Union[InterpolatedString, str]] = None 60 url_base: Optional[Union[InterpolatedString, str]] = None 61 path: Optional[Union[InterpolatedString, str]] = None 62 authenticator: Optional[DeclarativeAuthenticator] = None 63 http_method: Union[str, HttpMethod] = HttpMethod.GET 64 request_options_provider: Optional[InterpolatedRequestOptionsProvider] = None 65 error_handler: Optional[ErrorHandler] = None 66 api_budget: Optional[APIBudget] = None 67 disable_retries: bool = False 68 message_repository: MessageRepository = NoopMessageRepository() 69 use_cache: bool = False 70 connect_timeout_in_seconds: Optional[float] = None 71 read_timeout_in_seconds: Optional[float] = None 72 use_tcp_keepalive: bool = False 73 _exit_on_rate_limit: bool = False 74 stream_response: bool = False 75 decoder: Decoder = field(default_factory=lambda: JsonDecoder(parameters={})) 76 77 def __post_init__(self, parameters: Mapping[str, Any]) -> None: 78 self._url = InterpolatedString.create( 79 self.url if self.url else EmptyString, parameters=parameters 80 ) 81 # deprecated 82 self._url_base = InterpolatedString.create( 83 self.url_base if self.url_base else EmptyString, parameters=parameters 84 ) 85 # deprecated 86 self._path = InterpolatedString.create( 87 self.path if self.path else EmptyString, parameters=parameters 88 ) 89 if self.request_options_provider is None: 90 self._request_options_provider = InterpolatedRequestOptionsProvider( 91 config=self.config, parameters=parameters 92 ) 93 elif isinstance(self.request_options_provider, dict): 94 self._request_options_provider = InterpolatedRequestOptionsProvider( 95 config=self.config, **self.request_options_provider 96 ) 97 else: 98 self._request_options_provider = self.request_options_provider 99 self._authenticator = self.authenticator or NoAuth(parameters=parameters) 100 self._http_method = ( 101 HttpMethod[self.http_method] if isinstance(self.http_method, str) else self.http_method 102 ) 103 self.error_handler = self.error_handler 104 self._parameters = parameters 105 106 if self.error_handler is not None and hasattr(self.error_handler, "backoff_strategies"): 107 backoff_strategies = self.error_handler.backoff_strategies # type: ignore 108 else: 109 backoff_strategies = None 110 111 self._http_client = HttpClient( 112 name=self.name, 113 logger=self.logger, 114 error_handler=self.error_handler, 115 api_budget=self.api_budget, 116 authenticator=self._authenticator, 117 use_cache=self.use_cache, 118 backoff_strategy=backoff_strategies, 119 disable_retries=self.disable_retries, 120 message_repository=self.message_repository, 121 request_timeout=( 122 (self.connect_timeout_in_seconds, self.read_timeout_in_seconds) 123 if self.connect_timeout_in_seconds is not None 124 or self.read_timeout_in_seconds is not None 125 else None 126 ), 127 use_tcp_keepalive=self.use_tcp_keepalive, 128 ) 129 130 @property 131 def exit_on_rate_limit(self) -> bool: 132 return self._exit_on_rate_limit 133 134 @exit_on_rate_limit.setter 135 def exit_on_rate_limit(self, value: bool) -> None: 136 self._exit_on_rate_limit = value 137 138 def get_authenticator(self) -> DeclarativeAuthenticator: 139 return self._authenticator 140 141 def get_url( 142 self, 143 *, 144 stream_state: Optional[StreamState] = None, 145 stream_slice: Optional[StreamSlice] = None, 146 next_page_token: Optional[Mapping[str, Any]] = None, 147 ) -> str: 148 interpolation_context = get_interpolation_context( 149 stream_state=stream_state, 150 stream_slice=stream_slice, 151 next_page_token=next_page_token, 152 ) 153 154 return str(self._url.eval(self.config, **interpolation_context)) 155 156 def _get_url( 157 self, 158 *, 159 path: Optional[str] = None, 160 stream_state: Optional[StreamState] = None, 161 stream_slice: Optional[StreamSlice] = None, 162 next_page_token: Optional[Mapping[str, Any]] = None, 163 ) -> str: 164 url = self.get_url( 165 stream_state=stream_state, 166 stream_slice=stream_slice, 167 next_page_token=next_page_token, 168 ) 169 170 url_base = self.get_url_base( 171 stream_state=stream_state, 172 stream_slice=stream_slice, 173 next_page_token=next_page_token, 174 ) 175 176 path = path or self.get_path( 177 stream_state=stream_state, 178 stream_slice=stream_slice, 179 next_page_token=next_page_token, 180 ) 181 182 full_url = ( 183 self._join_url(url_base, path) 184 if url_base 185 else self._join_url(url, path) 186 if path 187 else url 188 ) 189 190 return full_url 191 192 def get_url_base( 193 self, 194 *, 195 stream_state: Optional[StreamState] = None, 196 stream_slice: Optional[StreamSlice] = None, 197 next_page_token: Optional[Mapping[str, Any]] = None, 198 ) -> str: 199 interpolation_context = get_interpolation_context( 200 stream_state=stream_state, 201 stream_slice=stream_slice, 202 next_page_token=next_page_token, 203 ) 204 return str(self._url_base.eval(self.config, **interpolation_context)) 205 206 def get_path( 207 self, 208 *, 209 stream_state: Optional[StreamState] = None, 210 stream_slice: Optional[StreamSlice] = None, 211 next_page_token: Optional[Mapping[str, Any]] = None, 212 ) -> str: 213 interpolation_context = get_interpolation_context( 214 stream_state=stream_state, 215 stream_slice=stream_slice, 216 next_page_token=next_page_token, 217 ) 218 path = str(self._path.eval(self.config, **interpolation_context)) 219 return path.lstrip("/") 220 221 def get_method(self) -> HttpMethod: 222 return self._http_method 223 224 def get_request_params( 225 self, 226 *, 227 stream_state: Optional[StreamState] = None, 228 stream_slice: Optional[StreamSlice] = None, 229 next_page_token: Optional[Mapping[str, Any]] = None, 230 ) -> MutableMapping[str, Any]: 231 return self._request_options_provider.get_request_params( 232 stream_state=stream_state, 233 stream_slice=stream_slice, 234 next_page_token=next_page_token, 235 ) 236 237 def get_request_headers( 238 self, 239 *, 240 stream_state: Optional[StreamState] = None, 241 stream_slice: Optional[StreamSlice] = None, 242 next_page_token: Optional[Mapping[str, Any]] = None, 243 ) -> Mapping[str, Any]: 244 return self._request_options_provider.get_request_headers( 245 stream_state=stream_state, 246 stream_slice=stream_slice, 247 next_page_token=next_page_token, 248 ) 249 250 # fixing request options provider types has a lot of dependencies 251 def get_request_body_data( # type: ignore 252 self, 253 *, 254 stream_state: Optional[StreamState] = None, 255 stream_slice: Optional[StreamSlice] = None, 256 next_page_token: Optional[Mapping[str, Any]] = None, 257 ) -> Union[Mapping[str, Any], str]: 258 return ( 259 self._request_options_provider.get_request_body_data( 260 stream_state=stream_state, 261 stream_slice=stream_slice, 262 next_page_token=next_page_token, 263 ) 264 or {} 265 ) 266 267 # fixing request options provider types has a lot of dependencies 268 def get_request_body_json( # type: ignore 269 self, 270 *, 271 stream_state: Optional[StreamState] = None, 272 stream_slice: Optional[StreamSlice] = None, 273 next_page_token: Optional[Mapping[str, Any]] = None, 274 ) -> Optional[Mapping[str, Any]]: 275 return self._request_options_provider.get_request_body_json( 276 stream_state=stream_state, 277 stream_slice=stream_slice, 278 next_page_token=next_page_token, 279 ) 280 281 @property 282 def logger(self) -> logging.Logger: 283 return logging.getLogger(f"airbyte.HttpRequester.{self.name}") 284 285 def _get_request_options( 286 self, 287 stream_state: Optional[StreamState], 288 stream_slice: Optional[StreamSlice], 289 next_page_token: Optional[Mapping[str, Any]], 290 requester_method: Callable[..., Optional[Union[Mapping[str, Any], str]]], 291 auth_options_method: Callable[..., Optional[Union[Mapping[str, Any], str]]], 292 extra_options: Optional[Union[Mapping[str, Any], str]] = None, 293 ) -> Union[Mapping[str, Any], str]: 294 """ 295 Get the request_option from the requester, the authenticator and extra_options passed in. 296 Raise a ValueError if there's a key collision 297 Returned merged mapping otherwise 298 """ 299 300 is_body_json = requester_method.__name__ == "get_request_body_json" 301 302 return combine_mappings( 303 [ 304 requester_method( 305 stream_state=stream_state, 306 stream_slice=stream_slice, 307 next_page_token=next_page_token, 308 ), 309 auth_options_method(), 310 extra_options, 311 ], 312 allow_same_value_merge=is_body_json, 313 ) 314 315 def _request_headers( 316 self, 317 stream_state: Optional[StreamState] = None, 318 stream_slice: Optional[StreamSlice] = None, 319 next_page_token: Optional[Mapping[str, Any]] = None, 320 extra_headers: Optional[Mapping[str, Any]] = None, 321 ) -> Mapping[str, Any]: 322 """ 323 Specifies request headers. 324 Authentication headers will overwrite any overlapping headers returned from this method. 325 """ 326 headers = self._get_request_options( 327 stream_state, 328 stream_slice, 329 next_page_token, 330 self.get_request_headers, 331 self.get_authenticator().get_auth_header, 332 extra_headers, 333 ) 334 if isinstance(headers, str): 335 raise ValueError("Request headers cannot be a string") 336 return {str(k): str(v) for k, v in headers.items()} 337 338 def _request_params( 339 self, 340 stream_state: Optional[StreamState], 341 stream_slice: Optional[StreamSlice], 342 next_page_token: Optional[Mapping[str, Any]], 343 extra_params: Optional[Mapping[str, Any]] = None, 344 ) -> Mapping[str, Any]: 345 """ 346 Specifies the query parameters that should be set on an outgoing HTTP request given the inputs. 347 348 E.g: you might want to define query parameters for paging if next_page_token is not None. 349 """ 350 options = self._get_request_options( 351 stream_state, 352 stream_slice, 353 next_page_token, 354 self.get_request_params, 355 self.get_authenticator().get_request_params, 356 extra_params, 357 ) 358 if isinstance(options, str): 359 raise ValueError("Request params cannot be a string") 360 361 for k, v in options.items(): 362 if isinstance(v, (dict,)): 363 raise ValueError( 364 f"Invalid value for `{k}` parameter. The values of request params cannot be an object." 365 ) 366 367 return options 368 369 def _request_body_data( 370 self, 371 stream_state: Optional[StreamState], 372 stream_slice: Optional[StreamSlice], 373 next_page_token: Optional[Mapping[str, Any]], 374 extra_body_data: Optional[Union[Mapping[str, Any], str]] = None, 375 ) -> Optional[Union[Mapping[str, Any], str]]: 376 """ 377 Specifies how to populate the body of the request with a non-JSON payload. 378 379 If returns a ready text that it will be sent as is. 380 If returns a dict that it will be converted to a urlencoded form. 381 E.g. {"key1": "value1", "key2": "value2"} => "key1=value1&key2=value2" 382 383 At the same time only one of the 'request_body_data' and 'request_body_json' functions can be overridden. 384 """ 385 # Warning: use self.state instead of the stream_state passed as argument! 386 return self._get_request_options( 387 stream_state, 388 stream_slice, 389 next_page_token, 390 self.get_request_body_data, 391 self.get_authenticator().get_request_body_data, 392 extra_body_data, 393 ) 394 395 def _request_body_json( 396 self, 397 stream_state: Optional[StreamState], 398 stream_slice: Optional[StreamSlice], 399 next_page_token: Optional[Mapping[str, Any]], 400 extra_body_json: Optional[Mapping[str, Any]] = None, 401 ) -> Optional[Mapping[str, Any]]: 402 """ 403 Specifies how to populate the body of the request with a JSON payload. 404 405 At the same time only one of the 'request_body_data' and 'request_body_json' functions can be overridden. 406 """ 407 # Warning: use self.state instead of the stream_state passed as argument! 408 options = self._get_request_options( 409 stream_state, 410 stream_slice, 411 next_page_token, 412 self.get_request_body_json, 413 self.get_authenticator().get_request_body_json, 414 extra_body_json, 415 ) 416 if isinstance(options, str): 417 raise ValueError("Request body json cannot be a string") 418 return options 419 420 @classmethod 421 def _join_url(cls, url_base: str, path: Optional[str] = None) -> str: 422 """ 423 Joins a base URL with a given path and returns the resulting URL with any trailing slash removed. 424 425 This method ensures that there are no duplicate slashes when concatenating the base URL and the path, 426 which is useful when the full URL is provided from an interpolation context. 427 428 Args: 429 url_base (str): The base URL to which the path will be appended. 430 path (Optional[str]): The path to join with the base URL. 431 432 Returns: 433 str: The resulting joined URL. 434 435 Note: 436 Related issue: https://github.com/airbytehq/airbyte-internal-issues/issues/11869 437 - If the path is an empty string or None, the method returns the base URL with any trailing slash removed. 438 439 Example: 440 1) _join_url("https://example.com/api/", "endpoint") >> 'https://example.com/api/endpoint' 441 2) _join_url("https://example.com/api", "/endpoint") >> 'https://example.com/api/endpoint' 442 3) _join_url("https://example.com/api/", "") >> 'https://example.com/api/' 443 4) _join_url("https://example.com/api", None) >> 'https://example.com/api' 444 """ 445 446 # return a full-url if provided directly from interpolation context 447 if path == EmptyString or path is None: 448 return url_base 449 else: 450 # since we didn't provide a full-url, the url_base might not have a trailing slash 451 # so we join the url_base and path correctly 452 if not url_base.endswith("/"): 453 url_base += "/" 454 455 return urljoin(url_base, path) 456 457 def send_request( 458 self, 459 stream_state: Optional[StreamState] = None, 460 stream_slice: Optional[StreamSlice] = None, 461 next_page_token: Optional[Mapping[str, Any]] = None, 462 path: Optional[str] = None, 463 request_headers: Optional[Mapping[str, Any]] = None, 464 request_params: Optional[Mapping[str, Any]] = None, 465 request_body_data: Optional[Union[Mapping[str, Any], str]] = None, 466 request_body_json: Optional[Mapping[str, Any]] = None, 467 log_formatter: Optional[Callable[[requests.Response], Any]] = None, 468 ) -> Optional[requests.Response]: 469 request, response = self._http_client.send_request( 470 http_method=self.get_method().value, 471 url=self._get_url( 472 path=path, 473 stream_state=stream_state, 474 stream_slice=stream_slice, 475 next_page_token=next_page_token, 476 ), 477 request_kwargs={"stream": self.stream_response}, 478 headers=self._request_headers( 479 stream_state, stream_slice, next_page_token, request_headers 480 ), 481 params=self._request_params( 482 stream_state, stream_slice, next_page_token, request_params 483 ), 484 json=self._request_body_json( 485 stream_state, stream_slice, next_page_token, request_body_json 486 ), 487 data=self._request_body_data( 488 stream_state, stream_slice, next_page_token, request_body_data 489 ), 490 dedupe_query_params=True, 491 log_formatter=log_formatter, 492 exit_on_rate_limit=self._exit_on_rate_limit, 493 ) 494 495 return response
Default implementation of a Requester
Attributes:
- name (str): Name of the stream. Only used for request/response caching
- url_base (Union[InterpolatedString, str]): Base url to send requests to
- path (Union[InterpolatedString, str]): Path to send requests to
- http_method (Union[str, HttpMethod]): HTTP method to use when sending requests
- request_options_provider (Optional[InterpolatedRequestOptionsProvider]): request option provider defining the options to set on outgoing requests
- authenticator (DeclarativeAuthenticator): Authenticator defining how to authenticate to the source
- error_handler (Optional[ErrorHandler]): Error handler defining how to detect and handle errors
- backoff_strategies (Optional[List[BackoffStrategy]]): List of backoff strategies to use when retrying requests
- config (Config): The user-provided configuration as specified by the source's spec
- use_cache (bool): Indicates that data should be cached for this stream
141 def get_url( 142 self, 143 *, 144 stream_state: Optional[StreamState] = None, 145 stream_slice: Optional[StreamSlice] = None, 146 next_page_token: Optional[Mapping[str, Any]] = None, 147 ) -> str: 148 interpolation_context = get_interpolation_context( 149 stream_state=stream_state, 150 stream_slice=stream_slice, 151 next_page_token=next_page_token, 152 ) 153 154 return str(self._url.eval(self.config, **interpolation_context))
Returns
URL base for the API endpoint e.g: if you wanted to hit https://myapi.com/v1/some_entity then this should return "https://myapi.com/v1/"
192 def get_url_base( 193 self, 194 *, 195 stream_state: Optional[StreamState] = None, 196 stream_slice: Optional[StreamSlice] = None, 197 next_page_token: Optional[Mapping[str, Any]] = None, 198 ) -> str: 199 interpolation_context = get_interpolation_context( 200 stream_state=stream_state, 201 stream_slice=stream_slice, 202 next_page_token=next_page_token, 203 ) 204 return str(self._url_base.eval(self.config, **interpolation_context))
Returns
URL base for the API endpoint e.g: if you wanted to hit https://myapi.com/v1/some_entity then this should return "https://myapi.com/v1/"
206 def get_path( 207 self, 208 *, 209 stream_state: Optional[StreamState] = None, 210 stream_slice: Optional[StreamSlice] = None, 211 next_page_token: Optional[Mapping[str, Any]] = None, 212 ) -> str: 213 interpolation_context = get_interpolation_context( 214 stream_state=stream_state, 215 stream_slice=stream_slice, 216 next_page_token=next_page_token, 217 ) 218 path = str(self._path.eval(self.config, **interpolation_context)) 219 return path.lstrip("/")
Returns the URL path for the API endpoint e.g: if you wanted to hit https://myapi.com/v1/some_entity then this should return "some_entity"
224 def get_request_params( 225 self, 226 *, 227 stream_state: Optional[StreamState] = None, 228 stream_slice: Optional[StreamSlice] = None, 229 next_page_token: Optional[Mapping[str, Any]] = None, 230 ) -> MutableMapping[str, Any]: 231 return self._request_options_provider.get_request_params( 232 stream_state=stream_state, 233 stream_slice=stream_slice, 234 next_page_token=next_page_token, 235 )
Specifies the query parameters that should be set on an outgoing HTTP request given the inputs.
E.g: you might want to define query parameters for paging if next_page_token is not None.
237 def get_request_headers( 238 self, 239 *, 240 stream_state: Optional[StreamState] = None, 241 stream_slice: Optional[StreamSlice] = None, 242 next_page_token: Optional[Mapping[str, Any]] = None, 243 ) -> Mapping[str, Any]: 244 return self._request_options_provider.get_request_headers( 245 stream_state=stream_state, 246 stream_slice=stream_slice, 247 next_page_token=next_page_token, 248 )
Return any non-auth headers. Authentication headers will overwrite any overlapping headers returned from this method.
251 def get_request_body_data( # type: ignore 252 self, 253 *, 254 stream_state: Optional[StreamState] = None, 255 stream_slice: Optional[StreamSlice] = None, 256 next_page_token: Optional[Mapping[str, Any]] = None, 257 ) -> Union[Mapping[str, Any], str]: 258 return ( 259 self._request_options_provider.get_request_body_data( 260 stream_state=stream_state, 261 stream_slice=stream_slice, 262 next_page_token=next_page_token, 263 ) 264 or {} 265 )
Specifies how to populate the body of the request with a non-JSON payload.
If returns a ready text that it will be sent as is. If returns a dict that it will be converted to a urlencoded form. E.g. {"key1": "value1", "key2": "value2"} => "key1=value1&key2=value2"
At the same time only one of the 'request_body_data' and 'request_body_json' functions can be overridden.
268 def get_request_body_json( # type: ignore 269 self, 270 *, 271 stream_state: Optional[StreamState] = None, 272 stream_slice: Optional[StreamSlice] = None, 273 next_page_token: Optional[Mapping[str, Any]] = None, 274 ) -> Optional[Mapping[str, Any]]: 275 return self._request_options_provider.get_request_body_json( 276 stream_state=stream_state, 277 stream_slice=stream_slice, 278 next_page_token=next_page_token, 279 )
Specifies how to populate the body of the request with a JSON payload.
At the same time only one of the 'request_body_data' and 'request_body_json' functions can be overridden.
457 def send_request( 458 self, 459 stream_state: Optional[StreamState] = None, 460 stream_slice: Optional[StreamSlice] = None, 461 next_page_token: Optional[Mapping[str, Any]] = None, 462 path: Optional[str] = None, 463 request_headers: Optional[Mapping[str, Any]] = None, 464 request_params: Optional[Mapping[str, Any]] = None, 465 request_body_data: Optional[Union[Mapping[str, Any], str]] = None, 466 request_body_json: Optional[Mapping[str, Any]] = None, 467 log_formatter: Optional[Callable[[requests.Response], Any]] = None, 468 ) -> Optional[requests.Response]: 469 request, response = self._http_client.send_request( 470 http_method=self.get_method().value, 471 url=self._get_url( 472 path=path, 473 stream_state=stream_state, 474 stream_slice=stream_slice, 475 next_page_token=next_page_token, 476 ), 477 request_kwargs={"stream": self.stream_response}, 478 headers=self._request_headers( 479 stream_state, stream_slice, next_page_token, request_headers 480 ), 481 params=self._request_params( 482 stream_state, stream_slice, next_page_token, request_params 483 ), 484 json=self._request_body_json( 485 stream_state, stream_slice, next_page_token, request_body_json 486 ), 487 data=self._request_body_data( 488 stream_state, stream_slice, next_page_token, request_body_data 489 ), 490 dedupe_query_params=True, 491 log_formatter=log_formatter, 492 exit_on_rate_limit=self._exit_on_rate_limit, 493 ) 494 495 return response
Sends a request and returns the response. Might return no response if the error handler chooses to ignore the response or throw an exception in case of an error. If path is set, the path configured on the requester itself is ignored. If header, params and body are set, they are merged with the ones configured on the requester itself.
If a log formatter is provided, it's used to log the performed request and response. If it's not provided, no logging is performed.
25@dataclass 26class RequestOption: 27 """ 28 Describes an option to set on a request 29 30 Attributes: 31 field_name (str): Describes the name of the parameter to inject. Mutually exclusive with field_path. 32 field_path (list(str)): Describes the path to a nested field as a list of field names. 33 Only valid for body_json injection type, and mutually exclusive with field_name. 34 inject_into (RequestOptionType): Describes where in the HTTP request to inject the parameter 35 """ 36 37 inject_into: RequestOptionType 38 parameters: InitVar[Mapping[str, Any]] 39 field_name: Optional[Union[InterpolatedString, str]] = None 40 field_path: Optional[List[Union[InterpolatedString, str]]] = None 41 42 def __post_init__(self, parameters: Mapping[str, Any]) -> None: 43 # Validate inputs. We should expect either field_name or field_path, but not both 44 if self.field_name is None and self.field_path is None: 45 raise ValueError("RequestOption requires either a field_name or field_path") 46 47 if self.field_name is not None and self.field_path is not None: 48 raise ValueError( 49 "Only one of field_name or field_path can be provided to RequestOption" 50 ) 51 52 # Nested field injection is only supported for body JSON injection 53 if self.field_path is not None and self.inject_into != RequestOptionType.body_json: 54 raise ValueError( 55 "Nested field injection is only supported for body JSON injection. Please use a top-level field_name for other injection types." 56 ) 57 58 # Convert field_name and field_path into InterpolatedString objects if they are strings 59 if self.field_name is not None: 60 self.field_name = InterpolatedString.create(self.field_name, parameters=parameters) 61 elif self.field_path is not None: 62 self.field_path = [ 63 InterpolatedString.create(segment, parameters=parameters) 64 for segment in self.field_path 65 ] 66 67 @property 68 def _is_field_path(self) -> bool: 69 """Returns whether this option is a field path (ie, a nested field)""" 70 return self.field_path is not None 71 72 def inject_into_request( 73 self, 74 target: MutableMapping[str, Any], 75 value: Any, 76 config: Config, 77 ) -> None: 78 """ 79 Inject a request option value into a target request structure using either field_name or field_path. 80 For non-body-json injection, only top-level field names are supported. 81 For body-json injection, both field names and nested field paths are supported. 82 83 Args: 84 target: The request structure to inject the value into 85 value: The value to inject 86 config: The config object to use for interpolation 87 """ 88 if self._is_field_path: 89 if self.inject_into != RequestOptionType.body_json: 90 raise ValueError( 91 "Nested field injection is only supported for body JSON injection. Please use a top-level field_name for other injection types." 92 ) 93 94 assert self.field_path is not None # for type checker 95 current = target 96 # Convert path segments into strings, evaluating any interpolated segments 97 # Example: ["data", "{{ config[user_type] }}", "id"] -> ["data", "admin", "id"] 98 *path_parts, final_key = [ 99 str( 100 segment.eval(config=config) 101 if isinstance(segment, InterpolatedString) 102 else segment 103 ) 104 for segment in self.field_path 105 ] 106 107 # Build a nested dictionary structure and set the final value at the deepest level 108 for part in path_parts: 109 current = current.setdefault(part, {}) 110 current[final_key] = value 111 else: 112 # For non-nested fields, evaluate the field name if it's an interpolated string 113 key = ( 114 self.field_name.eval(config=config) 115 if isinstance(self.field_name, InterpolatedString) 116 else self.field_name 117 ) 118 target[str(key)] = value
Describes an option to set on a request
Attributes:
- field_name (str): Describes the name of the parameter to inject. Mutually exclusive with field_path.
- field_path (list(str)): Describes the path to a nested field as a list of field names. Only valid for body_json injection type, and mutually exclusive with field_name.
- inject_into (RequestOptionType): Describes where in the HTTP request to inject the parameter
72 def inject_into_request( 73 self, 74 target: MutableMapping[str, Any], 75 value: Any, 76 config: Config, 77 ) -> None: 78 """ 79 Inject a request option value into a target request structure using either field_name or field_path. 80 For non-body-json injection, only top-level field names are supported. 81 For body-json injection, both field names and nested field paths are supported. 82 83 Args: 84 target: The request structure to inject the value into 85 value: The value to inject 86 config: The config object to use for interpolation 87 """ 88 if self._is_field_path: 89 if self.inject_into != RequestOptionType.body_json: 90 raise ValueError( 91 "Nested field injection is only supported for body JSON injection. Please use a top-level field_name for other injection types." 92 ) 93 94 assert self.field_path is not None # for type checker 95 current = target 96 # Convert path segments into strings, evaluating any interpolated segments 97 # Example: ["data", "{{ config[user_type] }}", "id"] -> ["data", "admin", "id"] 98 *path_parts, final_key = [ 99 str( 100 segment.eval(config=config) 101 if isinstance(segment, InterpolatedString) 102 else segment 103 ) 104 for segment in self.field_path 105 ] 106 107 # Build a nested dictionary structure and set the final value at the deepest level 108 for part in path_parts: 109 current = current.setdefault(part, {}) 110 current[final_key] = value 111 else: 112 # For non-nested fields, evaluate the field name if it's an interpolated string 113 key = ( 114 self.field_name.eval(config=config) 115 if isinstance(self.field_name, InterpolatedString) 116 else self.field_name 117 ) 118 target[str(key)] = value
Inject a request option value into a target request structure using either field_name or field_path. For non-body-json injection, only top-level field names are supported. For body-json injection, both field names and nested field paths are supported.
Arguments:
- target: The request structure to inject the value into
- value: The value to inject
- config: The config object to use for interpolation
30class Requester(RequestOptionsProvider): 31 @abstractmethod 32 def get_authenticator(self) -> DeclarativeAuthenticator: 33 """ 34 Specifies the authenticator to use when submitting requests 35 """ 36 pass 37 38 @abstractmethod 39 def get_url( 40 self, 41 *, 42 stream_state: Optional[StreamState], 43 stream_slice: Optional[StreamSlice], 44 next_page_token: Optional[Mapping[str, Any]], 45 ) -> str: 46 """ 47 :return: URL base for the API endpoint e.g: if you wanted to hit https://myapi.com/v1/some_entity then this should return "https://myapi.com/v1/" 48 """ 49 50 @abstractmethod 51 def get_url_base( 52 self, 53 *, 54 stream_state: Optional[StreamState], 55 stream_slice: Optional[StreamSlice], 56 next_page_token: Optional[Mapping[str, Any]], 57 ) -> str: 58 """ 59 :return: URL base for the API endpoint e.g: if you wanted to hit https://myapi.com/v1/some_entity then this should return "https://myapi.com/v1/" 60 """ 61 62 @abstractmethod 63 def get_path( 64 self, 65 *, 66 stream_state: Optional[StreamState], 67 stream_slice: Optional[StreamSlice], 68 next_page_token: Optional[Mapping[str, Any]], 69 ) -> str: 70 """ 71 Returns the URL path for the API endpoint e.g: if you wanted to hit https://myapi.com/v1/some_entity then this should return "some_entity" 72 """ 73 74 @abstractmethod 75 def get_method(self) -> HttpMethod: 76 """ 77 Specifies the HTTP method to use 78 """ 79 80 @abstractmethod 81 def get_request_params( 82 self, 83 *, 84 stream_state: Optional[StreamState] = None, 85 stream_slice: Optional[StreamSlice] = None, 86 next_page_token: Optional[Mapping[str, Any]] = None, 87 ) -> MutableMapping[str, Any]: 88 """ 89 Specifies the query parameters that should be set on an outgoing HTTP request given the inputs. 90 91 E.g: you might want to define query parameters for paging if next_page_token is not None. 92 """ 93 94 @abstractmethod 95 def get_request_headers( 96 self, 97 *, 98 stream_state: Optional[StreamState] = None, 99 stream_slice: Optional[StreamSlice] = None, 100 next_page_token: Optional[Mapping[str, Any]] = None, 101 ) -> Mapping[str, Any]: 102 """ 103 Return any non-auth headers. Authentication headers will overwrite any overlapping headers returned from this method. 104 """ 105 106 @abstractmethod 107 def get_request_body_data( 108 self, 109 *, 110 stream_state: Optional[StreamState] = None, 111 stream_slice: Optional[StreamSlice] = None, 112 next_page_token: Optional[Mapping[str, Any]] = None, 113 ) -> Union[Mapping[str, Any], str]: 114 """ 115 Specifies how to populate the body of the request with a non-JSON payload. 116 117 If returns a ready text that it will be sent as is. 118 If returns a dict that it will be converted to a urlencoded form. 119 E.g. {"key1": "value1", "key2": "value2"} => "key1=value1&key2=value2" 120 121 At the same time only one of the 'request_body_data' and 'request_body_json' functions can be overridden. 122 """ 123 124 @abstractmethod 125 def get_request_body_json( 126 self, 127 *, 128 stream_state: Optional[StreamState] = None, 129 stream_slice: Optional[StreamSlice] = None, 130 next_page_token: Optional[Mapping[str, Any]] = None, 131 ) -> Mapping[str, Any]: 132 """ 133 Specifies how to populate the body of the request with a JSON payload. 134 135 At the same time only one of the 'request_body_data' and 'request_body_json' functions can be overridden. 136 """ 137 138 @abstractmethod 139 def send_request( 140 self, 141 stream_state: Optional[StreamState] = None, 142 stream_slice: Optional[StreamSlice] = None, 143 next_page_token: Optional[Mapping[str, Any]] = None, 144 path: Optional[str] = None, 145 request_headers: Optional[Mapping[str, Any]] = None, 146 request_params: Optional[Mapping[str, Any]] = None, 147 request_body_data: Optional[Union[Mapping[str, Any], str]] = None, 148 request_body_json: Optional[Mapping[str, Any]] = None, 149 log_formatter: Optional[Callable[[requests.Response], Any]] = None, 150 ) -> Optional[requests.Response]: 151 """ 152 Sends a request and returns the response. Might return no response if the error handler chooses to ignore the response or throw an exception in case of an error. 153 If path is set, the path configured on the requester itself is ignored. 154 If header, params and body are set, they are merged with the ones configured on the requester itself. 155 156 If a log formatter is provided, it's used to log the performed request and response. If it's not provided, no logging is performed. 157 """
Defines the request options to set on an outgoing HTTP request
Options can be passed by
- request parameter
- request headers
- body data
- json content
31 @abstractmethod 32 def get_authenticator(self) -> DeclarativeAuthenticator: 33 """ 34 Specifies the authenticator to use when submitting requests 35 """ 36 pass
Specifies the authenticator to use when submitting requests
38 @abstractmethod 39 def get_url( 40 self, 41 *, 42 stream_state: Optional[StreamState], 43 stream_slice: Optional[StreamSlice], 44 next_page_token: Optional[Mapping[str, Any]], 45 ) -> str: 46 """ 47 :return: URL base for the API endpoint e.g: if you wanted to hit https://myapi.com/v1/some_entity then this should return "https://myapi.com/v1/" 48 """
Returns
URL base for the API endpoint e.g: if you wanted to hit https://myapi.com/v1/some_entity then this should return "https://myapi.com/v1/"
50 @abstractmethod 51 def get_url_base( 52 self, 53 *, 54 stream_state: Optional[StreamState], 55 stream_slice: Optional[StreamSlice], 56 next_page_token: Optional[Mapping[str, Any]], 57 ) -> str: 58 """ 59 :return: URL base for the API endpoint e.g: if you wanted to hit https://myapi.com/v1/some_entity then this should return "https://myapi.com/v1/" 60 """
Returns
URL base for the API endpoint e.g: if you wanted to hit https://myapi.com/v1/some_entity then this should return "https://myapi.com/v1/"
62 @abstractmethod 63 def get_path( 64 self, 65 *, 66 stream_state: Optional[StreamState], 67 stream_slice: Optional[StreamSlice], 68 next_page_token: Optional[Mapping[str, Any]], 69 ) -> str: 70 """ 71 Returns the URL path for the API endpoint e.g: if you wanted to hit https://myapi.com/v1/some_entity then this should return "some_entity" 72 """
Returns the URL path for the API endpoint e.g: if you wanted to hit https://myapi.com/v1/some_entity then this should return "some_entity"
74 @abstractmethod 75 def get_method(self) -> HttpMethod: 76 """ 77 Specifies the HTTP method to use 78 """
Specifies the HTTP method to use
80 @abstractmethod 81 def get_request_params( 82 self, 83 *, 84 stream_state: Optional[StreamState] = None, 85 stream_slice: Optional[StreamSlice] = None, 86 next_page_token: Optional[Mapping[str, Any]] = None, 87 ) -> MutableMapping[str, Any]: 88 """ 89 Specifies the query parameters that should be set on an outgoing HTTP request given the inputs. 90 91 E.g: you might want to define query parameters for paging if next_page_token is not None. 92 """
Specifies the query parameters that should be set on an outgoing HTTP request given the inputs.
E.g: you might want to define query parameters for paging if next_page_token is not None.
94 @abstractmethod 95 def get_request_headers( 96 self, 97 *, 98 stream_state: Optional[StreamState] = None, 99 stream_slice: Optional[StreamSlice] = None, 100 next_page_token: Optional[Mapping[str, Any]] = None, 101 ) -> Mapping[str, Any]: 102 """ 103 Return any non-auth headers. Authentication headers will overwrite any overlapping headers returned from this method. 104 """
Return any non-auth headers. Authentication headers will overwrite any overlapping headers returned from this method.
106 @abstractmethod 107 def get_request_body_data( 108 self, 109 *, 110 stream_state: Optional[StreamState] = None, 111 stream_slice: Optional[StreamSlice] = None, 112 next_page_token: Optional[Mapping[str, Any]] = None, 113 ) -> Union[Mapping[str, Any], str]: 114 """ 115 Specifies how to populate the body of the request with a non-JSON payload. 116 117 If returns a ready text that it will be sent as is. 118 If returns a dict that it will be converted to a urlencoded form. 119 E.g. {"key1": "value1", "key2": "value2"} => "key1=value1&key2=value2" 120 121 At the same time only one of the 'request_body_data' and 'request_body_json' functions can be overridden. 122 """
Specifies how to populate the body of the request with a non-JSON payload.
If returns a ready text that it will be sent as is. If returns a dict that it will be converted to a urlencoded form. E.g. {"key1": "value1", "key2": "value2"} => "key1=value1&key2=value2"
At the same time only one of the 'request_body_data' and 'request_body_json' functions can be overridden.
124 @abstractmethod 125 def get_request_body_json( 126 self, 127 *, 128 stream_state: Optional[StreamState] = None, 129 stream_slice: Optional[StreamSlice] = None, 130 next_page_token: Optional[Mapping[str, Any]] = None, 131 ) -> Mapping[str, Any]: 132 """ 133 Specifies how to populate the body of the request with a JSON payload. 134 135 At the same time only one of the 'request_body_data' and 'request_body_json' functions can be overridden. 136 """
Specifies how to populate the body of the request with a JSON payload.
At the same time only one of the 'request_body_data' and 'request_body_json' functions can be overridden.
138 @abstractmethod 139 def send_request( 140 self, 141 stream_state: Optional[StreamState] = None, 142 stream_slice: Optional[StreamSlice] = None, 143 next_page_token: Optional[Mapping[str, Any]] = None, 144 path: Optional[str] = None, 145 request_headers: Optional[Mapping[str, Any]] = None, 146 request_params: Optional[Mapping[str, Any]] = None, 147 request_body_data: Optional[Union[Mapping[str, Any], str]] = None, 148 request_body_json: Optional[Mapping[str, Any]] = None, 149 log_formatter: Optional[Callable[[requests.Response], Any]] = None, 150 ) -> Optional[requests.Response]: 151 """ 152 Sends a request and returns the response. Might return no response if the error handler chooses to ignore the response or throw an exception in case of an error. 153 If path is set, the path configured on the requester itself is ignored. 154 If header, params and body are set, they are merged with the ones configured on the requester itself. 155 156 If a log formatter is provided, it's used to log the performed request and response. If it's not provided, no logging is performed. 157 """
Sends a request and returns the response. Might return no response if the error handler chooses to ignore the response or throw an exception in case of an error. If path is set, the path configured on the requester itself is ignored. If header, params and body are set, they are merged with the ones configured on the requester itself.
If a log formatter is provided, it's used to log the performed request and response. If it's not provided, no logging is performed.