Skip to content

Commit

Permalink
Initial checkin of Stats, LLC wrapper
Browse files Browse the repository at this point in the history
  • Loading branch information
pirateandy committed Apr 8, 2015
1 parent e3c6d63 commit dd82b30
Show file tree
Hide file tree
Showing 14 changed files with 3,532 additions and 1 deletion.
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Byte-compiled / optimized / DLL files
led / optimized / DLL files
__pycache__/
*.py[cod]

Expand Down Expand Up @@ -55,3 +55,9 @@ docs/_build/

# PyBuilder
target/

#PyCharm
.idea/

#Testing
test_config.py
72 changes: 72 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# sports-stats

sports-stats is written to make restful API calls to [Stats, LLC](http://www.stats.com/) easier and faster.

sports-stats must be used with Python 2.7 or above.

## Install

To install sports-stats using `pip`:

pip install sports-stats -U

To manually install sports-stats:

python setup.py install

To install sports-stats using `pip` from GitHub:

pip install -e git+git://github.com/sportsy/sports-stats.git#egg=sports-stats


## Example
You can make a simple call with this API wrapper, below is an example to get a MLB team's information. Please refer to the IO docs on Stats for endpoints and data returned.

```python
from stats import StatsAPI

#Get a team's schedule
api = StatsAPI('API_KEY', 'API_SECRET')
resp = api.request('/stats/baseball/mlb/events/teams/:teamId', {'teamId': 251})
print resp.json()
```
The wrapper itself is pretty flexible allowing both rest url parameters and get parameters when passing flags in with the request.

To make requests, you can keep the same structure as the endpoints on Stats documentation and the second argument in the request method is the rest URL params you want to replace with their values. The third argument is the URL get parameters. Both of these method arguments take a *dictionary*.
```python
from stats import StatsAPI

#Get an event's score
api = StatsAPI('API_KEY', 'API_SECRET')
resp = api.request('/stats/baseball/mlb/scores/:eventId', {'eventId': 1234}, {'linescore': 'true'})
print resp.json()
```

```python
from stats import StatsAPI

#Get information about a specific MLB team
api = StatsAPI('API_KEY', 'API_SECRET')
resp = api.request('/stats/baseball/mlb/teams/:teamId', {'teamId': 251})
print resp.json()
```

## Response methods / values
- `resp.headers` returns the headers of the request
- `resp.status_code` returns the status code of the request
- `resp.text()` returns the raw object
- `resp.json()` returns the object in json format

## Testing
We've integrated pytest into our library so you can run your tests. To run your test, all you need to do is run

python setup.py test

The test will then run and make API calls to the Stats API, we've put 1 second delay into each call so there won't be any false positives with API rate limiting.

## Need help?
Submit a ticket, we'll be more than happy to help out!

## Useful Links
[Stats IO Docs](http://developer.stats.com/io-docs)
[Stats Documentation](http://developer.stats.com/docs)
5 changes: 5 additions & 0 deletions __init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
__title__ = 'Stats LLC API Wrapper'
__version__ = '0.0.1'
__author__ = 'Sportsy, Inc.'
__license__ = 'MIT'
__copyright__ = 'Copyright 2015 Sportsy, Inc.'
2 changes: 2 additions & 0 deletions makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
test:
cd sportsstats && nosetests -v
3 changes: 3 additions & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mox==0.5.3
pytest==2.7.0
requests==2.6.0
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
requests==2.6.0
3,114 changes: 3,114 additions & 0 deletions runtests.py

Large diffs are not rendered by default.

75 changes: 75 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from __future__ import absolute_import
from setuptools import setup, find_packages, Command
import re
import os
import codecs

__title__ = 'Stats, LLC API Wrapper'
__version__ = '0.0.1'
__author__ = 'Sportsy, Inc.'
__license__ = 'MIT'
__copyright__ = 'Copyright 2015 Sportsy, Inc.'



class PyTest(Command):
user_options = []
def initialize_options(self):
pass

def finalize_options(self):
pass

def run(self):
import subprocess
import sys
errno = subprocess.call([sys.executable, 'runtests.py'])
raise SystemExit(errno)

def read(*parts):
path = os.path.join(os.path.dirname(__file__), *parts)
with codecs.open(path, encoding='utf-8') as fobj:
return fobj.read()


def find_version(*file_paths):
version_file = read(*file_paths)
version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]",
version_file, re.M)
if version_match:
return version_match.group(1)
raise RuntimeError("Unable to find version string.")


with open('requirements.txt') as f:
install_requires = f.read().splitlines()

with open('requirements-dev.txt') as f:
tests_require = f.read().splitlines()

print install_requires
print tests_require

setup(
name='sports-stats',
version=__version__,
description='',
url='https://github.com/sportsy/sports-stats',
author=__author__,
license=__license__,
packages=find_packages(exclude=['tests.*', 'tests']),
include_package_data=True,
test_suite='nose.collector',
install_requires=install_requires,
dependency_links=['https://github.com/kennethreitz/requests/tarball/master#egg=requests==2.6.0'],
tests_require=tests_require,
cmdclass = {'test': PyTest},
#entry_points={
# 'console_scripts': [
# 'sportsstats = sportsstats.api:main',
# ]
# },
)
26 changes: 26 additions & 0 deletions sportsstats/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
__title__ = 'Stats LLC API Wrapper'
__version__ = '0.0.1'
__author__ = 'Sportsy, Inc.'
__license__ = 'MIT'
__copyright__ = 'Copyright 2015 Sportsy, Inc.'

import logging


# No logging unless the client provides a handler,
logging.getLogger(__name__).addHandler(logging.NullHandler())

try:
from .api import StatsAPI, StatsResponse
from .exceptions import StatsConnectionError, StatsError, StatsRequestError
except:
pass


__all__ = [
'StatsAPI',
'StatsResponse',
'StatsConnectionError',
'StatsError',
'StatsRequestError'
]
161 changes: 161 additions & 0 deletions sportsstats/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
from datetime import datetime
from requests.exceptions import ConnectionError, ReadTimeout, SSLError
from requests.packages.urllib3.exceptions import ReadTimeoutError, ProtocolError
from .exceptions import *

import requests
import hashlib
import socket
import ssl
import time

BASE_URL = 'http://api.stats.com/v1'
REST_TIMEOUT = None
API_SECRET = None


class StatsAPI(object):

"""Access REST API or Streaming API resources.
:param key: Stats application key
:param secret: Stats application secret
:param accept: Stats accept type (json, xml)
"""

api_key = None
api_secret = None
api_accept = None

def __init__(
self,
key=None,
secret=None,
accept='json'):
"""Initialize with your application credentials"""

if not all([key, secret]):
raise Exception('Missing authentication parameter')

self.api_key = key
self.api_secret = secret
self.api_accept = accept

def _prepare_url(self, domain, path, params=None):
auth = self.generate_auth()
url = '{}{}?api_key={}&sig={}'.format(domain, path, auth['api_key'], auth['sig'])
if params is not None:
for k in params:
url += '&{}={}'.format(str(k), str(params[k]))
return url

def _get_endpoint(self, resource, params=None):
"""Substitute any parameters in the resource path with :PARAM."""
if ':' in resource:
if params is not None:
for k in params:
try:
url_param = ':%s' % k
resource = resource.replace(url_param, str(params[k]))
except:
pass
return resource, resource

else:
return resource, resource

else:
return resource, resource

def request(self, resource, rest_params=None, params=None, files=None):
"""Request a Stats REST API .
:param resource: A valid Stats endpoint
:param params: Dictionary with endpoint parameters or None (default)
:returns: StatsResponse
:raises: StatsConnectionError
"""
resource, endpoint = self._get_endpoint(resource, rest_params)
# if endpoint not in ENDPOINTS:
# raise Exception('Endpoint "%s" unsupported' % endpoint)

# payload = {'key1': 'value1', 'key2': 'value2'}
# r = requests.get("http://httpbin.org/get", params=payload)
session = requests.Session()
# session.headers = {'User-Agent': USER_AGENT}
method = 'GET'
subdomain = BASE_URL
url = self._prepare_url(subdomain, resource, params)
session.stream = False
timeout = REST_TIMEOUT
if method == 'POST':
data = params
params = None
else:
data = None
try:
r = session.request(
method,
url,
data=data,
params=params,
timeout=timeout,
files=files,
# proxies=self.proxies
)
except (ConnectionError, ProtocolError, ReadTimeout, ReadTimeoutError,
SSLError, ssl.SSLError, socket.error) as e:
raise StatsConnectionError(e)
return StatsResponse(r)

def generate_auth(self):
"""Returns the dictionary for the authentication get params
"""
return {'accept': self.api_accept, 'api_key': self.api_key, 'sig': self.generate_sig()}

def generate_sig(self):
"""Returns the SHA256 signature for making requests
"""
timestamp = int(time.time())
hash_it = '{}{}{}'.format(self.api_key, self.api_secret, timestamp)
return hashlib.sha256(hash_it).hexdigest()


class StatsResponse(object):

"""Response from either a REST API call.
:param response: The requests.Response object returned by the API call
"""

def __init__(self, response):
self.response = response

@property
def headers(self):
""":returns: Dictionary of API response header contents."""
return self.response.headers

@property
def status_code(self):
""":returns: HTTP response status code."""
return self.response.status_code

@property
def text(self):
""":returns: Raw API response text."""
return self.response.text

def json(self):
""":returns: response as JSON object."""
return self.response.json()

def get_rest_quota(self):
""":returns: Quota information in the REST-only response header."""
remaining, limit, reset = None, None, None
if self.response:
if 'x-rate-limit-remaining' in self.response.headers:
remaining = int(
self.response.headers['x-rate-limit-remaining'])
if remaining == 0:
limit = int(self.response.headers['x-rate-limit-limit'])
reset = int(self.response.headers['x-rate-limit-reset'])
reset = datetime.fromtimestamp(reset)
return {'remaining': remaining, 'limit': limit, 'reset': reset}
Loading

0 comments on commit dd82b30

Please sign in to comment.