Skip to content

Commit afb9303

Browse files
committed
add api.helix
- initial staging
1 parent 50250c8 commit afb9303

9 files changed

Lines changed: 184 additions & 9 deletions

File tree

resources/lib/twitch/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@
66
CLIENT_ID = ''
77
CLIENT_SECRET = ''
88
OAUTH_TOKEN = ''
9+
APP_TOKEN = ''

resources/lib/twitch/api/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22

33
from twitch.api import v5 # V5 is deprecated and will be removed entirely on 2/14/18
44
from twitch.api import v5 as default
5+
from twitch.api import helix
56

6-
__all__ = ['v5', 'default']
7+
__all__ = ['v5', 'default', 'helix']
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# -*- encoding: utf-8 -*-
2+
# https://dev.twitch.tv/docs/
3+
4+
from twitch.api.helix import users # NOQA
5+
from twitch.api.helix import streams # NOQA
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# -*- encoding: utf-8 -*-
2+
# https://dev.twitch.tv/docs/api/reference
3+
4+
from twitch import keys
5+
from twitch.api.parameters import Cursor, Language, StreamTypes
6+
from twitch.queries import HelixQuery as Qry
7+
from twitch.queries import query
8+
9+
10+
# required scope: none
11+
@query
12+
def get_streams(community_id=list(), game_id=list(), user_id=list(),
13+
user_login=list(), stream_type=StreamTypes.ALL, language=list(),
14+
after='MA==', first=20, use_app_token=False):
15+
q = Qry('streams', use_app_token=use_app_token)
16+
q.add_param(keys.AFTER, Cursor.validate(after), 'MA==')
17+
q.add_param(keys.FIRST, first, 20)
18+
q.add_param(keys.COMMUNITY_ID, community_id, list())
19+
q.add_param(keys.GAME_ID, game_id, list())
20+
q.add_param(keys.USER_ID, user_id, list())
21+
q.add_param(keys.USER_LOGIN, user_login, list())
22+
q.add_param(keys.TYPE, StreamTypes.validate(stream_type), StreamTypes.ALL)
23+
if isinstance(language, list):
24+
_language = [lang for lang in language if lang in Language.valid()]
25+
q.add_param(keys.LANGUAGE, _language, list())
26+
else:
27+
q.add_param(keys.LANGUAGE, Language.validate(language), '')
28+
29+
return q
30+
31+
32+
# required scope: none
33+
@query
34+
def get_metadata(community_id=list(), game_id=list(), user_id=list(),
35+
user_login=list(), stream_type=StreamTypes.ALL, language=list(),
36+
after='MA==', first=20, use_app_token=False):
37+
q = Qry('streams/metadata', use_app_token=use_app_token)
38+
q.add_param(keys.AFTER, Cursor.validate(after), 'MA==')
39+
q.add_param(keys.FIRST, first, 20)
40+
q.add_param(keys.COMMUNITY_ID, community_id, list())
41+
q.add_param(keys.GAME_ID, game_id, list())
42+
q.add_param(keys.USER_ID, user_id, list())
43+
q.add_param(keys.USER_LOGIN, user_login, list())
44+
q.add_param(keys.TYPE, StreamTypes.validate(stream_type), StreamTypes.ALL)
45+
if isinstance(language, list):
46+
_language = [lang for lang in language if lang in Language.valid()]
47+
q.add_param(keys.LANGUAGE, _language, list())
48+
else:
49+
q.add_param(keys.LANGUAGE, Language.validate(language), '')
50+
51+
return q
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# -*- encoding: utf-8 -*-
2+
# https://dev.twitch.tv/docs/api/reference
3+
4+
from twitch import keys, methods
5+
from twitch.api.parameters import Cursor
6+
from twitch.queries import HelixQuery as Qry
7+
from twitch.queries import query
8+
9+
10+
# optional scope: user:read:email
11+
@query
12+
def get_users(user_id=list(), user_login=list(), use_app_token=False):
13+
use_token = (not user_id and not user_login)
14+
use_app_token = False if use_token else use_app_token
15+
q = Qry('users', use_app_token=use_app_token)
16+
q.add_param(keys.ID, user_id, list())
17+
q.add_param(keys.LOGIN, user_login, list())
18+
return q
19+
20+
21+
# required scope: none
22+
@query
23+
def get_follows(from_id='', to_id='', after='MA==', before='MA==', first=20, use_app_token=False):
24+
q = Qry('users/follows', use_app_token=use_app_token)
25+
q.add_param(keys.FROM_ID, from_id, '')
26+
q.add_param(keys.TO_ID, to_id, '')
27+
q.add_param(keys.AFTER, Cursor.validate(after), 'MA==')
28+
q.add_param(keys.BEFORE, Cursor.validate(before), 'MA==')
29+
q.add_param(keys.FIRST, first, 20)
30+
return q
31+
32+
33+
# required scope: user:edit
34+
@query
35+
def put_users(description):
36+
q = Qry('users', method=methods.PUT)
37+
q.add_param(keys.DESCRIPTION, description, '')
38+
return q

