29
29
import webbrowser
30
30
import zipfile
31
31
from concurrent .futures import ThreadPoolExecutor
32
+ from copy import deepcopy
32
33
from http .client import HTTPResponse
33
34
from typing import Any , Dict , List , Optional , Tuple , Union
34
35
210
211
mimetypes .add_type ("text/x-markdown" , ".markdown" , strict = False )
211
212
212
213
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]+$"
213
216
214
217
215
218
def login (* args , ** kwargs ):
@@ -252,17 +255,61 @@ class Synapse(object):
252
255
when making http requests.
253
256
cache_root_dir: Root directory for storing cache data
254
257
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.
255
283
256
284
Example: Getting started
257
285
Logging in to Synapse using an authToken
258
286
287
+ ```python
259
288
import synapseclient
260
289
syn = synapseclient.login(authToken="authtoken")
290
+ ```
261
291
262
292
Using environment variable or `.synapseConfig`
263
293
294
+ ```python
264
295
import synapseclient
265
296
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
+ ```
266
313
267
314
"""
268
315
@@ -287,6 +334,7 @@ def __init__(
287
334
requests_session_storage : httpx .Client = None ,
288
335
asyncio_event_loop : asyncio .AbstractEventLoop = None ,
289
336
cache_client : bool = True ,
337
+ user_agent : Union [str , List [str ]] = None ,
290
338
) -> "Synapse" :
291
339
"""
292
340
Initialize Synapse object
@@ -316,6 +364,15 @@ def __init__(
316
364
When working in a multi-user environment it is
317
365
recommended to set this to False, or use
318
366
`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.
319
376
320
377
Raises:
321
378
ValueError: Warn for non-boolean debug value.
@@ -372,6 +429,12 @@ def __init__(
372
429
}
373
430
self .credentials = None
374
431
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
+
375
438
self .silent = silent
376
439
self ._init_logger () # initializes self.logger
377
440
@@ -393,6 +456,24 @@ def __init__(
393
456
if cache_client and Synapse ._allow_client_caching :
394
457
Synapse .set_client (synapse_client = self )
395
458
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
+
396
477
def _get_requests_session_async_synapse (
397
478
self , asyncio_event_loop : asyncio .AbstractEventLoop
398
479
) -> httpx .AsyncClient :
@@ -6010,14 +6091,32 @@ def sendMessage(
6010
6091
# Low level Rest calls #
6011
6092
############################################################
6012
6093
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 ]:
6014
6097
"""
6015
6098
Generate headers (auth headers produced separately by credentials object)
6016
6099
6017
6100
"""
6018
6101
if headers is None :
6019
6102
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
+ )
6021
6120
6022
6121
return headers
6023
6122
@@ -6245,7 +6344,11 @@ def restDELETE(
6245
6344
)
6246
6345
6247
6346
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 ,
6249
6352
) -> Tuple [str , Dict [str , str ]]:
6250
6353
"""Returns a tuple of the URI and headers to request with."""
6251
6354
@@ -6261,7 +6364,7 @@ def _build_uri_and_headers(
6261
6364
uri = endpoint + uri
6262
6365
6263
6366
if headers is None :
6264
- headers = self ._generate_headers ()
6367
+ headers = self ._generate_headers (is_httpx = is_httpx )
6265
6368
return uri , headers
6266
6369
6267
6370
def _build_retry_policy (self , retryPolicy = {}):
@@ -6318,7 +6421,7 @@ async def _rest_call_async(
6318
6421
JSON encoding of response
6319
6422
"""
6320
6423
uri , headers = self ._build_uri_and_headers (
6321
- uri , endpoint = endpoint , headers = headers
6424
+ uri , endpoint = endpoint , headers = headers , is_httpx = True
6322
6425
)
6323
6426
6324
6427
retry_policy = self ._build_retry_policy_async (retry_policy )
0 commit comments