"""Module containing funcitonality for interfacing with the
stats.nba.com HTTP interface."""
import requests
import json
from os import path
from abc import ABCMeta, abstractmethod
from collections import OrderedDict
__version__ = '0.1.2'
_HTTP_HEADERS = {
'Accept-Encoding': 'gzip, deflate, sdch',
'Accept-Language': 'en-US,en;q=0.8',
'Upgrade-Insecure-Requests': '1',
'User-Agent': ('Mozilla/5.0 (Windows NT 10.0; WOW64) '
'AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/48.0.2564.82 '
'Safari/537.36'),
'Accept': ('text/html,application/xhtml+xml,application/xml;q=0.9,'
'image/webp,*/*;q=0.8'),
'Cache-Control': 'max-age=0',
'Connection': 'keep-alive',
}
[docs]class HTTPResponseError(Exception):
"""Error indicating that the stats.nba.com server returned an
unexpected status code.
Attributes:
server_response (requests.models.Response): Response given by
the server. May contain useful information about the reason
for the failure.
"""
def __init__(self, server_response):
self.server_response = server_response
def __repr__(self):
return ("Server returned unexpected HTTP status code {}"
.format(self.server_response.status_code))
[docs]def request_stats(request_name, params={}, **kwargs):
"""Send an HTTP request to stats.nba.com.
Args:
request_name (str): Identifier to the request type.
params (dict): Dictionary of paramters to the request.
Any parameters not provided in this argument will be set
to default vaues. Some paramters do not have default
values; all of these must be provided. See the
documantation for individual request types for which
parameters they accept. A parameter may be left unspecified
by providing an empty string.
kwargs: Any additional keyword arguments that would be accepted
by requests.get().
Returns:
dict: Dictionary containing fields specific to the
request type.
"""
return _REQUEST_TYPES[request_name].send(params, **kwargs)
class _RequestType:
def __init__(self, name, data, param_types):
self.name = name
self.endpoint = data['endpoint']
self.params = [param_types[x] for x in data['params']]
self.response_format = data['response-format']
if self.response_format == 'result-set':
self.outputs = [x[0] for x in data['returns']]
self.url_param = data.get('url-param')
self.status = data.get('status', 'supported')
def send(self, params, **kwargs):
params_composed = self._compose_params(params)
url = 'http://stats.nba.com/{0}?'.format(self.endpoint)
if self.url_param is not None:
url = url.format(params_composed.pop(self.url_param))
response = requests.get(url,
params=params_composed,
headers=_HTTP_HEADERS,
**kwargs)
if not (response.status_code >= 200 and response.status_code <= 399):
raise HTTPResponseError(response)
if self.response_format == 'result-set':
if 'resultSets' in response.json().keys():
return self._label_result_sets(response.json()['resultSets'])
else:
# The response from the league-leaders request has a typo. This
# accounts for that.
return self._label_result_sets(response.json()['resultSet'])
else:
return response.json()
def _compose_params(self, param_values_provided):
params_composed = {}
for param in self.params:
if param.name in param_values_provided:
value_provided = param_values_provided[param.name]
params_composed[param.name] = \
param.format_value(value_provided)
else:
if param.has_default:
params_composed[param.name] = param.default_formatted
else:
raise ValueError("Request {0} is missing parameter {1}."
.format(self.name, param.name))
return params_composed
def _label_result_sets(self, result_set_list):
results = OrderedDict()
if isinstance(result_set_list, list):
for output, index in zip(self.outputs, range(len(self.outputs))):
results[output] = \
self._extract_result_set(result_set_list[index])
else:
results[self.outputs[0]] = \
self._extract_result_set(result_set_list)
return results
def _extract_result_set(self, result_set):
headers = result_set['headers']
values = result_set['rowSet']
return [dict(zip(headers, x)) for x in values]
class _ParamType(metaclass=ABCMeta):
def __init__(self, name, data):
self.name = name
self.default_string = data.get('default')
self.has_default = (self.default_string is not None)
if self.has_default:
if self.default_string != '':
self.default_value = self._parse(self.default_string)
self.default_formatted = self.format_value(self.default_value)
else:
self.default_value = None
self.default_formatted = ""
@abstractmethod
def _parse(self, text):
"""Parse argument value from string"""
@abstractmethod
def format_value(self, value):
"""Format argument value into string to be used in HTTP request"""
class _IntParamType(_ParamType):
def _parse(self, text):
return int(text)
def format_value(self, value):
return value
class _SeasonParamType(_IntParamType):
def format_value(self, value):
if value < 1000 or value > 9999:
raise ValueError("Seasons should be four digit integers")
next_year_two_digits = str(int(value) % 100 + 1)[-2:].zfill(2)
return '{0}-{1}'.format(value, next_year_two_digits)
class _SeasonIDParamType(_IntParamType):
def format_value(self, value):
if value < 1000 or value > 9999:
raise ValueError("Seasons should be four digit integers")
return '2{0}'.format(value)
class _BooleanParamType(_ParamType):
def _parse(self, text):
return {'True': True, 'False': False}[text]
class _BooleanYNParamType(_BooleanParamType):
def format_value(self, value):
return 'Y' if value else 'N'
class _Boolean01ParamType(_BooleanParamType):
def format_value(self, value):
return '1' if value else '0'
class _EnumParamType(_ParamType):
def __init__(self, name, data):
self.options = data['options']
super().__init__(name, data)
def _parse(self, text):
return text
def format_value(self, value):
if value not in self.options:
raise ValueError(("Unrecognized value '{0}' for option "
"'{1}'. Options are [{2}]")
.format(value, self.name, self.options))
return value
class _MappedEnumParamType(_EnumParamType):
def format_value(self, value):
if value not in self.options:
raise ValueError(("Unrecognized value '{0}' for option "
"'{1}'. Options are [{2}]")
.format(value, self.name,
list(self.options.keys())))
return self.options[value]
class _DateParamType(_ParamType):
def _parse(self, text):
if not text:
return None
else:
raise NotImplementedError
def format_value(self, value):
return str(value)
def _construct_param_type_from_json(name, data):
param_type_map = {
'int': _IntParamType,
'season': _SeasonParamType,
'season-id': _SeasonIDParamType,
'boolean-yn': _BooleanYNParamType,
'boolean-01': _Boolean01ParamType,
'enum': _EnumParamType,
'enum-mapped': _MappedEnumParamType,
'date': _DateParamType,
}
return param_type_map[data['type']](name, data)
def _load_request_metadata():
request_data_path = path.join(path.dirname(path.abspath(__file__)),
"nbawebstats/requests.json")
with open(request_data_path, 'r') as f:
request_data = json.load(f)
param_types = {x: _construct_param_type_from_json(x, y)
for x, y in request_data['params'].items()}
request_types = {x: _RequestType(x, y, param_types)
for x, y in request_data['requests'].items()}
return request_types
_REQUEST_TYPES = _load_request_metadata()