resources/lib/twitch/api/parameters.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,14 @@ class StreamType(_Parameter):
8484
_valid = [LIVE, PLAYLIST, ALL]
8585

8686

87+
class StreamTypes(_Parameter):
88+
LIVE = 'live'
89+
VODCAST = 'vodcast'
90+
ALL = 'all'
91+
92+
_valid = [LIVE, VODCAST, ALL]
93+
94+
8795
class Platform(_Parameter):
8896
XBOX_ONE = 'xbox_one'
8997
PS4 = 'ps4'

resources/lib/twitch/keys.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
string constants
66
"""
77

8+
AFTER = 'after'
89
ALLOW_AUDIO_ONLY = 'allow_audio_only'
910
ALLOW_SOURCE = 'allow_source'
1011
ALLOW_SPECTRE = 'allow_spectre'
1112
AVATAR_IMAGE = 'avatar_image'
13+
BEFORE = 'before'
1214
BROADCAST_TYPE = 'broadcast_type'
1315
BROADCASTER_LANGUAGE = 'broadcaster_language'
1416
BROADCASTS = 'broadcasts'
@@ -36,8 +38,11 @@
3638
EMOTESETS = 'emotesets'
3739
ERROR = 'error'
3840
FEATURED = 'featured'
41+
FIRST = 'first'
3942
FOLLOWS = 'follows'
43+
FROM_ID = 'from_id'
4044
GAME = 'game'
45+
GAME_ID = 'game_id'
4146
HLS = 'hls'
4247
ID = 'id'
4348
IDENTIFIER = 'identifier'
@@ -46,6 +51,7 @@
4651
LANGUAGE = 'language'
4752
LIMIT = 'limit'
4853
LIVE = 'live'
54+
LOGIN = 'login'
4955
MESSAGE = 'message'
5056
NAME = 'name'
5157
NAUTH = 'nauth'
@@ -73,6 +79,7 @@
7379
TARGET_ID = 'target_id'
7480
TEAM = 'team'
7581
TITLE = 'title'
82+
TO_ID = 'to_id'
7683
TOKEN = 'token'
7784
TRENDING = 'trending'
7885
TYPE = 'type'
@@ -82,6 +89,7 @@
8289
USER_AGENT_STRING = ('Mozilla/5.0 (Windows NT 6.1; WOW64; rv:6.0) '
8390
'Gecko/20100101 Firefox/6.0')
8491
USER_ID = 'user_id'
92+
USER_LOGIN = 'user_login'
8593
UPLOAD_TOKEN = 'upload_token'
8694
VIDEO_ID = 'video_id'
8795
VOD = 'vod'

resources/lib/twitch/queries.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22

33
from six.moves.urllib.parse import urljoin
44

5-
from twitch import CLIENT_ID, OAUTH_TOKEN
5+
from twitch import CLIENT_ID, OAUTH_TOKEN, APP_TOKEN
66
from twitch.exceptions import ResourceUnavailableException
77
from twitch.logging import log
8-
from twitch.scraper import download, get_json
8+
from twitch.scraper import download, get_json, get_json_and_headers
99
from twitch import methods
1010

1111
_kraken_baseurl = 'https://api.twitch.tv/kraken/'
12+
_helix_baseurl = 'https://api.twitch.tv/helix/'
1213
_hidden_baseurl = 'https://api.twitch.tv/api/'
1314
_usher_baseurl = 'https://usher.ttvnw.net/'
1415
_clips_baseurl = 'https://clips.twitch.tv/'
@@ -104,6 +105,12 @@ def execute(self):
104105
return super(JsonQuery, self).execute(get_json)
105106

106107

108+
class HelixJsonQuery(_Query):
109+
def execute(self):
110+
# TODO implement get_json completely here
111+
return super(HelixJsonQuery, self).execute(get_json_and_headers)
112+
113+
107114
class ApiQuery(JsonQuery):
108115
def __init__(self, path, headers={}, data={}, use_token=True, method=methods.GET):
109116
headers.setdefault('Client-ID', CLIENT_ID)
@@ -113,6 +120,29 @@ def __init__(self, path, headers={}, data={}, use_token=True, method=methods.GET
113120
self.add_path(path)
114121

115122

123+
class HelixApiQuery(HelixJsonQuery):
124+
def __init__(self, path, headers={}, data={}, use_app_token=False, method=methods.GET):
125+
headers.setdefault('Client-ID', CLIENT_ID)
126+
if use_app_token and APP_TOKEN:
127+
headers.setdefault('Authorization', 'Bearer {access_token}'.format(access_token=APP_TOKEN))
128+
elif OAUTH_TOKEN:
129+
headers.setdefault('Authorization', 'Bearer {access_token}'.format(access_token=OAUTH_TOKEN))
130+
super(HelixApiQuery, self).__init__(_helix_baseurl, headers, data, method)
131+
self._params = list()
132+
self.add_path(path)
133+
134+
def add_param(self, key, value, default=None):
135+
if value != default:
136+
if isinstance(value, list):
137+
_params = []
138+
for val in value:
139+
_params += [(key, val)]
140+
self._params += _params
141+
elif (key, value) not in self._params:
142+
self._params += [(key, value)]
143+
return self
144+
145+
116146
class HiddenApiQuery(JsonQuery):
117147
def __init__(self, path, headers={}, data={}, use_token=True, method=methods.GET):
118148
headers.setdefault('Client-ID', CLIENT_ID)
@@ -148,6 +178,11 @@ def __init__(self, path, use_token=True, method=methods.GET):
148178
super(V5Query, self).__init__(path, _v5_headers, use_token=use_token, method=method)
149179

150180

181+
class HelixQuery(HelixApiQuery):
182+
def __init__(self, path, use_app_token=False, method=methods.GET):
183+
super(HelixQuery, self).__init__(path, use_app_token=use_app_token, method=method)
184+
185+
151186
def assert_new(d, k):
152187
if k in d:
153188
v = d.get(k)

resources/lib/twitch/scraper.py

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
def get_json(baseurl, parameters={}, headers={}, data={}, method=methods.GET):
2727
'''Download Data from an URL and returns it as JSON
2828
@param url Url to download from
29-
@param parameters Parameter dict to be encoded with url
29+
@param parameters Parameter dict to be encoded with url or list of tuple pairs
3030
@param headers Headers dict to pass with Request
3131
@param data Request body
3232
@param method Request method
@@ -39,18 +39,42 @@ def get_json(baseurl, parameters={}, headers={}, data={}, method=methods.GET):
3939
return jsonDict
4040

4141

42-
def download(baseurl, parameters={}, headers={}, data={}, method=methods.GET):
42+
def get_json_and_headers(baseurl, parameters={}, headers={}, data={}, method=methods.GET):
43+
'''Download Data from an URL and returns it as JSON
44+
@param url Url to download from
45+
@param parameters Parameter dict to be encoded with url or list of tuple pairs
46+
@param headers Headers dict to pass with Request
47+
@param data Request body
48+
@param method Request method
49+
@returns JSON Object with data and headers from URL {'response': {}, 'headers': {}}
50+
'''
51+
method = methods.validate(method)
52+
content = download(baseurl, parameters, headers, data, method, response_headers=True)
53+
content['response'] = json.loads(content['response'])
54+
log.debug(json.dumps(content['response'], indent=4, sort_keys=True))
55+
return content
56+
57+
58+
def download(baseurl, parameters={}, headers={}, data={}, method=methods.GET, response_headers=False):
4359
'''Download Data from an url and returns it as a String
4460
@param method Request method
4561
@param baseurl Url to download from (e.g. http://www.google.com)
46-
@param parameters Parameter dict to be encoded with url
62+
@param parameters Parameter dict to be encoded with url or list of tuple pairs
4763
@param headers Headers dict to pass with Request
4864
@param data Request body
4965
@param method Request method
50-
@returns String of data from URL
66+
@param response_headers Include response headers in response {'response': {}, 'headers': {}}
67+
@returns String of data from URL or {'response': {}, 'headers': {}} if response_headers is True
5168
'''
5269
method = methods.validate(method)
53-
url = '?'.join([baseurl, urlencode(parameters)])
70+
if isinstance(parameters, dict):
71+
url = '?'.join([baseurl, urlencode(parameters)])
72+
else:
73+
_parameters = ''
74+
for param in parameters:
75+
_parameters += '{0}={1}&'.format(param[0], quote_plus(str(param[1])))
76+
_parameters = _parameters.rstrip('&')
77+
url = '?'.join([baseurl, _parameters])
5478
log.debug('Downloading: ' + url)
5579
content = ""
5680
for _ in range(MAX_RETRIES):
@@ -68,4 +92,8 @@ def download(baseurl, parameters={}, headers={}, data={}, method=methods.GET):
6892
log.debug("Error %s during HTTP Request, retrying", repr(err))
6993
else:
7094
raise
71-
return content
95+
96+
if not response_headers:
97+
return content
98+
else:
99+
return {'response': content, 'headers': response.headers}

0 commit comments

Comments
 (0)