Skip to content

Commit 0091d01

Browse files
committed
Move creation of the User-Agent header string to a dedicated function
- Add a user_agent() function to create the User-Agent header - Refactor the user agent comment data structure - Replace string-defining single quotes with double quotes
1 parent 34a0c6f commit 0091d01

File tree

1 file changed

+113
-90
lines changed

1 file changed

+113
-90
lines changed

webexteamssdk/restsession.py

Lines changed: 113 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -30,49 +30,54 @@
3030
unicode_literals,
3131
)
3232

33+
from builtins import *
34+
3335
from future import standard_library
3436
standard_library.install_aliases()
3537

38+
import json
39+
import logging
40+
import platform
41+
import sys
3642
import time
43+
import urllib
3744
import urllib.parse
3845
import warnings
39-
from builtins import *
4046

4147
import requests
42-
import urllib
43-
import platform
44-
import sys
45-
import json
4648
from past.builtins import basestring
4749

50+
from ._metadata import __title__, __version__
4851
from .config import DEFAULT_SINGLE_REQUEST_TIMEOUT, DEFAULT_WAIT_ON_RATE_LIMIT
4952
from .exceptions import MalformedResponse, RateLimitError, RateLimitWarning
5053
from .response_codes import EXPECTED_RESPONSE_CODE
5154
from .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
5863
def _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
102173
class 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

Comments
 (0)