diff --git a/cookiecutter/tap-template/{{cookiecutter.tap_id}}/{{cookiecutter.library_name}}/tap.py b/cookiecutter/tap-template/{{cookiecutter.tap_id}}/{{cookiecutter.library_name}}/tap.py index df3f9f754..74c8927e9 100644 --- a/cookiecutter/tap-template/{{cookiecutter.tap_id}}/{{cookiecutter.library_name}}/tap.py +++ b/cookiecutter/tap-template/{{cookiecutter.tap_id}}/{{cookiecutter.library_name}}/tap.py @@ -50,6 +50,16 @@ class Tap{{ cookiecutter.source_name }}({{ 'SQL' if cookiecutter.stream_type == default="https://api.mysample.com", description="The url for the API service", ), + {%- if cookiecutter.stream_type in ("GraphQL", "REST") %} + th.Property( + "user_agent", + th.StringType, + description=( + "A custom User-Agent header to send with each request. Default is " + "'/'" + ), + ), + {%- endif %} ).to_dict() {%- if cookiecutter.stream_type in ("GraphQL", "REST", "Other") %} diff --git a/samples/sample_tap_dummy_json/tap_dummyjson/client.py b/samples/sample_tap_dummy_json/tap_dummyjson/client.py index c946675f3..3f0ba2a91 100644 --- a/samples/sample_tap_dummy_json/tap_dummyjson/client.py +++ b/samples/sample_tap_dummy_json/tap_dummyjson/client.py @@ -28,10 +28,6 @@ def authenticator(self): password=self.config["password"], ) - @property - def http_headers(self): - return {"User-Agent": "tap-dummyjson"} - def get_new_paginator(self): return BaseOffsetPaginator(start_value=0, page_size=PAGE_SIZE) diff --git a/singer_sdk/streams/rest.py b/singer_sdk/streams/rest.py index 559389e8c..76e2abf5d 100644 --- a/singer_sdk/streams/rest.py +++ b/singer_sdk/streams/rest.py @@ -6,6 +6,7 @@ import copy import logging import typing as t +from functools import cached_property from http import HTTPStatus from urllib.parse import urlparse from warnings import warn @@ -100,7 +101,7 @@ def __init__( super().__init__(name=name, schema=schema, tap=tap) if path: self.path = path - self._http_headers: dict = {} + self._http_headers: dict = {"User-Agent": self.user_agent} self._requests_session = requests.Session() self._compiled_jsonpath = None self._next_page_token_compiled_jsonpath = None @@ -150,6 +151,18 @@ def requests_session(self) -> requests.Session: self._requests_session = requests.Session() return self._requests_session + @cached_property + def user_agent(self) -> str: + """Get the user agent string for the stream. + + Returns: + The user agent string. + """ + return self.config.get( + "user_agent", + f"{self.tap_name}/{self._tap.plugin_version}", + ) + def validate_response(self, response: requests.Response) -> None: """Validate HTTP response. @@ -553,10 +566,7 @@ def http_headers(self) -> dict: Returns: Dictionary of HTTP headers to use as a base for every request. """ - result = self._http_headers - if "user_agent" in self.config: - result["User-Agent"] = self.config.get("user_agent") - return result + return self._http_headers @property def timeout(self) -> int: