diff --git a/docs/source/dev/types.rst b/docs/source/dev/types.rst index dafa258d..f9314f14 100644 --- a/docs/source/dev/types.rst +++ b/docs/source/dev/types.rst @@ -62,3 +62,8 @@ Url === .. autoclass:: uplink.Url + +Timeout +======= + +.. autoclass:: uplink.Timeout diff --git a/tests/unit/test_arguments.py b/tests/unit/test_arguments.py index 555e34c5..826fcd62 100644 --- a/tests/unit/test_arguments.py +++ b/tests/unit/test_arguments.py @@ -424,3 +424,12 @@ def test_modify_request_definition_failure( def test_modify_request(self, request_builder): arguments.Url().modify_request(request_builder, "/some/path") assert request_builder.url == "/some/path" + + +class TestTimeout(ArgumentTestCase, FuncDecoratorTestCase): + type_cls = arguments.Timeout + expected_converter_key = keys.Identity() + + def test_modify_request(self, request_builder): + arguments.Timeout().modify_request(request_builder, 10) + assert request_builder.info["timeout"] == 10 diff --git a/tests/unit/test_converters.py b/tests/unit/test_converters.py index b269fa0c..32d5509b 100644 --- a/tests/unit/test_converters.py +++ b/tests/unit/test_converters.py @@ -312,6 +312,37 @@ def test_eq(self): assert not (converters.keys.Sequence(1) == 1) +class TestIdentity(object): + _sentinel = object() + + @pytest.fixture(scope="class") + def registry(self): + return converters.ConverterFactoryRegistry( + (converters.StandardConverter(),) + ) + + @pytest.fixture(scope="class") + def key(self): + return converters.keys.Identity() + + @pytest.mark.parametrize( + "value, expected", + [ + (1, 1), + ("a", "a"), + (_sentinel, _sentinel), + ({"a": "b"}, {"a": "b"}), + ([1, 2], [1, 2]), + ], + ) + def test_convert(self, registry, key, value, expected): + converter = registry[key]() + assert converter(value) == expected + + def test_eq(self): + assert converters.keys.Identity() == converters.keys.Identity() + + class TestRegistry(object): @pytest.mark.parametrize( "converter", diff --git a/uplink/__init__.py b/uplink/__init__.py index 62dc55b9..9e8ec9cb 100644 --- a/uplink/__init__.py +++ b/uplink/__init__.py @@ -40,6 +40,7 @@ PartMap, Body, Url, + Timeout, ) from uplink.ratelimit import ratelimit from uplink.retry import retry @@ -88,6 +89,7 @@ "PartMap", "Body", "Url", + "Timeout", "retry", "ratelimit", ] diff --git a/uplink/arguments.py b/uplink/arguments.py index 8e095b3e..1e7e7e79 100644 --- a/uplink/arguments.py +++ b/uplink/arguments.py @@ -23,6 +23,7 @@ "PartMap", "Body", "Url", + "Timeout", ] @@ -675,3 +676,35 @@ def modify_request_definition(self, request_definition_builder): def _modify_request(cls, request_builder, value): """Updates request url.""" request_builder.url = value + + +class Timeout(FuncDecoratorMixin, ArgumentAnnotation): + """ + Pass a timeout as a method argument at runtime. + + While :py:class:`uplink.timeout` attaches static timeout to all requests + sent from a consumer method, this class turns a method argument into a + dynamic timeout value. + + Example: + .. code-block:: python + + @get("/user/posts") + def get_posts(self, timeout: Timeout() = 60): + \"""Fetch all posts for the current users giving up after given + number of seconds.\""" + + """ + + @property + def type(self): + return float + + @property + def converter_key(self): + """Do not convert passed argument.""" + return keys.Identity() + + def _modify_request(self, request_builder, value): + """Modifies request timeout.""" + request_builder.info["timeout"] = value diff --git a/uplink/converters/keys.py b/uplink/converters/keys.py index 09c07e84..c9142372 100644 --- a/uplink/converters/keys.py +++ b/uplink/converters/keys.py @@ -86,3 +86,21 @@ def convert(self, converter, value): return list(map(converter, value)) else: return converter(value) + + +class Identity(object): + """ + Identity conversion - pass value as is + """ + + def __call__(self, converter_registry): + return self._identity_factory + + def __eq__(self, other): + return type(other) is type(self) + + def _identity_factory(self, *args, **kwargs): + return self._identity + + def _identity(self, value): + return value