Skip to content

Commit

Permalink
Add timeout option to Client (#463)
Browse files Browse the repository at this point in the history
* Add timeout option to Client.open

* [wip] Messing around with catching errors

* [wip] Trying to use mock to do the thing

* The closest I was able to get to a test of timeout, but still not right as the timeout came from the mock infrastructure, not from the request failing to serve in time

* Remove timeout test, since it wasn't working correctly; going ahead without an explicit test

* Push Timeout type alias to all relevant locations

* Update changelog

* tests: add smoke test for timeout

* Propegate timeout parameter to required locations and add documentation

* Add missing import statement

---------

Co-authored-by: Pete Gadomski <[email protected]>
  • Loading branch information
jpolchlo and gadomski authored Apr 25, 2023
1 parent 0a43803 commit 114724d
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 4 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

### Added

- Timeout option added to `Client.open` [#463](https://github.com/stac-utils/pystac-client/pull/463)

### Changed

- Switched to Ruff from isort/flake8 [#457](https://github.com/stac-utils/pystac-client/pull/457)
Expand Down
21 changes: 19 additions & 2 deletions pystac_client/client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
from functools import lru_cache
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Optional, Union
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
Iterator,
List,
Optional,
Union,
)

import pystac
import pystac.utils
Expand Down Expand Up @@ -27,7 +36,7 @@
SortbyLike,
)
from pystac_client.mixins import QueryablesMixin
from pystac_client.stac_api_io import StacApiIO
from pystac_client.stac_api_io import StacApiIO, Timeout

if TYPE_CHECKING:
from pystac.item import Item as Item_Type
Expand Down Expand Up @@ -88,6 +97,7 @@ def open(
modifier: Optional[Callable[[Modifiable], None]] = None,
request_modifier: Optional[Callable[[Request], Union[Request, None]]] = None,
stac_io: Optional[StacApiIO] = None,
timeout: Timeout = None,
) -> "Client":
"""Opens a STAC Catalog or API
This function will read the root catalog of a STAC Catalog or API
Expand Down Expand Up @@ -137,6 +147,9 @@ def open(
stac_io: A `StacApiIO` object to use for I/O requests. Generally, leave
this to the default. However in cases where customized I/O processing
is required, a custom instance can be provided here.
timeout: Optional float or (float, float) tuple following the semantics
defined by `Requests
<https://requests.readthedocs.io/en/latest/api/#main-interface>`__.
Return:
catalog : A :class:`Client` instance for this Catalog/API
Expand All @@ -148,6 +161,7 @@ def open(
modifier=modifier,
request_modifier=request_modifier,
stac_io=stac_io,
timeout=timeout,
)
search_link = client.get_search_link()
# if there is a search link, but no conformsTo advertised, ignore
Expand Down Expand Up @@ -176,6 +190,7 @@ def from_file( # type: ignore
parameters: Optional[Dict[str, Any]] = None,
modifier: Optional[Callable[[Modifiable], None]] = None,
request_modifier: Optional[Callable[[Request], Union[Request, None]]] = None,
timeout: Timeout = None,
) -> "Client":
"""Open a STAC Catalog/API
Expand All @@ -187,12 +202,14 @@ def from_file( # type: ignore
headers=headers,
parameters=parameters,
request_modifier=request_modifier,
timeout=timeout,
)
else:
stac_io.update(
headers=headers,
parameters=parameters,
request_modifier=request_modifier,
timeout=timeout,
)

client: Client = super().from_file(href, stac_io)
Expand Down
31 changes: 29 additions & 2 deletions pystac_client/stac_api_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,18 @@
import logging
import re
from copy import deepcopy
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Optional, Union
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
Iterator,
List,
Optional,
Tuple,
Union,
)
from typing_extensions import TypeAlias
from urllib.parse import urlparse

import pystac
Expand All @@ -28,13 +39,17 @@
logger = logging.getLogger(__name__)


Timeout: TypeAlias = Optional[Union[float, Tuple[float, float], Tuple[float, None]]]


