From 9c46579ad79c204740c843c17c729e5386a6d17c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Ram=C3=ADrez=20Mondrag=C3=B3n?= Date: Tue, 10 Dec 2024 16:33:14 -0600 Subject: [PATCH] fix: Allow developers to set `RESTStream.http_method` --- singer_sdk/streams/rest.py | 38 ++++++++++++++++++++++++++--- tests/core/test_streams.py | 49 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 3 deletions(-) diff --git a/singer_sdk/streams/rest.py b/singer_sdk/streams/rest.py index f4f3e7fce..cb964c242 100644 --- a/singer_sdk/streams/rest.py +++ b/singer_sdk/streams/rest.py @@ -91,6 +91,8 @@ def __init__( name: str | None = None, schema: dict[str, t.Any] | Schema | None = None, path: str | None = None, + *, + http_method: str | None = None, ) -> None: """Initialize the HTTP stream. @@ -99,11 +101,13 @@ def __init__( schema: JSON schema for records in this stream. name: Name of this stream. path: URL path for this entity stream. + http_method: HTTP method to use for requests. """ super().__init__(name=name, schema=schema, tap=tap) if path: self.path = path self._http_headers: dict = {"User-Agent": self.user_agent} + self._http_method = http_method self._requests_session = requests.Session() @staticmethod @@ -151,12 +155,37 @@ def rest_method(self) -> str: .. deprecated:: 0.43.0 Override :meth:`~singer_sdk.RESTStream.http_method` instead. """ - return "GET" + return self._http_method or "GET" + + @rest_method.setter + @deprecated( + "Use `http_method` instead.", + category=SingerSDKDeprecationWarning, + ) + def rest_method(self, value: str) -> None: + """Set the HTTP method for requests. + + Args: + value: The HTTP method to use for requests. + + .. deprecated:: 0.43.0 + Override :meth:`~singer_sdk.RESTStream.http_method` instead. + """ + self._http_method = value @property def http_method(self) -> str: """HTTP method to use for requests. Defaults to "GET".""" - return self.rest_method + return self._http_method or self.rest_method + + @http_method.setter + def http_method(self, value: str) -> None: + """Set the HTTP method for requests. + + Args: + value: The HTTP method to use for requests. + """ + self._http_method = value @property def requests_session(self) -> requests.Session: @@ -758,6 +787,8 @@ def __init__( name: str | None = None, schema: dict[str, t.Any] | Schema | None = None, path: str | None = None, + *, + http_method: str | None = None, ) -> None: """Initialize the REST stream. @@ -766,8 +797,9 @@ def __init__( schema: JSON schema for records in this stream. name: Name of this stream. path: URL path for this entity stream. + http_method: HTTP method to use for requests """ - super().__init__(tap, name, schema, path) + super().__init__(tap, name, schema, path, http_method=http_method) self._compiled_jsonpath = None self._next_page_token_compiled_jsonpath = None diff --git a/tests/core/test_streams.py b/tests/core/test_streams.py index 562621a5f..8b595ac0d 100644 --- a/tests/core/test_streams.py +++ b/tests/core/test_streams.py @@ -10,12 +10,15 @@ import pytest import requests +import requests_mock.adapter as requests_mock_adapter from singer_sdk._singerlib import Catalog, MetadataMapping from singer_sdk.exceptions import ( + FatalAPIError, InvalidReplicationKeyException, ) from singer_sdk.helpers._classproperty import classproperty +from singer_sdk.helpers._compat import SingerSDKDeprecationWarning from singer_sdk.helpers._compat import datetime_fromisoformat as parse from singer_sdk.helpers.jsonpath import _compile_jsonpath from singer_sdk.streams.core import REPLICATION_FULL_TABLE, REPLICATION_INCREMENTAL @@ -569,6 +572,52 @@ def prepare_request_payload(self, context, next_page_token): # noqa: ARG002 ] +def test_mutate_http_method(tap: Tap, requests_mock: requests_mock.Mocker): + """Test HTTP method can be overridden.""" + + def callback(request: requests.PreparedRequest, context: requests_mock.Context): + if request.method == "POST": + return { + "data": [ + {"id": 1, "value": "abc"}, + {"id": 2, "value": "def"}, + ] + } + + # Method not allowed + context.status_code = 405 + context.reason = "Method Not Allowed" + return {"error": "Check your method"} + + class PostStream(RestTestStream): + records_jsonpath = "$.data[*]" + path = "/endpoint" + + stream = PostStream(tap, http_method="PUT") + requests_mock.request( + requests_mock_adapter.ANY, + url="https://example.com/endpoint", + json=callback, + ) + + with pytest.raises(FatalAPIError, match="Method Not Allowed"): + list(stream.request_records(None)) + + with pytest.warns(SingerSDKDeprecationWarning): + stream.rest_method = "GET" + + with pytest.raises(FatalAPIError, match="Method Not Allowed"): + list(stream.request_records(None)) + + stream.http_method = "POST" + + records = list(stream.request_records(None)) + assert records == [ + {"id": 1, "value": "abc"}, + {"id": 2, "value": "def"}, + ] + + def test_parse_response(tap: Tap): content = """[ {"id": 1, "value": 3.14159},