3030 unicode_literals ,
3131)
3232
33+ from builtins import *
34+
3335from future import standard_library
3436standard_library .install_aliases ()
3537
38+ import json
39+ import logging
40+ import platform
41+ import sys
3642import time
43+ import urllib
3744import urllib .parse
3845import warnings
39- from builtins import *
4046
4147import requests
42- import urllib
43- import platform
44- import sys
45- import json
4648from past .builtins import basestring
4749
50+ from ._metadata import __title__ , __version__
4851from .config import DEFAULT_SINGLE_REQUEST_TIMEOUT , DEFAULT_WAIT_ON_RATE_LIMIT
4952from .exceptions import MalformedResponse , RateLimitError , RateLimitWarning
5053from .response_codes import EXPECTED_RESPONSE_CODE
5154from .utils import (
5255 check_response_code , check_type , extract_and_parse_json , validate_base_url ,
5356)
5457
55- from webexteamssdk ._version import get_versions
58+
59+ logger = logging .getLogger (__name__ )
60+
5661
5762# Helper Functions
5863def _fix_next_url (next_url ):
5964 """Remove max=null parameter from URL.
6065
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
6368 next request (to this URL) to fail if the URL is requested as-is.
6469
6570 This patch parses the next_url to remove the max=null parameter.
6671
6772 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.
6974
7075 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.
7277
7378 Raises:
7479 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
7681 (scheme, netloc and path).
7782
7883 """
@@ -81,23 +86,89 @@ def _fix_next_url(next_url):
8186
8287 if not parsed_url .scheme or not parsed_url .netloc or not parsed_url .path :
8388 raise ValueError (
84- "' next_url' must be a valid API endpoint URL, minimally "
89+ "` next_url` must be a valid API endpoint URL, minimally "
8590 "containing a scheme, netloc and path."
8691 )
8792
8893 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" )
9297 warnings .warn ("`max=null` still present in next-URL returned "
9398 "from Webex Teams" , RuntimeWarning )
94- new_query = '&' .join (query_list )
99+ new_query = "&" .join (query_list )
95100 parsed_url = list (parsed_url )
96101 parsed_url [4 ] = new_query
97102
98103 return urllib .parse .urlunparse (parsed_url )
99104
100105
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+
101172# Main module interface
102173class RestSession (object ):
103174 """RESTful HTTP session class for making calls to the Webex Teams APIs."""
@@ -146,66 +217,18 @@ def __init__(self, access_token, base_url,
146217 self ._single_request_timeout = single_request_timeout
147218 self ._wait_on_rate_limit = wait_on_rate_limit
148219
149- # Initialize a new `requests` session
220+ # Initialize a new session
150221 self ._req_session = requests .session ()
151222
152223 if proxies is not None :
153224 self ._req_session .proxies .update (proxies )
154225
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+ })
209232
210233 @property
211234 def base_url (self ):
@@ -296,7 +319,7 @@ def request(self, method, url, erc, **kwargs):
296319 * Inspects response codes and raises exceptions as appropriate
297320
298321 Args:
299- method(basestring): The request-method type (' GET', ' POST' , etc.).
322+ method(basestring): The request-method type (" GET", " POST" , etc.).
300323 url(basestring): The URL of the API endpoint to be called.
301324 erc(int): The expected response code that should be returned by the
302325 Webex Teams API endpoint to indicate success.
@@ -311,7 +334,7 @@ def request(self, method, url, erc, **kwargs):
311334 abs_url = self .abs_url (url )
312335
313336 # Update request kwargs with session defaults
314- kwargs .setdefault (' timeout' , self .single_request_timeout )
337+ kwargs .setdefault (" timeout" , self .single_request_timeout )
315338
316339 while True :
317340 # Make the HTTP request to the API endpoint
@@ -352,9 +375,9 @@ def get(self, url, params=None, **kwargs):
352375 check_type (params , dict , optional = True )
353376
354377 # Expected response code
355- erc = kwargs .pop (' erc' , EXPECTED_RESPONSE_CODE [' GET' ])
378+ erc = kwargs .pop (" erc" , EXPECTED_RESPONSE_CODE [" GET" ])
356379
357- response = self .request (' GET' , url , erc , params = params , ** kwargs )
380+ response = self .request (" GET" , url , erc , params = params , ** kwargs )
358381 return extract_and_parse_json (response )
359382
360383 def get_pages (self , url , params = None , ** kwargs ):
@@ -378,33 +401,33 @@ def get_pages(self, url, params=None, **kwargs):
378401 check_type (params , dict , optional = True )
379402
380403 # Expected response code
381- erc = kwargs .pop (' erc' , EXPECTED_RESPONSE_CODE [' GET' ])
404+ erc = kwargs .pop (" erc" , EXPECTED_RESPONSE_CODE [" GET" ])
382405
383406 # First request
384- response = self .request (' GET' , url , erc , params = params , ** kwargs )
407+ response = self .request (" GET" , url , erc , params = params , ** kwargs )
385408
386409 while True :
387410 yield extract_and_parse_json (response )
388411
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" )
391414
392- # Patch for Webex Teams ' max=null' in next URL bug.
415+ # Patch for Webex Teams " max=null" in next URL bug.
393416 # Testing shows that patch is no longer needed; raising a
394417 # warnning if it is still taking effect;
395418 # considering for future removal
396419 next_url = _fix_next_url (next_url )
397420
398421 # Subsequent requests
399- response = self .request (' GET' , next_url , erc , ** kwargs )
422+ response = self .request (" GET" , next_url , erc , ** kwargs )
400423
401424 else :
402425 break
403426
404427 def get_items (self , url , params = None , ** kwargs ):
405428 """Return a generator that GETs and yields individual JSON `items`.
406429
407- Yields individual `items` from Webex Teams' s top-level {' items' : [...]}
430+ Yields individual `items` from Webex Teams" s top-level {" items" : [...]}
408431 JSON objects. Provides native support for RFC5988 Web Linking. The
409432 generator will request additional pages as needed until all items have
410433 been returned.
@@ -420,7 +443,7 @@ def get_items(self, url, params=None, **kwargs):
420443 ApiError: If anything other than the expected response code is
421444 returned by the Webex Teams API endpoint.
422445 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.
424447
425448 """
426449 # Get generator for pages of JSON data
@@ -429,7 +452,7 @@ def get_items(self, url, params=None, **kwargs):
429452 for json_page in pages :
430453 assert isinstance (json_page , dict )
431454
432- items = json_page .get (' items' )
455+ items = json_page .get (" items" )
433456
434457 if items is None :
435458 error_message = "'items' key not found in JSON data: " \
@@ -459,9 +482,9 @@ def post(self, url, json=None, data=None, **kwargs):
459482 check_type (url , basestring )
460483
461484 # Expected response code
462- erc = kwargs .pop (' erc' , EXPECTED_RESPONSE_CODE [' POST' ])
485+ erc = kwargs .pop (" erc" , EXPECTED_RESPONSE_CODE [" POST" ])
463486
464- response = self .request (' POST' , url , erc , json = json , data = data ,
487+ response = self .request (" POST" , url , erc , json = json , data = data ,
465488 ** kwargs )
466489 return extract_and_parse_json (response )
467490
@@ -484,9 +507,9 @@ def put(self, url, json=None, data=None, **kwargs):
484507 check_type (url , basestring )
485508
486509 # Expected response code
487- erc = kwargs .pop (' erc' , EXPECTED_RESPONSE_CODE [' PUT' ])
510+ erc = kwargs .pop (" erc" , EXPECTED_RESPONSE_CODE [" PUT" ])
488511
489- response = self .request (' PUT' , url , erc , json = json , data = data ,
512+ response = self .request (" PUT" , url , erc , json = json , data = data ,
490513 ** kwargs )
491514 return extract_and_parse_json (response )
492515
@@ -507,6 +530,6 @@ def delete(self, url, **kwargs):
507530 check_type (url , basestring )
508531
509532 # Expected response code
510- erc = kwargs .pop (' erc' , EXPECTED_RESPONSE_CODE [' DELETE' ])
533+ erc = kwargs .pop (" erc" , EXPECTED_RESPONSE_CODE [" DELETE" ])
511534
512- self .request (' DELETE' , url , erc , ** kwargs )
535+ self .request (" DELETE" , url , erc , ** kwargs )
0 commit comments