class StacApiIO(DefaultStacIO):
def __init__(
self,
headers: Optional[Dict[str, str]] = None,
conformance: Optional[List[str]] = None,
parameters: Optional[Dict[str, Any]] = None,
request_modifier: Optional[Callable[[Request], Union[Request, None]]] = None,
timeout: Timeout = None,
):
"""Initialize class for API IO
Expand All @@ -48,13 +63,17 @@ def __init__(
objects before they are sent. If provided, the callable receives a
`request.Request` and must either modify the object directly or return
a new / modified request instance.
timeout: Optional float or (float, float) tuple following the semantics
defined by `Requests
<https://requests.readthedocs.io/en/latest/api/#main-interface>`__.
Return:
StacApiIO : StacApiIO instance
"""
# TODO - this should super() to parent class
self.session = Session()
self._conformance = conformance
self.timeout = timeout
self.update(
headers=headers, parameters=parameters, request_modifier=request_modifier
)
Expand All @@ -64,6 +83,7 @@ def update(
headers: Optional[Dict[str, str]] = None,
parameters: Optional[Dict[str, Any]] = None,
request_modifier: Optional[Callable[[Request], Union[Request, None]]] = None,
timeout: Timeout = None,
) -> None:
"""Updates this StacApi's headers, parameters, and/or request_modifer.
Expand All @@ -75,10 +95,14 @@ def update(
objects before they are sent. If provided, the callable receives a
`request.Request` and must either modify the object directly or return
a new / modified request instance.
timeout: Optional float or (float, float) tuple following the semantics
defined by `Requests
<https://requests.readthedocs.io/en/latest/api/#main-interface>`__.
"""
self.session.headers.update(headers or {})
self.session.params.update(parameters or {}) # type: ignore
self._req_modifier = request_modifier
self.timeout = timeout

def read_text(self, source: pystac.link.HREF, *args: Any, **kwargs: Any) -> str:
"""Read text from the given URI.
Expand Down Expand Up @@ -161,9 +185,12 @@ def request(
msg = f"{prepped.method} {prepped.url} Headers: {prepped.headers}"
if method == "POST":
msg += f" Payload: {json.dumps(request.json)}"
if self.timeout is not None:
msg += f" Timeout: {self.timeout}"
logger.debug(msg)
resp = self.session.send(prepped)
resp = self.session.send(prepped, timeout=self.timeout)
except Exception as err:
logger.debug(err)
raise APIError(str(err))
if resp.status_code != 200:
raise APIError.from_response(resp)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
interactions:
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
User-Agent:
- python-requests/2.28.2
method: GET
uri: https://planetarycomputer.microsoft.com/api/stac/v1
response:
body:
string: !!binary |
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
headers:
Accept-Ranges:
- bytes
Access-Control-Allow-Credentials:
- 'true'
Access-Control-Allow-Origin:
- '*'
Connection:
- keep-alive
Content-Length:
- '2906'
Content-Type:
- application/json
Date:
- Fri, 21 Apr 2023 13:32:05 GMT
Strict-Transport-Security:
- max-age=15724800; includeSubDomains
X-Cache:
- CONFIG_NOCACHE
content-encoding:
- gzip
vary:
- Accept-Encoding
x-azure-ref:
- 20230421T133204Z-s8t62zhfm931t3a14qcgbhkkrg00000005u0000000009bmb
status:
code: 200
message: OK
version: 1
8 changes: 8 additions & 0 deletions tests/test_stac_api_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,3 +279,11 @@ def test_stop_on_first_empty_page(
stac_api_io = StacApiIO()
pages = list(stac_api_io.get_pages(url))
assert len(pages) == 0

@pytest.mark.vcr
def test_timeout_smoke_test(self) -> None:
# Testing timeout behavior is hard, so we just have a simple smoke test to make
# sure that providing a timeout doesn't break anything.
stac_api_io = StacApiIO(timeout=42)
response = stac_api_io.read_text(STAC_URLS["PLANETARY-COMPUTER"])
assert isinstance(response, str)

0 comments on commit 114724d

Please sign in to comment.