We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Proper testing for rgd_client
We need to implement real tests for the Python client

The tests for this would require running RGD with data prepopulated in the background then executing the client tests

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `rgd-client/rgd_client/rgdc.py`
Content:
```
1 from base64 import b64encode
2 from dataclasses import dataclass
3 import getpass
4 from pathlib import Path
5 import tempfile
6 from typing import Dict, Iterator, List, Optional, Tuple, Union
7 
8 from tqdm import tqdm
9 
10 from .session import RgdcSession
11 from .types import DATETIME_OR_STR_TUPLE, SEARCH_PREDICATE_CHOICE
12 from .utils import (
13     DEFAULT_RGD_API,
14     download_checksum_file_to_path,
15     limit_offset_pager,
16     spatial_search_params,
17     spatial_subentry_id,
18 )
19 
20 
21 @dataclass
22 class RasterDownload:
23     path: Path
24     images: List[Path]
25     ancillary: List[Path]
26 
27 
28 class Rgdc:
29     def __init__(
30         self,
31         api_url: str = DEFAULT_RGD_API,
32         username: Optional[str] = None,
33         password: Optional[str] = None,
34     ):
35         """
36         Initialize a RGD Client.
37 
38         Args:
39             api_url: The base url of the RGD API instance.
40             username: The username to authenticate to the instance with, if any.
41             password: The password associated with the provided username. If None, a prompt will be provided.
42 
43         Returns:
44             A new Rgdc instance.
45         """
46         auth_header = None
47 
48         # Prompt for password if not provided
49         if username is not None and password is None:
50             password = getpass.getpass()
51 
52         if username and password:
53             encoded_credentials = b64encode(f'{username}:{password}'.encode('utf-8')).decode()
54             auth_header = f'Basic {encoded_credentials}'
55 
56         self.session = RgdcSession(base_url=api_url, auth_header=auth_header)
57 
58     def list_image_tiles(self, image_id: Union[str, int]) -> Dict:
59         """List geodata imagery tiles."""
60         r = self.session.get(f'image_process/imagery/{image_id}/tiles')
61         return r.json()
62 
63     def download_image_file(
64         self, image_id: Union[str, int], chunk_size: int = 1024 * 1024
65     ) -> Iterator[bytes]:
66         """
67         Download the associated ImageFile data for this ImageEntry directly from S3.
68 
69         Args:
70             image_id: The ID of the ImageEntry to download.
71             chunk_size: The size (in bytes) of each item in the returned iterator (defaults to 1MB).
72 
73         Returns:
74             An iterator of byte chunks.
75         """
76         r = self.session.get(f'rgd_imagery/{image_id}/data', stream=True)
77         return r.iter_content(chunk_size=chunk_size)
78 
79     def download_image_thumbnail(
80         self,
81         image_id: Union[str, int],
82     ) -> bytes:
83         """
84         Download the generated thumbnail for this ImageEntry.
85 
86         Args:
87             image_id: The ID of the ImageEntry to download.
88 
89         Returns:
90             Thumbnail bytes.
91         """
92         r = self.session.get(f'image_process/imagery/{image_id}/thumbnail')
93         return r.content
94 
95     def download_raster_thumbnail(
96         self,
97         raster_meta_id: Union[str, int, dict],
98         band: int = 0,
99     ) -> bytes:
100         """
101         Download the generated thumbnail for this ImageEntry.
102 
103         Args:
104             raster_meta_id: The id of the RasterMeta, which is a child to the desired raster entry, or search result.
105             band: The index of the image in the raster's image set to produce thumbnail from.
106 
107         Returns:
108             Thumbnail bytes.
109         """
110         if isinstance(raster_meta_id, dict):
111             raster_meta_id = spatial_subentry_id(raster_meta_id)
112 
113         r = self.session.get(f'rgd_imagery/raster/{raster_meta_id}')
114         parent_raster = r.json().get('parent_raster', {})
115         images = parent_raster.get('image_set', {}).get('images', [])
116         try:
117             return self.download_image_thumbnail(images[band]['id'])
118         except IndexError:
119             raise IndexError(f'Band index ({band}) out of range.')
120 
121     def get_raster(self, raster_meta_id: Union[str, int, dict], stac: bool = False) -> Dict:
122         """Get raster entry detail.
123 
124         Args:
125             stac: Optionally return as STAC Item dictionary/JSON.
126 
127         Returns:
128             Serialized object representation.
129         """
130         if isinstance(raster_meta_id, dict):
131             raster_meta_id = spatial_subentry_id(raster_meta_id)
132 
133         if stac:
134             r = self.session.get(f'rgd_imagery/raster/{raster_meta_id}/stac')
135         else:
136             r = self.session.get(f'rgd_imagery/raster/{raster_meta_id}')
137         return r.json()
138 
139     def download_raster(
140         self,
141         raster_meta_id: Union[str, int, dict],
142         pathname: Optional[str] = None,
143         nest_with_name: bool = False,
144         keep_existing: bool = True,
145     ) -> RasterDownload:
146         """
147         Download the image set associated with a raster entry to disk.
148 
149         Args:
150             raster_meta_id: The id of the RasterMeta, which is a child to the desired raster entry, or search result.
151             pathname: The directory to download the image set to. If not supplied, a temporary directory will be used.
152             nest_with_name: If True, nests the download within an additional directory, using the raster entry name.
153             keep_existing: If False, replace files existing on disk. Only valid if `pathname` is given.
154 
155         Returns:
156             A dictionary of the paths to all files downloaded under the directory.
157         """
158         if isinstance(raster_meta_id, dict):
159             raster_meta_id = spatial_subentry_id(raster_meta_id)
160 
161         r = self.session.get(f'rgd_imagery/raster/{raster_meta_id}')
162         parent_raster = r.json().get('parent_raster', {})
163 
164         # Create dirs after request to avoid empty dirs if failed
165         if pathname is None:
166             pathname = tempfile.mkdtemp()
167 
168         # Handle optional nesting with raster entry name
169         path = Path(pathname)
170         parent_raster_name: Optional[str] = parent_raster.get('name')
171 
172         if nest_with_name and parent_raster_name:
173             path = path / parent_raster_name
174 
175         # Ensure base download directory exists
176         if not path.exists():
177             path.mkdir()
178 
179         # Initialize dataclass
180         raster_download = RasterDownload(path, [], [])
181 
182         # Download images
183         images = parent_raster.get('image_set', {}).get('images', [])
184         for image in tqdm(images, desc='Downloading image files'):
185             file = image.get('file', {})
186             file_path = download_checksum_file_to_path(file, path, keep_existing=keep_existing)
187             if file_path:
188                 raster_download.images.append(file_path)
189 
190         # Download ancillary files
191         ancillary = parent_raster.get('ancillary_files', [])
192         for file in tqdm(ancillary, desc='Downloading ancillary files'):
193             file_path = download_checksum_file_to_path(file, path, keep_existing=keep_existing)
194             if file_path:
195                 raster_download.ancillary.append(file_path)
196 
197         return raster_download
198 
199     def search(
200         self,
201         query: Optional[Union[Dict, str]] = None,
202         predicate: Optional[SEARCH_PREDICATE_CHOICE] = None,
203         relates: Optional[str] = None,
204         distance: Optional[Tuple[float, float]] = None,
205         acquired: Optional[DATETIME_OR_STR_TUPLE] = None,
206         instrumentation: Optional[str] = None,
207         limit: Optional[int] = None,
208         offset: Optional[int] = None,
209     ) -> List[Dict]:
210         """
211         Search for geospatial entries based on various criteria.
212 
213         For Ranges (Tuples), an entry of `None` means that side of the range is unbounded.
214         E.g. a range of (2, None) is 2 or more, (None, 5) is at most 5, (2, 5) is between 2 and 5.
215 
216         Args:
217             query: Either a WKT GeoJSON representation, a GeoJSON string, or a GeoJSON dict.
218             predicate: A named spatial predicate based on the DE-9IM. This spatial predicate will
219                 be used to filter data such that predicate(a, b) where b is the queried geometry.
220             relates: Specify exactly how the queried geometry should relate to the data using a
221                 DE-9IM string code.
222             distance: The min/max distance around the queried geometry in meters.
223             acquired: The min/max date and time (ISO 8601) when data was acquired.
224             instrumentation: The instrumentation used to acquire at least one of these data.
225             limit: The maximum number of results to return.
226             offset: The number of results to skip.
227 
228         Returns:
229             A list of Spatial Entries.
230         """
231         params = spatial_search_params(
232             query=query,
233             predicate=predicate,
234             relates=relates,
235             distance=distance,
236             acquired=acquired,
237             instrumentation=instrumentation,
238             limit=limit,
239             offset=offset,
240         )
241         return list(limit_offset_pager(self.session, 'rgd/search', params=params))
242 
243     def create_raster_stac(self, raster: Dict) -> Dict:
244         """Create a raster entry using STAC format."""
245         r = self.session.post('rgd_imagery/raster/stac', json=raster)
246         r.raise_for_status()
247 
248         return r.json()
249 
250     def search_raster_stac(
251         self,
252         query: Optional[Union[Dict, str]] = None,
253         predicate: Optional[SEARCH_PREDICATE_CHOICE] = None,
254         relates: Optional[str] = None,
255         distance: Optional[Tuple[float, float]] = None,
256         acquired: Optional[DATETIME_OR_STR_TUPLE] = None,
257         instrumentation: Optional[str] = None,
258         num_bands: Optional[Tuple[int, int]] = None,
259         resolution: Optional[Tuple[int, int]] = None,
260         cloud_cover: Optional[Tuple[float, float]] = None,
261         limit: Optional[int] = None,
262         offset: Optional[int] = None,
263     ) -> List[Dict]:
264         """
265         Search for raster entries based on various criteria.
266 
267         For Ranges (Tuples), an entry of `None` means that side of the range is unbounded.
268         E.g. a range of (2, None) is 2 or more, (None, 5) is at most 5, (2, 5) is between 2 and 5.
269 
270         Args:
271             query: Either a WKT GeoJSON representation, a GeoJSON string, or a GeoJSON dict.
272             predicate: A named spatial predicate based on the DE-9IM. This spatial predicate will
273                 be used to filter data such that predicate(a, b) where b is the queried geometry.
274             relates: Specify exactly how the queried geometry should relate to the data using a
275                 DE-9IM string code.
276             distance: The min/max distance around the queried geometry in meters.
277             acquired: The min/max date and time (ISO 8601) when data was acquired.
278             instrumentation: The instrumentation used to acquire at least one of these data.
279             num_bands: The min/max number of bands in the raster.
280             resolution: The min/max resolution of the raster.
281             cloud_cover: The min/max cloud coverage of the raster.
282             limit: The maximum number of results to return.
283             offset: The number of results to skip.
284 
285         Returns:
286             A list of Spatial Entries in STAC Item format.
287         """
288         params = spatial_search_params(
289             query=query,
290             predicate=predicate,
291             relates=relates,
292             distance=distance,
293             acquired=acquired,
294             instrumentation=instrumentation,
295             limit=limit,
296             offset=offset,
297         )
298 
299         if num_bands and len(num_bands) == 2:
300             nbmin, nbmax = num_bands
301             params['num_bands_min'] = nbmin
302             params['num_bands_max'] = nbmax
303 
304         if resolution and len(resolution) == 2:
305             rmin, rmax = resolution
306             params['resolution_min'] = rmin
307             params['resolution_max'] = rmax
308 
309         if cloud_cover and len(cloud_cover) == 2:
310             ccmin, ccmax = cloud_cover
311             params['cloud_cover_min'] = ccmin
312             params['cloud_cover_max'] = ccmax
313 
314         return list(limit_offset_pager(self.session, 'rgd_imagery/raster/search', params=params))
315
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/rgd-client/rgd_client/rgdc.py b/rgd-client/rgd_client/rgdc.py
--- a/rgd-client/rgd_client/rgdc.py
+++ b/rgd-client/rgd_client/rgdc.py
@@ -238,7 +238,11 @@
             limit=limit,
             offset=offset,
         )
-        return list(limit_offset_pager(self.session, 'rgd/search', params=params))
+
+        r = self.session.get('rgd/search', params=params)
+        r.raise_for_status()
+
+        return r.json()
 
     def create_raster_stac(self, raster: Dict) -> Dict:
         """Create a raster entry using STAC format."""

{"golden_diff": "diff --git a/rgd-client/rgd_client/rgdc.py b/rgd-client/rgd_client/rgdc.py\n--- a/rgd-client/rgd_client/rgdc.py\n+++ b/rgd-client/rgd_client/rgdc.py\n@@ -238,7 +238,11 @@\n             limit=limit,\n             offset=offset,\n         )\n-        return list(limit_offset_pager(self.session, 'rgd/search', params=params))\n+\n+        r = self.session.get('rgd/search', params=params)\n+        r.raise_for_status()\n+\n+        return r.json()\n \n     def create_raster_stac(self, raster: Dict) -> Dict:\n         \"\"\"Create a raster entry using STAC format.\"\"\"\n", "issue": "Proper testing for rgd_client\nWe need to implement real tests for the Python client\r\n\r\nThe tests for this would require running RGD with data prepopulated in the background then executing the client tests\n", "before_files": [{"content": "from base64 import b64encode\nfrom dataclasses import dataclass\nimport getpass\nfrom pathlib import Path\nimport tempfile\nfrom typing import Dict, Iterator, List, Optional, Tuple, Union\n\nfrom tqdm import tqdm\n\nfrom .session import RgdcSession\nfrom .types import DATETIME_OR_STR_TUPLE, SEARCH_PREDICATE_CHOICE\nfrom .utils import (\n    DEFAULT_RGD_API,\n    download_checksum_file_to_path,\n    limit_offset_pager,\n    spatial_search_params,\n    spatial_subentry_id,\n)\n\n\n@dataclass\nclass RasterDownload:\n    path: Path\n    images: List[Path]\n    ancillary: List[Path]\n\n\nclass Rgdc:\n    def __init__(\n        self,\n        api_url: str = DEFAULT_RGD_API,\n        username: Optional[str] = None,\n        password: Optional[str] = None,\n    ):\n        \"\"\"\n        Initialize a RGD Client.\n\n        Args:\n            api_url: The base url of the RGD API instance.\n            username: The username to authenticate to the instance with, if any.\n            password: The password associated with the provided username. If None, a prompt will be provided.\n\n        Returns:\n            A new Rgdc instance.\n        \"\"\"\n        auth_header = None\n\n        # Prompt for password if not provided\n        if username is not None and password is None:\n            password = getpass.getpass()\n\n        if username and password:\n            encoded_credentials = b64encode(f'{username}:{password}'.encode('utf-8')).decode()\n            auth_header = f'Basic {encoded_credentials}'\n\n        self.session = RgdcSession(base_url=api_url, auth_header=auth_header)\n\n    def list_image_tiles(self, image_id: Union[str, int]) -> Dict:\n        \"\"\"List geodata imagery tiles.\"\"\"\n        r = self.session.get(f'image_process/imagery/{image_id}/tiles')\n        return r.json()\n\n    def download_image_file(\n        self, image_id: Union[str, int], chunk_size: int = 1024 * 1024\n    ) -> Iterator[bytes]:\n        \"\"\"\n        Download the associated ImageFile data for this ImageEntry directly from S3.\n\n        Args:\n            image_id: The ID of the ImageEntry to download.\n            chunk_size: The size (in bytes) of each item in the returned iterator (defaults to 1MB).\n\n        Returns:\n            An iterator of byte chunks.\n        \"\"\"\n        r = self.session.get(f'rgd_imagery/{image_id}/data', stream=True)\n        return r.iter_content(chunk_size=chunk_size)\n\n    def download_image_thumbnail(\n        self,\n        image_id: Union[str, int],\n    ) -> bytes:\n        \"\"\"\n        Download the generated thumbnail for this ImageEntry.\n\n        Args:\n            image_id: The ID of the ImageEntry to download.\n\n        Returns:\n            Thumbnail bytes.\n        \"\"\"\n        r = self.session.get(f'image_process/imagery/{image_id}/thumbnail')\n        return r.content\n\n    def download_raster_thumbnail(\n        self,\n        raster_meta_id: Union[str, int, dict],\n        band: int = 0,\n    ) -> bytes:\n        \"\"\"\n        Download the generated thumbnail for this ImageEntry.\n\n        Args:\n            raster_meta_id: The id of the RasterMeta, which is a child to the desired raster entry, or search result.\n            band: The index of the image in the raster's image set to produce thumbnail from.\n\n        Returns:\n            Thumbnail bytes.\n        \"\"\"\n        if isinstance(raster_meta_id, dict):\n            raster_meta_id = spatial_subentry_id(raster_meta_id)\n\n        r = self.session.get(f'rgd_imagery/raster/{raster_meta_id}')\n        parent_raster = r.json().get('parent_raster', {})\n        images = parent_raster.get('image_set', {}).get('images', [])\n        try:\n            return self.download_image_thumbnail(images[band]['id'])\n        except IndexError:\n            raise IndexError(f'Band index ({band}) out of range.')\n\n    def get_raster(self, raster_meta_id: Union[str, int, dict], stac: bool = False) -> Dict:\n        \"\"\"Get raster entry detail.\n\n        Args:\n            stac: Optionally return as STAC Item dictionary/JSON.\n\n        Returns:\n            Serialized object representation.\n        \"\"\"\n        if isinstance(raster_meta_id, dict):\n            raster_meta_id = spatial_subentry_id(raster_meta_id)\n\n        if stac:\n            r = self.session.get(f'rgd_imagery/raster/{raster_meta_id}/stac')\n        else:\n            r = self.session.get(f'rgd_imagery/raster/{raster_meta_id}')\n        return r.json()\n\n    def download_raster(\n        self,\n        raster_meta_id: Union[str, int, dict],\n        pathname: Optional[str] = None,\n        nest_with_name: bool = False,\n        keep_existing: bool = True,\n    ) -> RasterDownload:\n        \"\"\"\n        Download the image set associated with a raster entry to disk.\n\n        Args:\n            raster_meta_id: The id of the RasterMeta, which is a child to the desired raster entry, or search result.\n            pathname: The directory to download the image set to. If not supplied, a temporary directory will be used.\n            nest_with_name: If True, nests the download within an additional directory, using the raster entry name.\n            keep_existing: If False, replace files existing on disk. Only valid if `pathname` is given.\n\n        Returns:\n            A dictionary of the paths to all files downloaded under the directory.\n        \"\"\"\n        if isinstance(raster_meta_id, dict):\n            raster_meta_id = spatial_subentry_id(raster_meta_id)\n\n        r = self.session.get(f'rgd_imagery/raster/{raster_meta_id}')\n        parent_raster = r.json().get('parent_raster', {})\n\n        # Create dirs after request to avoid empty dirs if failed\n        if pathname is None:\n            pathname = tempfile.mkdtemp()\n\n        # Handle optional nesting with raster entry name\n        path = Path(pathname)\n        parent_raster_name: Optional[str] = parent_raster.get('name')\n\n        if nest_with_name and parent_raster_name:\n            path = path / parent_raster_name\n\n        # Ensure base download directory exists\n        if not path.exists():\n            path.mkdir()\n\n        # Initialize dataclass\n        raster_download = RasterDownload(path, [], [])\n\n        # Download images\n        images = parent_raster.get('image_set', {}).get('images', [])\n        for image in tqdm(images, desc='Downloading image files'):\n            file = image.get('file', {})\n            file_path = download_checksum_file_to_path(file, path, keep_existing=keep_existing)\n            if file_path:\n                raster_download.images.append(file_path)\n\n        # Download ancillary files\n        ancillary = parent_raster.get('ancillary_files', [])\n        for file in tqdm(ancillary, desc='Downloading ancillary files'):\n            file_path = download_checksum_file_to_path(file, path, keep_existing=keep_existing)\n            if file_path:\n                raster_download.ancillary.append(file_path)\n\n        return raster_download\n\n    def search(\n        self,\n        query: Optional[Union[Dict, str]] = None,\n        predicate: Optional[SEARCH_PREDICATE_CHOICE] = None,\n        relates: Optional[str] = None,\n        distance: Optional[Tuple[float, float]] = None,\n        acquired: Optional[DATETIME_OR_STR_TUPLE] = None,\n        instrumentation: Optional[str] = None,\n        limit: Optional[int] = None,\n        offset: Optional[int] = None,\n    ) -> List[Dict]:\n        \"\"\"\n        Search for geospatial entries based on various criteria.\n\n        For Ranges (Tuples), an entry of `None` means that side of the range is unbounded.\n        E.g. a range of (2, None) is 2 or more, (None, 5) is at most 5, (2, 5) is between 2 and 5.\n\n        Args:\n            query: Either a WKT GeoJSON representation, a GeoJSON string, or a GeoJSON dict.\n            predicate: A named spatial predicate based on the DE-9IM. This spatial predicate will\n                be used to filter data such that predicate(a, b) where b is the queried geometry.\n            relates: Specify exactly how the queried geometry should relate to the data using a\n                DE-9IM string code.\n            distance: The min/max distance around the queried geometry in meters.\n            acquired: The min/max date and time (ISO 8601) when data was acquired.\n            instrumentation: The instrumentation used to acquire at least one of these data.\n            limit: The maximum number of results to return.\n            offset: The number of results to skip.\n\n        Returns:\n            A list of Spatial Entries.\n        \"\"\"\n        params = spatial_search_params(\n            query=query,\n            predicate=predicate,\n            relates=relates,\n            distance=distance,\n            acquired=acquired,\n            instrumentation=instrumentation,\n            limit=limit,\n            offset=offset,\n        )\n        return list(limit_offset_pager(self.session, 'rgd/search', params=params))\n\n    def create_raster_stac(self, raster: Dict) -> Dict:\n        \"\"\"Create a raster entry using STAC format.\"\"\"\n        r = self.session.post('rgd_imagery/raster/stac', json=raster)\n        r.raise_for_status()\n\n        return r.json()\n\n    def search_raster_stac(\n        self,\n        query: Optional[Union[Dict, str]] = None,\n        predicate: Optional[SEARCH_PREDICATE_CHOICE] = None,\n        relates: Optional[str] = None,\n        distance: Optional[Tuple[float, float]] = None,\n        acquired: Optional[DATETIME_OR_STR_TUPLE] = None,\n        instrumentation: Optional[str] = None,\n        num_bands: Optional[Tuple[int, int]] = None,\n        resolution: Optional[Tuple[int, int]] = None,\n        cloud_cover: Optional[Tuple[float, float]] = None,\n        limit: Optional[int] = None,\n        offset: Optional[int] = None,\n    ) -> List[Dict]:\n        \"\"\"\n        Search for raster entries based on various criteria.\n\n        For Ranges (Tuples), an entry of `None` means that side of the range is unbounded.\n        E.g. a range of (2, None) is 2 or more, (None, 5) is at most 5, (2, 5) is between 2 and 5.\n\n        Args:\n            query: Either a WKT GeoJSON representation, a GeoJSON string, or a GeoJSON dict.\n            predicate: A named spatial predicate based on the DE-9IM. This spatial predicate will\n                be used to filter data such that predicate(a, b) where b is the queried geometry.\n            relates: Specify exactly how the queried geometry should relate to the data using a\n                DE-9IM string code.\n            distance: The min/max distance around the queried geometry in meters.\n            acquired: The min/max date and time (ISO 8601) when data was acquired.\n            instrumentation: The instrumentation used to acquire at least one of these data.\n            num_bands: The min/max number of bands in the raster.\n            resolution: The min/max resolution of the raster.\n            cloud_cover: The min/max cloud coverage of the raster.\n            limit: The maximum number of results to return.\n            offset: The number of results to skip.\n\n        Returns:\n            A list of Spatial Entries in STAC Item format.\n        \"\"\"\n        params = spatial_search_params(\n            query=query,\n            predicate=predicate,\n            relates=relates,\n            distance=distance,\n            acquired=acquired,\n            instrumentation=instrumentation,\n            limit=limit,\n            offset=offset,\n        )\n\n        if num_bands and len(num_bands) == 2:\n            nbmin, nbmax = num_bands\n            params['num_bands_min'] = nbmin\n            params['num_bands_max'] = nbmax\n\n        if resolution and len(resolution) == 2:\n            rmin, rmax = resolution\n            params['resolution_min'] = rmin\n            params['resolution_max'] = rmax\n\n        if cloud_cover and len(cloud_cover) == 2:\n            ccmin, ccmax = cloud_cover\n            params['cloud_cover_min'] = ccmin\n            params['cloud_cover_max'] = ccmax\n\n        return list(limit_offset_pager(self.session, 'rgd_imagery/raster/search', params=params))\n", "path": "rgd-client/rgd_client/rgdc.py"}], "after_files": [{"content": "from base64 import b64encode\nfrom dataclasses import dataclass\nimport getpass\nfrom pathlib import Path\nimport tempfile\nfrom typing import Dict, Iterator, List, Optional, Tuple, Union\n\nfrom tqdm import tqdm\n\nfrom .session import RgdcSession\nfrom .types import DATETIME_OR_STR_TUPLE, SEARCH_PREDICATE_CHOICE\nfrom .utils import (\n    DEFAULT_RGD_API,\n    download_checksum_file_to_path,\n    limit_offset_pager,\n    spatial_search_params,\n    spatial_subentry_id,\n)\n\n\n@dataclass\nclass RasterDownload:\n    path: Path\n    images: List[Path]\n    ancillary: List[Path]\n\n\nclass Rgdc:\n    def __init__(\n        self,\n        api_url: str = DEFAULT_RGD_API,\n        username: Optional[str] = None,\n        password: Optional[str] = None,\n    ):\n        \"\"\"\n        Initialize a RGD Client.\n\n        Args:\n            api_url: The base url of the RGD API instance.\n            username: The username to authenticate to the instance with, if any.\n            password: The password associated with the provided username. If None, a prompt will be provided.\n\n        Returns:\n            A new Rgdc instance.\n        \"\"\"\n        auth_header = None\n\n        # Prompt for password if not provided\n        if username is not None and password is None:\n            password = getpass.getpass()\n\n        if username and password:\n            encoded_credentials = b64encode(f'{username}:{password}'.encode('utf-8')).decode()\n            auth_header = f'Basic {encoded_credentials}'\n\n        self.session = RgdcSession(base_url=api_url, auth_header=auth_header)\n\n    def list_image_tiles(self, image_id: Union[str, int]) -> Dict:\n        \"\"\"List geodata imagery tiles.\"\"\"\n        r = self.session.get(f'image_process/imagery/{image_id}/tiles')\n        return r.json()\n\n    def download_image_file(\n        self, image_id: Union[str, int], chunk_size: int = 1024 * 1024\n    ) -> Iterator[bytes]:\n        \"\"\"\n        Download the associated ImageFile data for this ImageEntry directly from S3.\n\n        Args:\n            image_id: The ID of the ImageEntry to download.\n            chunk_size: The size (in bytes) of each item in the returned iterator (defaults to 1MB).\n\n        Returns:\n            An iterator of byte chunks.\n        \"\"\"\n        r = self.session.get(f'rgd_imagery/{image_id}/data', stream=True)\n        return r.iter_content(chunk_size=chunk_size)\n\n    def download_image_thumbnail(\n        self,\n        image_id: Union[str, int],\n    ) -> bytes:\n        \"\"\"\n        Download the generated thumbnail for this ImageEntry.\n\n        Args:\n            image_id: The ID of the ImageEntry to download.\n\n        Returns:\n            Thumbnail bytes.\n        \"\"\"\n        r = self.session.get(f'image_process/imagery/{image_id}/thumbnail')\n        return r.content\n\n    def download_raster_thumbnail(\n        self,\n        raster_meta_id: Union[str, int, dict],\n        band: int = 0,\n    ) -> bytes:\n        \"\"\"\n        Download the generated thumbnail for this ImageEntry.\n\n        Args:\n            raster_meta_id: The id of the RasterMeta, which is a child to the desired raster entry, or search result.\n            band: The index of the image in the raster's image set to produce thumbnail from.\n\n        Returns:\n            Thumbnail bytes.\n        \"\"\"\n        if isinstance(raster_meta_id, dict):\n            raster_meta_id = spatial_subentry_id(raster_meta_id)\n\n        r = self.session.get(f'rgd_imagery/raster/{raster_meta_id}')\n        parent_raster = r.json().get('parent_raster', {})\n        images = parent_raster.get('image_set', {}).get('images', [])\n        try:\n            return self.download_image_thumbnail(images[band]['id'])\n        except IndexError:\n            raise IndexError(f'Band index ({band}) out of range.')\n\n    def get_raster(self, raster_meta_id: Union[str, int, dict], stac: bool = False) -> Dict:\n        \"\"\"Get raster entry detail.\n\n        Args:\n            stac: Optionally return as STAC Item dictionary/JSON.\n\n        Returns:\n            Serialized object representation.\n        \"\"\"\n        if isinstance(raster_meta_id, dict):\n            raster_meta_id = spatial_subentry_id(raster_meta_id)\n\n        if stac:\n            r = self.session.get(f'rgd_imagery/raster/{raster_meta_id}/stac')\n        else:\n            r = self.session.get(f'rgd_imagery/raster/{raster_meta_id}')\n        return r.json()\n\n    def download_raster(\n        self,\n        raster_meta_id: Union[str, int, dict],\n        pathname: Optional[str] = None,\n        nest_with_name: bool = False,\n        keep_existing: bool = True,\n    ) -> RasterDownload:\n        \"\"\"\n        Download the image set associated with a raster entry to disk.\n\n        Args:\n            raster_meta_id: The id of the RasterMeta, which is a child to the desired raster entry, or search result.\n            pathname: The directory to download the image set to. If not supplied, a temporary directory will be used.\n            nest_with_name: If True, nests the download within an additional directory, using the raster entry name.\n            keep_existing: If False, replace files existing on disk. Only valid if `pathname` is given.\n\n        Returns:\n            A dictionary of the paths to all files downloaded under the directory.\n        \"\"\"\n        if isinstance(raster_meta_id, dict):\n            raster_meta_id = spatial_subentry_id(raster_meta_id)\n\n        r = self.session.get(f'rgd_imagery/raster/{raster_meta_id}')\n        parent_raster = r.json().get('parent_raster', {})\n\n        # Create dirs after request to avoid empty dirs if failed\n        if pathname is None:\n            pathname = tempfile.mkdtemp()\n\n        # Handle optional nesting with raster entry name\n        path = Path(pathname)\n        parent_raster_name: Optional[str] = parent_raster.get('name')\n\n        if nest_with_name and parent_raster_name:\n            path = path / parent_raster_name\n\n        # Ensure base download directory exists\n        if not path.exists():\n            path.mkdir()\n\n        # Initialize dataclass\n        raster_download = RasterDownload(path, [], [])\n\n        # Download images\n        images = parent_raster.get('image_set', {}).get('images', [])\n        for image in tqdm(images, desc='Downloading image files'):\n            file = image.get('file', {})\n            file_path = download_checksum_file_to_path(file, path, keep_existing=keep_existing)\n            if file_path:\n                raster_download.images.append(file_path)\n\n        # Download ancillary files\n        ancillary = parent_raster.get('ancillary_files', [])\n        for file in tqdm(ancillary, desc='Downloading ancillary files'):\n            file_path = download_checksum_file_to_path(file, path, keep_existing=keep_existing)\n            if file_path:\n                raster_download.ancillary.append(file_path)\n\n        return raster_download\n\n    def search(\n        self,\n        query: Optional[Union[Dict, str]] = None,\n        predicate: Optional[SEARCH_PREDICATE_CHOICE] = None,\n        relates: Optional[str] = None,\n        distance: Optional[Tuple[float, float]] = None,\n        acquired: Optional[DATETIME_OR_STR_TUPLE] = None,\n        instrumentation: Optional[str] = None,\n        limit: Optional[int] = None,\n        offset: Optional[int] = None,\n    ) -> List[Dict]:\n        \"\"\"\n        Search for geospatial entries based on various criteria.\n\n        For Ranges (Tuples), an entry of `None` means that side of the range is unbounded.\n        E.g. a range of (2, None) is 2 or more, (None, 5) is at most 5, (2, 5) is between 2 and 5.\n\n        Args:\n            query: Either a WKT GeoJSON representation, a GeoJSON string, or a GeoJSON dict.\n            predicate: A named spatial predicate based on the DE-9IM. This spatial predicate will\n                be used to filter data such that predicate(a, b) where b is the queried geometry.\n            relates: Specify exactly how the queried geometry should relate to the data using a\n                DE-9IM string code.\n            distance: The min/max distance around the queried geometry in meters.\n            acquired: The min/max date and time (ISO 8601) when data was acquired.\n            instrumentation: The instrumentation used to acquire at least one of these data.\n            limit: The maximum number of results to return.\n            offset: The number of results to skip.\n\n        Returns:\n            A list of Spatial Entries.\n        \"\"\"\n        params = spatial_search_params(\n            query=query,\n            predicate=predicate,\n            relates=relates,\n            distance=distance,\n            acquired=acquired,\n            instrumentation=instrumentation,\n            limit=limit,\n            offset=offset,\n        )\n\n        r = self.session.get('rgd/search', params=params)\n        r.raise_for_status()\n\n        return r.json()\n\n    def create_raster_stac(self, raster: Dict) -> Dict:\n        \"\"\"Create a raster entry using STAC format.\"\"\"\n        r = self.session.post('rgd_imagery/raster/stac', json=raster)\n        r.raise_for_status()\n\n        return r.json()\n\n    def search_raster_stac(\n        self,\n        query: Optional[Union[Dict, str]] = None,\n        predicate: Optional[SEARCH_PREDICATE_CHOICE] = None,\n        relates: Optional[str] = None,\n        distance: Optional[Tuple[float, float]] = None,\n        acquired: Optional[DATETIME_OR_STR_TUPLE] = None,\n        instrumentation: Optional[str] = None,\n        num_bands: Optional[Tuple[int, int]] = None,\n        resolution: Optional[Tuple[int, int]] = None,\n        cloud_cover: Optional[Tuple[float, float]] = None,\n        limit: Optional[int] = None,\n        offset: Optional[int] = None,\n    ) -> List[Dict]:\n        \"\"\"\n        Search for raster entries based on various criteria.\n\n        For Ranges (Tuples), an entry of `None` means that side of the range is unbounded.\n        E.g. a range of (2, None) is 2 or more, (None, 5) is at most 5, (2, 5) is between 2 and 5.\n\n        Args:\n            query: Either a WKT GeoJSON representation, a GeoJSON string, or a GeoJSON dict.\n            predicate: A named spatial predicate based on the DE-9IM. This spatial predicate will\n                be used to filter data such that predicate(a, b) where b is the queried geometry.\n            relates: Specify exactly how the queried geometry should relate to the data using a\n                DE-9IM string code.\n            distance: The min/max distance around the queried geometry in meters.\n            acquired: The min/max date and time (ISO 8601) when data was acquired.\n            instrumentation: The instrumentation used to acquire at least one of these data.\n            num_bands: The min/max number of bands in the raster.\n            resolution: The min/max resolution of the raster.\n            cloud_cover: The min/max cloud coverage of the raster.\n            limit: The maximum number of results to return.\n            offset: The number of results to skip.\n\n        Returns:\n            A list of Spatial Entries in STAC Item format.\n        \"\"\"\n        params = spatial_search_params(\n            query=query,\n            predicate=predicate,\n            relates=relates,\n            distance=distance,\n            acquired=acquired,\n            instrumentation=instrumentation,\n            limit=limit,\n            offset=offset,\n        )\n\n        if num_bands and len(num_bands) == 2:\n            nbmin, nbmax = num_bands\n            params['num_bands_min'] = nbmin\n            params['num_bands_max'] = nbmax\n\n        if resolution and len(resolution) == 2:\n            rmin, rmax = resolution\n            params['resolution_min'] = rmin\n            params['resolution_max'] = rmax\n\n        if cloud_cover and len(cloud_cover) == 2:\n            ccmin, ccmax = cloud_cover\n            params['cloud_cover_min'] = ccmin\n            params['cloud_cover_max'] = ccmax\n\n        return list(limit_offset_pager(self.session, 'rgd_imagery/raster/search', params=params))\n", "path": "rgd-client/rgd_client/rgdc.py"}]}