30
30
unicode_literals ,
31
31
)
32
32
33
+ from builtins import *
34
+
33
35
from future import standard_library
34
36
standard_library .install_aliases ()
35
37
38
+ import json
39
+ import logging
40
+ import platform
41
+ import sys
36
42
import time
43
+ import urllib
37
44
import urllib .parse
38
45
import warnings
39
- from builtins import *
40
46
41
47
import requests
42
- import urllib
43
- import platform
44
- import sys
45
- import json
46
48
from past .builtins import basestring
47
49
50
+ from ._metadata import __title__ , __version__
48
51
from .config import DEFAULT_SINGLE_REQUEST_TIMEOUT , DEFAULT_WAIT_ON_RATE_LIMIT
49
52
from .exceptions import MalformedResponse , RateLimitError , RateLimitWarning
50
53
from .response_codes import EXPECTED_RESPONSE_CODE
51
54
from .utils import (
52
55
check_response_code , check_type , extract_and_parse_json , validate_base_url ,
53
56
)
54
57
55
- from webexteamssdk ._version import get_versions
58
+
59
+ logger = logging .getLogger (__name__ )
60
+
56
61
57
62
# Helper Functions
58
63
def _fix_next_url (next_url ):
59
64
"""Remove max=null parameter from URL.
60
65
61
- Patch for Webex Teams Defect: ' next' URL returned in the Link headers of
62
- the responses contain an errant ' max=null' parameter, which causes the
66
+ Patch for Webex Teams Defect: " next" URL returned in the Link headers of
67
+ the responses contain an errant " max=null" parameter, which causes the
63
68
next request (to this URL) to fail if the URL is requested as-is.
64
69
65
70
This patch parses the next_url to remove the max=null parameter.
66
71
67
72
Args:
68
- next_url(basestring): The ' next' URL to be parsed and cleaned.
73
+ next_url(basestring): The " next" URL to be parsed and cleaned.
69
74
70
75
Returns:
71
- basestring: The clean URL to be used for the ' next' request.
76
+ basestring: The clean URL to be used for the " next" request.
72
77
73
78
Raises:
74
79
AssertionError: If the parameter types are incorrect.
75
- ValueError: If ' next_url' does not contain a valid API endpoint URL
80
+ ValueError: If " next_url" does not contain a valid API endpoint URL
76
81
(scheme, netloc and path).
77
82
78
83
"""
@@ -81,23 +86,89 @@ def _fix_next_url(next_url):
81
86
82
87
if not parsed_url .scheme or not parsed_url .netloc or not parsed_url .path :
83
88
raise ValueError (
84
- "' next_url' must be a valid API endpoint URL, minimally "
89
+ "` next_url` must be a valid API endpoint URL, minimally "
85
90
"containing a scheme, netloc and path."
86
91
)
87
92
88
93
if parsed_url .query :
89
- query_list = parsed_url .query .split ('&' )
90
- if ' max=null' in query_list :
91
- query_list .remove (' max=null' )
94
+ query_list = parsed_url .query .split ("&" )
95
+ if " max=null" in query_list :
96
+ query_list .remove (" max=null" )
92
97
warnings .warn ("`max=null` still present in next-URL returned "
93
98
"from Webex Teams" , RuntimeWarning )
94
- new_query = '&' .join (query_list )
99
+ new_query = "&" .join (query_list )
95
100
parsed_url = list (parsed_url )
96
101
parsed_url [4 ] = new_query
97
102
98
103
return urllib .parse .urlunparse (parsed_url )
99
104
100
105
106
+ def user_agent (be_geo_id = None , caller = None ):
107
+ """Build a User-Agent HTTP header string."""
108
+
109
+ product = __title__
110
+ version = __version__
111
+
112
+ # Add platform data to comment portion of the User-Agent header.
113
+ # Inspired by PIP"s User-Agent header; serialize the data in JSON format.
114
+ # https://github.com/pypa/pip/blob/master/src/pip/_internal/network
115
+ data = dict ()
116
+
117
+ # Python implementation
118
+ data ["implementation" ] = {
119
+ "name" : platform .python_implementation (),
120
+ }
121
+
122
+ # Implementation version
123
+ if data ["implementation" ]["name" ] == "CPython" :
124
+ data ["implementation" ]["version" ] = platform .python_version ()
125
+
126
+ elif data ["implementation" ]["name" ] == "PyPy" :
127
+ if sys .pypy_version_info .releaselevel == "final" :
128
+ pypy_version_info = sys .pypy_version_info [:3 ]
129
+ else :
130
+ pypy_version_info = sys .pypy_version_info
131
+ data ["implementation" ]["version" ] = "." .join (
132
+ [str (x ) for x in pypy_version_info ]
133
+ )
134
+ elif data ["implementation" ]["name" ] == "Jython" :
135
+ data ["implementation" ]["version" ] = platform .python_version ()
136
+ elif data ["implementation" ]["name" ] == "IronPython" :
137
+ data ["implementation" ]["version" ] = platform .python_version ()
138
+
139
+ # Platform information
140
+ if sys .platform .startswith ("darwin" ) and platform .mac_ver ()[0 ]:
141
+ dist = {"name" : "macOS" , "version" : platform .mac_ver ()[0 ]}
142
+ data ["distro" ] = dist
143
+
144
+ if platform .system ():
145
+ data .setdefault ("system" , {})["name" ] = platform .system ()
146
+
147
+ if platform .release ():
148
+ data .setdefault ("system" , {})["release" ] = platform .release ()
149
+
150
+ if platform .machine ():
151
+ data ["cpu" ] = platform .machine ()
152
+
153
+ # Add self-identified organization information to the User-Agent Header.
154
+ if be_geo_id :
155
+ data ["organization" ]["be_geo_id" ] = be_geo_id
156
+
157
+ if caller :
158
+ data ["organization" ]["caller" ] = caller
159
+
160
+ # Create the User-Agent string
161
+ user_agent_string = "{product}/{version} {comment}" .format (
162
+ product = product ,
163
+ version = version ,
164
+ comment = json .dumps (data ),
165
+ )
166
+
167
+ logger .info ("User-Agent: " + user_agent_string )
168
+
169
+ return user_agent_string
170
+
171
+
101
172
# Main module interface
102
173
class RestSession (object ):
103
174
"""RESTful HTTP session class for making calls to the Webex Teams APIs."""
@@ -146,66 +217,18 @@ def __init__(self, access_token, base_url,
146
217
self ._single_request_timeout = single_request_timeout
147
218
self ._wait_on_rate_limit = wait_on_rate_limit
148
219
149
- # Initialize a new `requests` session
220
+ # Initialize a new session
150
221
self ._req_session = requests .session ()
151
222
152
223
if proxies is not None :
153
224
self ._req_session .proxies .update (proxies )
154
225
155
- # Build a User-Agent header
156
- ua_base = 'python-webexteams/' + get_versions ()['version' ] + ' '
157
-
158
- # Generate extended portion of the User-Agent
159
- ua_ext = {}
160
-
161
- # Mimic pip system data collection per
162
- # https://github.com/pypa/pip/blob/master/src/pip/_internal/network/session.py
163
- ua_ext ['implementation' ] = {
164
- "name" : platform .python_implementation (),
165
- }
166
-
167
- if ua_ext ["implementation" ]["name" ] == 'CPython' :
168
- ua_ext ["implementation" ]["version" ] = platform .python_version ()
169
- elif ua_ext ["implementation" ]["name" ] == 'PyPy' :
170
- if sys .pypy_version_info .releaselevel == 'final' :
171
- pypy_version_info = sys .pypy_version_info [:3 ]
172
- else :
173
- pypy_version_info = sys .pypy_version_info
174
- ua_ext ["implementation" ]["version" ] = "." .join (
175
- [str (x ) for x in pypy_version_info ]
176
- )
177
- elif ua_ext ["implementation" ]["name" ] == 'Jython' :
178
- ua_ext ["implementation" ]["version" ] = platform .python_version ()
179
- elif ua_ext ["implementation" ]["name" ] == 'IronPython' :
180
- ua_ext ["implementation" ]["version" ] = platform .python_version ()
181
-
182
- if sys .platform .startswith ("darwin" ) and platform .mac_ver ()[0 ]:
183
- dist = {"name" : "macOS" , "version" : platform .mac_ver ()[0 ]}
184
- ua_ext ["distro" ] = dist
185
-
186
- if platform .system ():
187
- ua_ext .setdefault ("system" , {})["name" ] = platform .system ()
188
-
189
- if platform .release ():
190
- ua_ext .setdefault ("system" , {})["release" ] = platform .release ()
191
-
192
- if platform .machine ():
193
- ua_ext ["cpu" ] = platform .machine ()
194
-
195
- if be_geo_id :
196
- ua_ext ["be_geo_id" ] = be_geo_id
197
-
198
- if caller :
199
- ua_ext ["caller" ] = caller
200
-
201
- # Override the default requests User-Agent but not other headers
202
- new_ua = ua_base + urllib .parse .quote (json .dumps (ua_ext ))
203
- self ._req_session .headers ['User-Agent' ] = new_ua
204
-
205
- # Update the headers of the `requests` session
206
- self .update_headers ({'Authorization' : 'Bearer ' + access_token ,
207
- 'Content-type' : 'application/json;charset=utf-8' })
208
- print (self ._req_session .headers )
226
+ # Update the HTTP headers for the session
227
+ self .update_headers ({
228
+ "Authorization" : "Bearer " + access_token ,
229
+ "Content-type" : "application/json;charset=utf-8" ,
230
+ "User-Agent" : user_agent (be_geo_id = be_geo_id , caller = caller ),
231
+ })
209
232
210
233
@property
211
234
def base_url (self ):
@@ -296,7 +319,7 @@ def request(self, method, url, erc, **kwargs):
296
319
* Inspects response codes and raises exceptions as appropriate
297
320
298
321
Args:
299
- method(basestring): The request-method type (' GET', ' POST' , etc.).
322
+ method(basestring): The request-method type (" GET", " POST" , etc.).
300
323
url(basestring): The URL of the API endpoint to be called.
301
324
erc(int): The expected response code that should be returned by the
302
325
Webex Teams API endpoint to indicate success.
@@ -311,7 +334,7 @@ def request(self, method, url, erc, **kwargs):
311
334
abs_url = self .abs_url (url )
312
335
313
336
# Update request kwargs with session defaults
314
- kwargs .setdefault (' timeout' , self .single_request_timeout )
337
+ kwargs .setdefault (" timeout" , self .single_request_timeout )
315
338
316
339
while True :
317
340
# Make the HTTP request to the API endpoint
@@ -352,9 +375,9 @@ def get(self, url, params=None, **kwargs):
352
375
check_type (params , dict , optional = True )
353
376
354
377
# Expected response code
355
- erc = kwargs .pop (' erc' , EXPECTED_RESPONSE_CODE [' GET' ])
378
+ erc = kwargs .pop (" erc" , EXPECTED_RESPONSE_CODE [" GET" ])
356
379
357
- response = self .request (' GET' , url , erc , params = params , ** kwargs )
380
+ response = self .request (" GET" , url , erc , params = params , ** kwargs )
358
381
return extract_and_parse_json (response )
359
382
360
383
def get_pages (self , url , params = None , ** kwargs ):
@@ -378,33 +401,33 @@ def get_pages(self, url, params=None, **kwargs):
378
401
check_type (params , dict , optional = True )
379
402
380
403
# Expected response code
381
- erc = kwargs .pop (' erc' , EXPECTED_RESPONSE_CODE [' GET' ])
404
+ erc = kwargs .pop (" erc" , EXPECTED_RESPONSE_CODE [" GET" ])
382
405
383
406
# First request
384
- response = self .request (' GET' , url , erc , params = params , ** kwargs )
407
+ response = self .request (" GET" , url , erc , params = params , ** kwargs )
385
408
386
409
while True :
387
410
yield extract_and_parse_json (response )
388
411
389
- if response .links .get (' next' ):
390
- next_url = response .links .get (' next' ).get (' url' )
412
+ if response .links .get (" next" ):
413
+ next_url = response .links .get (" next" ).get (" url" )
391
414
392
- # Patch for Webex Teams ' max=null' in next URL bug.
415
+ # Patch for Webex Teams " max=null" in next URL bug.
393
416
# Testing shows that patch is no longer needed; raising a
394
417
# warnning if it is still taking effect;
395
418
# considering for future removal
396
419
next_url = _fix_next_url (next_url )
397
420
398
421
# Subsequent requests
399
- response = self .request (' GET' , next_url , erc , ** kwargs )
422
+ response = self .request (" GET" , next_url , erc , ** kwargs )
400
423
401
424
else :
402
425
break
403
426
404
427
def get_items (self , url , params = None , ** kwargs ):
405
428
"""Return a generator that GETs and yields individual JSON `items`.
406
429
407
- Yields individual `items` from Webex Teams' s top-level {' items' : [...]}
430
+ Yields individual `items` from Webex Teams" s top-level {" items" : [...]}
408
431
JSON objects. Provides native support for RFC5988 Web Linking. The
409
432
generator will request additional pages as needed until all items have
410
433
been returned.
@@ -420,7 +443,7 @@ def get_items(self, url, params=None, **kwargs):
420
443
ApiError: If anything other than the expected response code is
421
444
returned by the Webex Teams API endpoint.
422
445
MalformedResponse: If the returned response does not contain a
423
- top-level dictionary with an ' items' key.
446
+ top-level dictionary with an " items" key.
424
447
425
448
"""
426
449
# Get generator for pages of JSON data
@@ -429,7 +452,7 @@ def get_items(self, url, params=None, **kwargs):
429
452
for json_page in pages :
430
453
assert isinstance (json_page , dict )
431
454
432
- items = json_page .get (' items' )
455
+ items = json_page .get (" items" )
433
456
434
457
if items is None :
435
458
error_message = "'items' key not found in JSON data: " \
@@ -459,9 +482,9 @@ def post(self, url, json=None, data=None, **kwargs):
459
482
check_type (url , basestring )
460
483
461
484
# Expected response code
462
- erc = kwargs .pop (' erc' , EXPECTED_RESPONSE_CODE [' POST' ])
485
+ erc = kwargs .pop (" erc" , EXPECTED_RESPONSE_CODE [" POST" ])
463
486
464
- response = self .request (' POST' , url , erc , json = json , data = data ,
487
+ response = self .request (" POST" , url , erc , json = json , data = data ,
465
488
** kwargs )
466
489
return extract_and_parse_json (response )
467
490
@@ -484,9 +507,9 @@ def put(self, url, json=None, data=None, **kwargs):
484
507
check_type (url , basestring )
485
508
486
509
# Expected response code
487
- erc = kwargs .pop (' erc' , EXPECTED_RESPONSE_CODE [' PUT' ])
510
+ erc = kwargs .pop (" erc" , EXPECTED_RESPONSE_CODE [" PUT" ])
488
511
489
- response = self .request (' PUT' , url , erc , json = json , data = data ,
512
+ response = self .request (" PUT" , url , erc , json = json , data = data ,
490
513
** kwargs )
491
514
return extract_and_parse_json (response )
492
515
@@ -507,6 +530,6 @@ def delete(self, url, **kwargs):
507
530
check_type (url , basestring )
508
531
509
532
# Expected response code
510
- erc = kwargs .pop (' erc' , EXPECTED_RESPONSE_CODE [' DELETE' ])
533
+ erc = kwargs .pop (" erc" , EXPECTED_RESPONSE_CODE [" DELETE" ])
511
534
512
- self .request (' DELETE' , url , erc , ** kwargs )
535
+ self .request (" DELETE" , url , erc , ** kwargs )
0 commit comments