Skip to content

Commit c164de2

Browse files
authored
feat: add names to clients (#239)
* feat: add name to client * feat: add client override to cli * chore: update changelog * feat: dynamically look up subclasses
1 parent fa1d83d commit c164de2

File tree

10 files changed

+66
-9
lines changed

10 files changed

+66
-9
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
99
### Added
1010

1111
- `--http-header` CLI argument ([#238](https://github.com/stac-utils/stac-asset/pull/238))
12+
- `--client` CLI argument, names to clients ([#239](https://github.com/stac-utils/stac-asset/pull/239))
1213

1314
## [0.4.5] - 2024-10-29
1415

src/stac_asset/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
open_href,
2525
read_href,
2626
)
27-
from .client import Client
27+
from .client import Client, get_client_classes
2828
from .config import Config
2929
from .earthdata_client import EarthdataClient
3030
from .errors import (
@@ -65,6 +65,7 @@
6565
"download_item",
6666
"download_item_collection",
6767
"download_file",
68+
"get_client_classes",
6869
"open_href",
6970
"read_href",
7071
]

src/stac_asset/_cli.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from pystac import Asset, Item, ItemCollection
1919

2020
from . import Config, ErrorStrategy, _functions
21-
from .client import Clients
21+
from .client import Clients, get_client_classes
2222
from .config import (
2323
DEFAULT_HTTP_CLIENT_TIMEOUT,
2424
DEFAULT_HTTP_MAX_ATTEMPTS,
@@ -61,6 +61,13 @@ def cli() -> None:
6161
@cli.command()
6262
@click.argument("href", required=False)
6363
@click.argument("directory", required=False)
64+
@click.option(
65+
"-c",
66+
"--client",
67+
type=Choice([c.name for c in get_client_classes()]),
68+
help="Set the client to use for all downloads. If not "
69+
"provided, the client will be guessed from the asset href.",
70+
)
6471
@click.option(
6572
"-p",
6673
"--path-template",
@@ -164,6 +171,7 @@ def cli() -> None:
164171
def download(
165172
href: str | None,
166173
directory: str | None,
174+
client: str | None,
167175
path_template: str | None,
168176
alternate_assets: list[str],
169177
include: list[str],
@@ -213,6 +221,7 @@ def download(
213221
download_async(
214222
href,
215223
directory,
224+
client,
216225
path_template,
217226
alternate_assets,
218227
include,
@@ -237,6 +246,7 @@ def download(
237246
async def download_async(
238247
href: str | None,
239248
directory: str | None,
249+
client: str | None,
240250
path_template: str | None,
241251
alternate_assets: list[str],
242252
include: list[str],
@@ -275,6 +285,7 @@ async def download_async(
275285
warn=not fail_fast,
276286
fail_fast=fail_fast,
277287
overwrite=overwrite,
288+
client_override=client,
278289
)
279290

280291
input_dict = await read_as_dict(href, config)

src/stac_asset/client.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
class Client(ABC):
2323
"""An abstract base class for all clients."""
2424

25+
name: str
26+
"""The name of this client."""
27+
2528
@classmethod
2629
async def from_config(cls: type[T], config: Config) -> T:
2730
"""Creates a client using the provided configuration.
@@ -184,7 +187,7 @@ class Clients:
184187
"""An async-safe cache of clients."""
185188

186189
lock: Lock
187-
clients: dict[type[Client], Client]
190+
clients: dict[str, Client]
188191
config: Config
189192

190193
def __init__(self, config: Config, clients: list[Client] | None = None) -> None:
@@ -193,7 +196,7 @@ def __init__(self, config: Config, clients: list[Client] | None = None) -> None:
193196
if clients:
194197
# TODO check for duplicate types in clients list
195198
for client in clients:
196-
self.clients[type(client)] = client
199+
self.clients[client.name] = client
197200
self.config = config
198201

199202
async def get_client(self, href: str) -> Client:
@@ -205,15 +208,21 @@ async def get_client(self, href: str) -> Client:
205208
Returns:
206209
Client: An instance of that client.
207210
"""
211+
# TODO allow dynamic registration of new clients, e.g. via a plugin mechanism
212+
208213
from .earthdata_client import EarthdataClient
209214
from .filesystem_client import FilesystemClient
210215
from .http_client import HttpClient
211216
from .planetary_computer_client import PlanetaryComputerClient
212217
from .s3_client import S3Client
213218

214219
url = URL(href)
215-
if not url.host:
216-
client_class: type[Client] = FilesystemClient
220+
if self.config.client_override:
221+
client_class: type[Client] = _get_client_class_by_name(
222+
self.config.client_override
223+
)
224+
elif not url.host:
225+
client_class = FilesystemClient
217226
elif url.scheme == "s3":
218227
client_class = S3Client
219228
elif url.host.endswith("blob.core.windows.net"):
@@ -226,15 +235,34 @@ async def get_client(self, href: str) -> Client:
226235
raise ValueError(f"could not guess client class for href: {href}")
227236

228237
async with self.lock:
229-
if client_class in self.clients:
230-
return self.clients[client_class]
238+
if client_class.name in self.clients:
239+
return self.clients[client_class.name]
231240
else:
232241
client = await client_class.from_config(self.config)
233-
self.clients[client_class] = client
242+
self.clients[client_class.name] = client
234243
return client
235244

236245
async def close_all(self) -> None:
237246
"""Close all clients."""
238247
async with self.lock:
239248
for client in self.clients.values():
240249
await client.close()
250+
251+
252+
def _get_client_class_by_name(name: str) -> type[Client]:
253+
for client_class in get_client_classes():
254+
if client_class.name == name:
255+
return client_class
256+
raise ValueError(f"no client with name: {name}")
257+
258+
259+
def get_client_classes() -> list[type[Client]]:
260+
"""Returns a list of all known subclasses of Client."""
261+
262+
# https://stackoverflow.com/questions/3862310/how-to-find-all-the-subclasses-of-a-class-given-its-name
263+
def all_subclasses(cls: type[Client]) -> set[type[Client]]:
264+
return set(cls.__subclasses__()).union(
265+
[s for c in cls.__subclasses__() for s in all_subclasses(c)]
266+
)
267+
268+
return list(all_subclasses(Client)) # type: ignore

src/stac_asset/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ class Config:
6161
overwrite: bool = False
6262
"""Download files even if they already exist locally."""
6363

64+
client_override: str | None = None
65+
"""Use the same client for all asset requests.
66+
67+
If not set, each asset's client will be guessed from its href.
68+
"""
69+
6470
http_client_timeout: float | None = DEFAULT_HTTP_CLIENT_TIMEOUT
6571
"""Total number of seconds for the whole request."""
6672

src/stac_asset/earthdata_client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ class EarthdataClient(HttpClient):
1919
3. Use :py:meth:`EarthdataClient.from_config()` to create a new client.
2020
"""
2121

22+
name = "earthdata"
23+
2224
@classmethod
2325
async def from_config(cls, config: Config) -> EarthdataClient:
2426
"""Logs in to Earthdata and returns the default earthdata client.

src/stac_asset/filesystem_client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ class FilesystemClient(Client):
1818
Mostly used for testing, but could be useful in some real-world cases.
1919
"""
2020

21+
name = "filesystem"
22+
2123
async def open_url(
2224
self,
2325
url: URL,

src/stac_asset/http_client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ class HttpClient(Client):
2929
Configure the session to customize its behavior.
3030
"""
3131

32+
name = "http"
33+
3234
@classmethod
3335
async def from_config(cls: type[T], config: Config) -> T:
3436
"""Creates an HTTP client with an aiohttp session object.

src/stac_asset/planetary_computer_client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ class PlanetaryComputerClient(HttpClient):
5353
thanks Tom Augspurger!
5454
"""
5555

56+
name = "planetary-computer"
57+
5658
def __init__(
5759
self,
5860
session: ClientSession,

src/stac_asset/s3_client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ class S3Client(Client):
3131
for instructions.
3232
"""
3333

34+
name = "s3"
35+
3436
@classmethod
3537
async def from_config(cls, config: Config) -> S3Client:
3638
"""Creates an s3 client from a config.

0 commit comments

Comments
 (0)