Repository: meltano/sdk
Pull request #2807 (2024-12-10 22:35:07)

fix: Allow developers to set `RESTStream.http_method`
Follows from https://github.com/meltano/sdk/pull/2762

<!-- readthedocs-preview meltano-sdk start -->
----
📚 Documentation preview 📚: https://meltano-sdk--2807.org.readthedocs.build/en/2807/

<!-- readthedocs-preview meltano-sdk end -->

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/meltano/sdk/pull/2807?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=meltano) Report
Attention: Patch coverage is `90.90909%` with `1 line` in your changes missing coverage. Please review.
> Project coverage is 91.27%. Comparing base [(`2882731`)](https://app.codecov.io/gh/meltano/sdk/commit/288273182cfdb2ea716bed550c697513551601bd?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=meltano) to head [(`4bafaae`)](https://app.codecov.io/gh/meltano/sdk/commit/4bafaaefaaaba46d7327534c635bde83f5f2116b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=meltano).

| [Files with missing lines](https://app.codecov.io/gh/meltano/sdk/pull/2807?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=meltano) | Patch % | Lines |
|---|---|---|
| [singer\_sdk/streams/rest.py](https://app.codecov.io/gh/meltano/sdk/pull/2807?src=pr&el=tree&filepath=singer_sdk%2Fstreams%2Frest.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=meltano#diff-c2luZ2VyX3Nkay9zdHJlYW1zL3Jlc3QucHk=) | 90.90% | [1 Missing :warning: ](https://app.codecov.io/gh/meltano/sdk/pull/2807?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=meltano) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #2807      +/-   ##
==========================================
- Coverage   91.27%   91.27%   -0.01%     
==========================================
  Files          62       62              
  Lines        5182     5190       +8     
  Branches      669      669              
==========================================
+ Hits         4730     4737       +7     
- Misses        319      320       +1     
  Partials      133      133              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/meltano/sdk/pull/2807?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=meltano).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=meltano).

codspeed-hq[bot]: ## [CodSpeed Performance Report](https://codspeed.io/meltano/sdk/branches/edgarrmondragon%2Ffix%2Fallow-updates-http-method)

### Merging #2807 will **not alter performance**

<sub>Comparing <code>edgarrmondragon/fix/allow-updates-http-method</code> (4bafaae) with <code>main</code> (2882731)</sub>



### Summary

`✅ 7` untouched benchmarks  





Patch:
diff --git a/singer_sdk/streams/rest.py b/singer_sdk/streams/rest.py
index f4f3e7fc..cb964c24 100644
--- a/singer_sdk/streams/rest.py
+++ b/singer_sdk/streams/rest.py
@@ -91,6 +91,8 @@ class _HTTPStream(Stream, t.Generic[_TToken], metaclass=abc.ABCMeta):  # noqa: P
         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 @@ class _HTTPStream(Stream, t.Generic[_TToken], metaclass=abc.ABCMeta):  # noqa: P
             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 @@ class _HTTPStream(Stream, t.Generic[_TToken], metaclass=abc.ABCMeta):  # noqa: P
         .. 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 @@ class RESTStream(_HTTPStream, t.Generic[_TToken], metaclass=abc.ABCMeta):
         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 @@ class RESTStream(_HTTPStream, t.Generic[_TToken], metaclass=abc.ABCMeta):
             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
 


Test patch:
diff --git a/tests/core/test_streams.py b/tests/core/test_streams.py
index 562621a5..8b595ac0 100644
--- a/tests/core/test_streams.py
+++ b/tests/core/test_streams.py
@@ -10,12 +10,15 @@ import urllib.parse
 
 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 test_non_json_payload(tap: Tap, requests_mock: requests_mock.Mocker):
     ]
 
 
+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},