Skip to content

Commit e4f9641

Browse files
committed
feat(s3): tag S3 client user-agent for attribution
Signed-off-by: Gonzalo Peña-Castellanos <goanpeca@gmail.com>
1 parent 976bc4c commit e4f9641

3 files changed

Lines changed: 80 additions & 5 deletions

File tree

python/whylogs/api/_s3_client.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""Shared construction for the boto3 S3 clients that whylogs builds itself.
2+
3+
Works with Amazon S3 and any S3-compatible object store (for example
4+
Cloudflare R2, MinIO, or Backblaze B2) by passing an ``endpoint_url`` and
5+
matching credentials.
6+
"""
7+
8+
from typing import Any, Optional
9+
10+
import boto3
11+
from botocore.client import BaseClient
12+
from botocore.config import Config
13+
14+
15+
def _whylogs_version() -> str:
16+
try:
17+
from importlib import metadata
18+
except ImportError: # Python < 3.8
19+
import importlib_metadata as metadata # type: ignore
20+
21+
try:
22+
return metadata.version("whylogs")
23+
except metadata.PackageNotFoundError: # type: ignore
24+
return "dev"
25+
26+
27+
def build_s3_client(config: Optional[Config] = None, **kwargs: Any) -> BaseClient:
28+
"""Build an S3 client for clients whylogs creates itself.
29+
30+
The whylogs identifier is appended to ``user_agent_extra`` so it is added to,
31+
not substituted for, any value already present on a caller-supplied ``config``.
32+
A custom ``endpoint_url`` for an S3-compatible store may be passed through
33+
``kwargs``.
34+
"""
35+
suffix = f"whylogs/{_whylogs_version()}"
36+
existing = getattr(config, "user_agent_extra", None)
37+
user_agent_extra = f"{existing} {suffix}" if existing else suffix
38+
config = (config or Config()).merge(Config(user_agent_extra=user_agent_extra))
39+
return boto3.client("s3", config=config, **kwargs)

python/whylogs/api/reader/s3.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
from tempfile import NamedTemporaryFile
22
from typing import Optional
33

4-
import boto3
54
from botocore.client import BaseClient
5+
from botocore.config import Config
66

77
from whylogs import ResultSet
8+
from whylogs.api._s3_client import build_s3_client
89
from whylogs.api.reader.reader import Reader
910

1011

@@ -15,6 +16,10 @@ class S3Reader(Reader):
1516
>**IMPORTANT**: In order to correctly connect to your Amazon S3 bucket, make sure you have
1617
the following environment variables set: `[AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY]`.
1718
19+
The same reader targets any S3-compatible object store by passing an ``endpoint_url``
20+
(and matching credentials). To reuse a fully configured client instead, pass it as
21+
``s3_client`` and it is used as provided.
22+
1823
Parameters
1924
----------
2025
bucket_name: str, optional
@@ -23,6 +28,13 @@ class S3Reader(Reader):
2328
object_name: str, optional
2429
The s3's object name. It basically states the location where the file goes to.
2530
Also made optional, so it can be defined through the `option` method
31+
s3_client: BaseClient, optional
32+
A boto3 S3 client. When omitted, a client is built internally.
33+
endpoint_url: str, optional
34+
Endpoint URL of an S3-compatible object store. Leave unset for AWS S3.
35+
Only applies to the internally built client.
36+
config: botocore.config.Config, optional
37+
Extra botocore configuration merged into the internally built client.
2638
2739
Examples
2840
--------
@@ -40,8 +52,10 @@ def __init__(
4052
object_name: Optional[str] = None,
4153
bucket_name: Optional[str] = None,
4254
s3_client: Optional[BaseClient] = None,
55+
endpoint_url: Optional[str] = None,
56+
config: Optional[Config] = None,
4357
):
44-
self.s3_client = s3_client or boto3.client("s3")
58+
self.s3_client = s3_client or build_s3_client(config=config, endpoint_url=endpoint_url)
4559
self.object_name = object_name or None
4660
self.bucket_name = bucket_name or ""
4761

python/whylogs/api/writer/s3.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
import os
33
from typing import Any, List, Optional, Tuple, Union
44

5-
import boto3
65
from botocore.client import BaseClient
6+
from botocore.config import Config
77
from botocore.exceptions import ClientError
88

9+
from whylogs.api._s3_client import build_s3_client
910
from whylogs.api.writer import Writer
1011
from whylogs.api.writer.writer import _Writable
1112
from whylogs.core.utils import deprecated_alias
@@ -20,11 +21,16 @@ class S3Writer(Writer):
2021
>**IMPORTANT**: In order to correctly connect to your Amazon S3 bucket, make sure you have
2122
the following environment variables set: `[AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY]`
2223
24+
The same writer targets any S3-compatible object store by passing an ``endpoint_url``
25+
(and matching credentials). To reuse a fully configured client instead, pass it as
26+
``s3_client`` and it is used as provided.
27+
2328
Parameters
2429
----------
2530
s3_client: BaseClient, optional
2631
The s3 client used to authenticate and perform operations on the s3 bucket.
27-
Should be a BaseClient from the boto3 library
32+
Should be a BaseClient from the boto3 library. When omitted, a client is built
33+
internally.
2834
base_prefix: str, optional
2935
The base file prefix for s3, in order to organize. A placeholder 'profile' will take place if None is provided.
3036
bucket_name: str, optional
@@ -33,6 +39,11 @@ class S3Writer(Writer):
3339
object_name: str, optional
3440
The s3's object name. It basically states the location where the file goes to.
3541
Also made optional, so it can be defined through the `option` method
42+
endpoint_url: str, optional
43+
Endpoint URL of an S3-compatible object store. Leave unset for AWS S3.
44+
Only applies to the internally built client.
45+
config: botocore.config.Config, optional
46+
Extra botocore configuration merged into the internally built client.
3647
Returns
3748
-------
3849
None
@@ -49,6 +60,15 @@ class S3Writer(Writer):
4960
profile.writer("s3").option(bucket_name="my_bucket").write()
5061
```
5162
63+
Writing to an S3-compatible endpoint:
64+
65+
```python
66+
profile.writer("s3").option(
67+
bucket_name="my_bucket",
68+
endpoint_url="https://<s3-compatible-endpoint>",
69+
).write()
70+
```
71+
5272
"""
5373

5474
def __init__(
@@ -57,8 +77,10 @@ def __init__(
5777
base_prefix: Optional[str] = None,
5878
bucket_name: Optional[str] = None,
5979
object_name: Optional[str] = None,
80+
endpoint_url: Optional[str] = None,
81+
config: Optional[Config] = None,
6082
):
61-
self.s3_client = s3_client or boto3.client("s3")
83+
self.s3_client = s3_client or build_s3_client(config=config, endpoint_url=endpoint_url)
6284
self.base_prefix = base_prefix or "profile"
6385
self.bucket_name = bucket_name or ""
6486
self.object_name = object_name or None

0 commit comments

Comments
 (0)