Skip to content

Commit 167d610

Browse files
authored
[SYNPY-1570, SYNPY-1555] Support passing user_agent to the Synapse class constructor, and HTTPX Library usage (#1170)
* Support passing `user_agent` to the Synapse class constructor, and replace the `requests` package with HTTPX
1 parent 2ab4d5f commit 167d610

File tree

2 files changed

+355
-6
lines changed

2 files changed

+355
-6
lines changed

synapseclient/client.py

Lines changed: 108 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import webbrowser
3030
import zipfile
3131
from concurrent.futures import ThreadPoolExecutor
32+
from copy import deepcopy
3233
from http.client import HTTPResponse
3334
from typing import Any, Dict, List, Optional, Tuple, Union
3435

@@ -210,6 +211,8 @@
210211
mimetypes.add_type("text/x-markdown", ".markdown", strict=False)
211212

212213
DEFAULT_STORAGE_LOCATION_ID = 1
214+
# Verifies the pattern matches something like "my-project-identifier/1.0.0"
215+
USER_AGENT_REGEX_PATTERN = r"^[a-zA-Z0-9-]+\/[0-9]+\.[0-9]+\.[0-9]+$"
213216

214217

215218
def login(*args, **kwargs):
@@ -252,17 +255,61 @@ class Synapse(object):
252255
when making http requests.
253256
cache_root_dir: Root directory for storing cache data
254257
silent: Defaults to False.
258+
requests_session_async_synapse: A custom
259+
[httpx.AsyncClient](https://www.python-httpx.org/async/) that this synapse
260+
instance will use when making HTTP requests. `requests_session` is being
261+
deprecated in favor of this.
262+
requests_session_storage: A custom
263+
[httpx.Client](https://www.python-httpx.org/advanced/clients/) that this synapse
264+
instance will use when making HTTP requests to storage providers like AWS S3
265+
and Google Cloud Storage.
266+
asyncio_event_loop: The event loop that is going to be used while executing
267+
this code. This is optional and only used when you are manually specifying
268+
an async HTTPX client. This is important to pass when you are using the
269+
`requests_session_async_synapse` kwarg because the connection pooling is
270+
tied to the event loop.
271+
cache_client: Whether to cache the Synapse client object in the Synapse module.
272+
Defaults to True. When set to True anywhere a `Synapse` object is optional
273+
you do not need to pass an instance of `Synapse` to that function, method,
274+
or class. When working in a multi-user environment it is recommended to set
275+
this to False, or use `Synapse.allow_client_caching(False)`.
276+
user_agent: Additional values to add to the `User-Agent` header on HTTP
277+
requests. This should be in the format of `"my-project-identifier/1.0.0"`.
278+
This may be a single string or a list of strings to add onto the
279+
header. If the format is incorrect a `ValueError` exception will be
280+
raised. These will be appended to the default `User-Agent` header that
281+
already includes the version of this client that you are using, and the
282+
HTTP library used to make the request.
255283
256284
Example: Getting started
257285
Logging in to Synapse using an authToken
258286
287+
```python
259288
import synapseclient
260289
syn = synapseclient.login(authToken="authtoken")
290+
```
261291
262292
Using environment variable or `.synapseConfig`
263293
294+
```python
264295
import synapseclient
265296
syn = synapseclient.login()
297+
```
298+
299+
Example: Adding an additional `user_agent` value
300+
This example shows how to add an additional `user_agent` to the HTTP headers
301+
on the request. This is useful for tracking the requests that are being made
302+
from your application.
303+
304+
```python
305+
from synapseclient import Synapse
306+
307+
my_agent = "my-project-identifier/1.0.0"
308+
# You may also provide a list of strings to add to the User-Agent header.
309+
# my_agent = ["my-sub-library/1.0.0", "my-parent-project/2.0.0"]
310+
311+
syn = Synapse(user_agent=my_agent)
312+
```
266313
267314
"""
268315

@@ -287,6 +334,7 @@ def __init__(
287334
requests_session_storage: httpx.Client = None,
288335
asyncio_event_loop: asyncio.AbstractEventLoop = None,
289336
cache_client: bool = True,
337+
user_agent: Union[str, List[str]] = None,
290338
) -> "Synapse":
291339
"""
292340
Initialize Synapse object
@@ -316,6 +364,15 @@ def __init__(
316364
When working in a multi-user environment it is
317365
recommended to set this to False, or use
318366
`Synapse.allow_client_caching(False)`.
367+
user_agent: Additional values to add to the `User-Agent` header on HTTP
368+
requests. This should be in the format of
369+
`"my-project-identifier/1.0.0"`. Only
370+
[Semantic Versioning](https://semver.org/) is expected.
371+
This may be a single string or a list of strings to add onto the
372+
header. If the format is incorrect a `ValueError` exception will be
373+
raised. These will be appended to the default `User-Agent` header that
374+
already includes the version of this client that you are using, and the
375+
HTTP library used to make the request.
319376
320377
Raises:
321378
ValueError: Warn for non-boolean debug value.
@@ -372,6 +429,12 @@ def __init__(
372429
}
373430
self.credentials = None
374431

432+
self._validate_user_agent_format(user_agent)
433+
if isinstance(user_agent, str):
434+
self.user_agent = [user_agent]
435+
else:
436+
self.user_agent = user_agent
437+
375438
self.silent = silent
376439
self._init_logger() # initializes self.logger
377440

@@ -393,6 +456,24 @@ def __init__(
393456
if cache_client and Synapse._allow_client_caching:
394457
Synapse.set_client(synapse_client=self)
395458

459+
def _validate_user_agent_format(self, agent: Union[str, List[str]]) -> None:
460+
if not agent:
461+
return
462+
463+
if not isinstance(agent, str) and not isinstance(agent, list):
464+
raise ValueError(
465+
f"user_agent must be a string or a list of strings to add to the User-Agent header. Current value: {agent}"
466+
)
467+
468+
if isinstance(agent, str):
469+
if not re.match(USER_AGENT_REGEX_PATTERN, agent):
470+
raise ValueError(
471+
f"user_agent must be in the format of 'my-project-identifier/1.0.0'. Current value: {agent}"
472+
)
473+
else:
474+
for value in agent:
475+
self._validate_user_agent_format(agent=value)
476+
396477
def _get_requests_session_async_synapse(
397478
self, asyncio_event_loop: asyncio.AbstractEventLoop
398479
) -> httpx.AsyncClient:
@@ -6010,14 +6091,32 @@ def sendMessage(
60106091
# Low level Rest calls #
60116092
############################################################
60126093

6013-
def _generate_headers(self, headers: Dict[str, str] = None) -> Dict[str, str]:
6094+
def _generate_headers(
6095+
self, headers: Dict[str, str] = None, is_httpx: bool = False
6096+
) -> Dict[str, str]:
60146097
"""
60156098
Generate headers (auth headers produced separately by credentials object)
60166099
60176100
"""
60186101
if headers is None:
60196102
headers = dict(self.default_headers)
6020-
headers.update(synapseclient.USER_AGENT)
6103+
6104+
# Replace the `requests` package's default user-agent with the httpx user agent
6105+
if is_httpx:
6106+
httpx_agent = f"{httpx.__title__}/{httpx.__version__}"
6107+
requests_agent = requests.utils.default_user_agent()
6108+
agent = deepcopy(synapseclient.USER_AGENT)
6109+
agent["User-Agent"] = agent["User-Agent"].replace(
6110+
requests_agent, httpx_agent
6111+
)
6112+
headers.update(agent)
6113+
else:
6114+
headers.update(synapseclient.USER_AGENT)
6115+
6116+
if self.user_agent:
6117+
headers["User-Agent"] = (
6118+
headers["User-Agent"] + " " + " ".join(self.user_agent)
6119+
)
60216120

60226121
return headers
60236122

@@ -6245,7 +6344,11 @@ def restDELETE(
62456344
)
62466345

62476346
def _build_uri_and_headers(
6248-
self, uri: str, endpoint: str = None, headers: Dict[str, str] = None
6347+
self,
6348+
uri: str,
6349+
endpoint: str = None,
6350+
headers: Dict[str, str] = None,
6351+
is_httpx: bool = False,
62496352
) -> Tuple[str, Dict[str, str]]:
62506353
"""Returns a tuple of the URI and headers to request with."""
62516354

@@ -6261,7 +6364,7 @@ def _build_uri_and_headers(
62616364
uri = endpoint + uri
62626365

62636366
if headers is None:
6264-
headers = self._generate_headers()
6367+
headers = self._generate_headers(is_httpx=is_httpx)
62656368
return uri, headers
62666369

62676370
def _build_retry_policy(self, retryPolicy={}):
@@ -6318,7 +6421,7 @@ async def _rest_call_async(
63186421
JSON encoding of response
63196422
"""
63206423
uri, headers = self._build_uri_and_headers(
6321-
uri, endpoint=endpoint, headers=headers
6424+
uri, endpoint=endpoint, headers=headers, is_httpx=True
63226425
)
63236426

63246427
retry_policy = self._build_retry_policy_async(retry_policy)

0 commit comments

Comments
 (0)