├── tests ├── __init__.py ├── api │ ├── __init__.py │ ├── dummy_credentials.cfg │ ├── test_ingests.py │ ├── test_client.py │ ├── test_clips.py │ ├── test_teams.py │ ├── test_chat.py │ ├── test_games.py │ ├── test_videos.py │ ├── test_search.py │ ├── test_collections.py │ ├── test_streams.py │ ├── test_base.py │ ├── test_channel_feed.py │ ├── test_users.py │ └── test_communities.py ├── helix │ └── __init__.py └── test_resources.py ├── samples ├── __init__.py ├── users.py └── helix.py ├── twitch ├── helix │ ├── __init__.py │ └── base.py ├── __init__.py ├── api │ ├── ingests.py │ ├── __init__.py │ ├── games.py │ ├── chat.py │ ├── teams.py │ ├── search.py │ ├── clips.py │ ├── collections.py │ ├── videos.py │ ├── base.py │ ├── streams.py │ ├── channel_feed.py │ ├── users.py │ ├── communities.py │ └── channels.py ├── decorators.py ├── exceptions.py ├── conf.py ├── constants.py ├── resources.py └── client.py ├── .codecov.yml ├── .coveragerc ├── .gitignore ├── Makefile ├── setup.cfg ├── docs ├── v5 │ ├── index.rst │ ├── ingests.rst │ ├── games.rst │ ├── teams.rst │ ├── chat.rst │ ├── videos.rst │ ├── search.rst │ ├── communities.rst │ ├── clips.rst │ ├── streams.rst │ ├── collections.rst │ ├── users.rst │ ├── channel_feed.rst │ └── channels.rst ├── basic_usage.rst ├── Makefile ├── index.rst ├── conf.py └── helix.rst ├── .github └── workflows │ └── run-tests.yml ├── LICENSE ├── README.md ├── setup.py └── CHANGELOG.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /samples/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/helix/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /twitch/helix/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | comment: off 2 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | tests/* 4 | setup.py 5 | samples/* 6 | -------------------------------------------------------------------------------- /twitch/__init__.py: -------------------------------------------------------------------------------- 1 | from .client import TwitchClient # noqa 2 | from .helix.api import TwitchHelix # noqa 3 | 4 | __version__ = "0.7.1" 5 | -------------------------------------------------------------------------------- /tests/api/dummy_credentials.cfg: -------------------------------------------------------------------------------- 1 | [Credentials] 2 | client_id = spongebob 3 | oauth_token = squarepants 4 | 5 | [General] 6 | initial_backoff = 0.01 7 | max_retries = 1 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | _build/ 3 | .cache/ 4 | .coverage 5 | *.egg-info/ 6 | *.DS_Store 7 | *.sublime-* 8 | *.py[co] 9 | *.python-version 10 | *.pytest_cache 11 | build/ 12 | dist/ 13 | htmlcov/ 14 | spongebob.py -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | py.test -vsx $(ARGS) 3 | 4 | cov: 5 | py.test --cov=./ 6 | 7 | cov-html: 8 | py.test --cov=./ --cov-report html 9 | 10 | format: 11 | isort . 12 | black . 13 | 14 | lint: 15 | flake8 . 16 | black --check . 17 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | license_file = LICENSE 4 | 5 | [flake8] 6 | max-line-length = 100 7 | select = E,W,F,I,C 8 | ignore = W503 9 | exclude = docs/ 10 | application-import-names = twitch 11 | 12 | [isort] 13 | profile = black 14 | multi_line_output = 3 15 | -------------------------------------------------------------------------------- /twitch/api/ingests.py: -------------------------------------------------------------------------------- 1 | from twitch.api.base import TwitchAPI 2 | from twitch.resources import Ingest 3 | 4 | 5 | class Ingests(TwitchAPI): 6 | def get_server_list(self): 7 | response = self._request_get("ingests") 8 | return [Ingest.construct_from(x) for x in response["ingests"]] 9 | -------------------------------------------------------------------------------- /twitch/decorators.py: -------------------------------------------------------------------------------- 1 | from twitch.exceptions import TwitchAuthException 2 | 3 | 4 | def oauth_required(func): 5 | def wrapper(*args, **kwargs): 6 | if not args[0]._oauth_token: 7 | raise TwitchAuthException("OAuth token required") 8 | return func(*args, **kwargs) 9 | 10 | return wrapper 11 | -------------------------------------------------------------------------------- /docs/v5/index.rst: -------------------------------------------------------------------------------- 1 | .. twitch v5:: 2 | 3 | Twitch v5 4 | ========= 5 | 6 | .. toctree:: 7 | :maxdepth: 1 8 | 9 | clips 10 | chat 11 | channel_feed 12 | channels 13 | collections 14 | communities 15 | games 16 | ingests 17 | search 18 | streams 19 | teams 20 | users 21 | videos 22 | -------------------------------------------------------------------------------- /twitch/exceptions.py: -------------------------------------------------------------------------------- 1 | class TwitchException(Exception): 2 | pass 3 | 4 | 5 | class TwitchAuthException(TwitchException): 6 | pass 7 | 8 | 9 | class TwitchAttributeException(TwitchException): 10 | pass 11 | 12 | 13 | class TwitchNotProvidedException(TwitchException): 14 | pass 15 | 16 | 17 | class TwitchOAuthException(TwitchException): 18 | pass 19 | -------------------------------------------------------------------------------- /samples/users.py: -------------------------------------------------------------------------------- 1 | from twitch import TwitchClient 2 | 3 | CLIENT_ID = "" 4 | 5 | 6 | def translate_usernames_to_ids(): 7 | client = TwitchClient(CLIENT_ID) 8 | users = client.users.translate_usernames_to_ids(["lirik", "giantwaffle"]) 9 | 10 | for user in users: 11 | print("{}: {}".format(user.name, user.id)) 12 | 13 | 14 | if __name__ == "__main__": 15 | translate_usernames_to_ids() 16 | -------------------------------------------------------------------------------- /twitch/api/__init__.py: -------------------------------------------------------------------------------- 1 | from .channel_feed import ChannelFeed # noqa 2 | from .channels import Channels # noqa 3 | from .chat import Chat # noqa 4 | from .clips import Clips # noqa 5 | from .collections import Collections # noqa 6 | from .communities import Communities # noqa 7 | from .games import Games # noqa 8 | from .ingests import Ingests # noqa 9 | from .search import Search # noqa 10 | from .streams import Streams # noqa 11 | from .teams import Teams # noqa 12 | from .users import Users # noqa 13 | from .videos import Videos # noqa 14 | -------------------------------------------------------------------------------- /docs/v5/ingests.rst: -------------------------------------------------------------------------------- 1 | Ingests 2 | ======= 3 | 4 | .. currentmodule:: twitch.api.ingests 5 | 6 | .. class:: Ingests() 7 | 8 | This class provides methods for easy access to `Twitch Ingests API`_. 9 | 10 | .. classmethod:: get_server_list() 11 | 12 | Gets a list of ingest servers. 13 | 14 | .. code-block:: python 15 | 16 | >>> from twitch import TwitchClient 17 | >>> client = TwitchClient('') 18 | >>> ingests = client.ingests.get_server_list() 19 | 20 | 21 | .. _`Twitch Ingests API`: https://dev.twitch.tv/docs/v5/reference/ingests/ 22 | -------------------------------------------------------------------------------- /twitch/api/games.py: -------------------------------------------------------------------------------- 1 | from twitch.api.base import TwitchAPI 2 | from twitch.exceptions import TwitchAttributeException 3 | from twitch.resources import TopGame 4 | 5 | 6 | class Games(TwitchAPI): 7 | def get_top(self, limit=10, offset=0): 8 | if limit > 100: 9 | raise TwitchAttributeException( 10 | "Maximum number of objects returned in one request is 100" 11 | ) 12 | 13 | params = {"limit": limit, "offset": offset} 14 | response = self._request_get("games/top", params=params) 15 | return [TopGame.construct_from(x) for x in response["top"]] 16 | -------------------------------------------------------------------------------- /twitch/api/chat.py: -------------------------------------------------------------------------------- 1 | from twitch.api.base import TwitchAPI 2 | 3 | 4 | class Chat(TwitchAPI): 5 | def get_badges_by_channel(self, channel_id): 6 | response = self._request_get("chat/{}/badges".format(channel_id)) 7 | return response 8 | 9 | def get_emoticons_by_set(self, emotesets=None): 10 | params = { 11 | "emotesets": emotesets, 12 | } 13 | response = self._request_get("chat/emoticon_images", params=params) 14 | return response 15 | 16 | def get_all_emoticons(self): 17 | response = self._request_get("chat/emoticons") 18 | return response 19 | -------------------------------------------------------------------------------- /twitch/api/teams.py: -------------------------------------------------------------------------------- 1 | from twitch.api.base import TwitchAPI 2 | from twitch.exceptions import TwitchAttributeException 3 | from twitch.resources import Team 4 | 5 | 6 | class Teams(TwitchAPI): 7 | def get(self, team_name): 8 | response = self._request_get("teams/{}".format(team_name)) 9 | return Team.construct_from(response) 10 | 11 | def get_all(self, limit=10, offset=0): 12 | if limit > 100: 13 | raise TwitchAttributeException( 14 | "Maximum number of objects returned in one request is 100" 15 | ) 16 | 17 | params = {"limit": limit, "offset": offset} 18 | response = self._request_get("teams", params=params) 19 | return [Team.construct_from(x) for x in response["teams"]] 20 | -------------------------------------------------------------------------------- /docs/basic_usage.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | Basic Usage 3 | =========== 4 | 5 | The ``python-twitch-client`` allows you to easily access to Twitch API endpoints. 6 | 7 | This package is a modular wrapper designed to make Twitch API calls simpler and easier for you to 8 | use. Provided below are examples of how to interact with commonly used API endpoints, but this is by no means 9 | a complete list. 10 | 11 | -------- 12 | 13 | Getting a channel by ID 14 | ----------------------- 15 | 16 | .. code-block:: python 17 | 18 | from twitch import TwitchClient 19 | 20 | client = TwitchClient(client_id='') 21 | channel = client.channels.get_by_id(44322889) 22 | 23 | print(channel.id) 24 | print(channel.name) 25 | print(channel.display_name) 26 | -------------------------------------------------------------------------------- /docs/v5/games.rst: -------------------------------------------------------------------------------- 1 | Games 2 | ===== 3 | 4 | .. currentmodule:: twitch.api.games 5 | 6 | .. class:: Games() 7 | 8 | This class provides methods for easy access to `Twitch Games API`_. 9 | 10 | .. classmethod:: get_top(limit, offset) 11 | 12 | Gets a list of games sorted by number of current viewers. 13 | 14 | :param int limit: Maximum number of objects to return. Default 25. Maximum 100. 15 | :param int offset: Object offset for pagination of result. Default 0. 16 | 17 | .. code-block:: python 18 | 19 | >>> from twitch import TwitchClient 20 | >>> client = TwitchClient('') 21 | >>> games = client.games.get_top() 22 | 23 | 24 | .. _`Twitch Games API`: https://dev.twitch.tv/docs/v5/reference/games/ 25 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = python-twitch-client 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | 22 | remove-build: 23 | rm -rf ./_build 24 | 25 | rebuild-html: remove-build html 26 | -------------------------------------------------------------------------------- /tests/api/test_ingests.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import responses 4 | 5 | from twitch.client import TwitchClient 6 | from twitch.constants import BASE_URL 7 | from twitch.resources import Ingest 8 | 9 | example_response = {"ingests": [{"_id": 24, "name": "EU: Amsterdam, NL"}]} 10 | 11 | 12 | @responses.activate 13 | def test_get_top(): 14 | responses.add( 15 | responses.GET, 16 | "{}ingests".format(BASE_URL), 17 | body=json.dumps(example_response), 18 | status=200, 19 | content_type="application/json", 20 | ) 21 | 22 | client = TwitchClient("abcd") 23 | 24 | ingests = client.ingests.get_server_list() 25 | 26 | assert len(responses.calls) == 1 27 | assert len(ingests) == 1 28 | ingest = ingests[0] 29 | assert isinstance(ingest, Ingest) 30 | assert ingest.id == example_response["ingests"][0]["_id"] 31 | assert ingest.name == example_response["ingests"][0]["name"] 32 | -------------------------------------------------------------------------------- /docs/v5/teams.rst: -------------------------------------------------------------------------------- 1 | Teams 2 | ===== 3 | 4 | .. currentmodule:: twitch.api.teams 5 | 6 | .. class:: Teams() 7 | 8 | This class provides methods for easy access to `Twitch Teams API`_. 9 | 10 | .. classmethod:: get(team_name) 11 | 12 | Gets a specified team object. 13 | 14 | :param string team_name: Name of the team you want to get information of 15 | 16 | 17 | .. classmethod:: get_all(limit, offset) 18 | 19 | Gets all active teams. 20 | 21 | :param int limit: Maximum number of objects to return. Default 25. Maximum 100. 22 | :param int offset: Object offset for pagination of result. Default 0. 23 | 24 | 25 | .. code-block:: python 26 | 27 | >>> from twitch import TwitchClient 28 | >>> client = TwitchClient('') 29 | >>> teams = client.teams.get_all() 30 | 31 | 32 | .. _`Twitch Teams API`: https://dev.twitch.tv/docs/v5/reference/teams/ 33 | -------------------------------------------------------------------------------- /twitch/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | from configparser import ConfigParser 3 | 4 | from twitch.constants import CONFIG_FILE_PATH 5 | 6 | 7 | def _get_config(): 8 | config = ConfigParser() 9 | config.read(os.path.expanduser(CONFIG_FILE_PATH)) 10 | return config 11 | 12 | 13 | def credentials_from_config_file(): 14 | client_id = None 15 | oauth_token = None 16 | 17 | config = _get_config() 18 | if "Credentials" in config.sections(): 19 | client_id = config["Credentials"].get("client_id") 20 | oauth_token = config["Credentials"].get("oauth_token") 21 | 22 | return client_id, oauth_token 23 | 24 | 25 | def backoff_config(): 26 | initial_backoff = 0.5 27 | max_retries = 3 28 | 29 | config = _get_config() 30 | if "General" in config.sections(): 31 | initial_backoff = float(config["General"]["initial_backoff"]) 32 | max_retries = int(config["General"]["max_retries"]) 33 | 34 | return initial_backoff, max_retries 35 | -------------------------------------------------------------------------------- /docs/v5/chat.rst: -------------------------------------------------------------------------------- 1 | Chat 2 | ==== 3 | 4 | .. currentmodule:: twitch.api.chat 5 | 6 | .. class:: Chat() 7 | 8 | This class provides methods for easy access to `Twitch Chat API`_. 9 | 10 | .. classmethod:: get_badges_by_channel(channel_id) 11 | 12 | Gets a list of badges that can be used in chat for a specified channel. 13 | 14 | :param string channel_id: Channel ID. 15 | 16 | 17 | .. classmethod:: get_emoticons_by_set(emotesets) 18 | 19 | Gets all chat emoticons in one or more specified sets. 20 | 21 | :param list emotesets: List of emoticon sets to be returned. 22 | 23 | 24 | .. classmethod:: get_all_emoticons() 25 | 26 | Gets all chat emoticons. 27 | 28 | 29 | .. code-block:: python 30 | 31 | >>> from twitch import TwitchClient 32 | >>> client = TwitchClient('') 33 | >>> emotes = client.chat.get_all_emoticons() 34 | 35 | 36 | 37 | .. _`Twitch Chat API`: https://dev.twitch.tv/docs/v5/reference/chat/ 38 | -------------------------------------------------------------------------------- /tests/api/test_client.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from twitch import TwitchClient 6 | 7 | 8 | @pytest.mark.parametrize( 9 | "prop", 10 | [ 11 | ("channel_feed"), 12 | ("channels"), 13 | ("chat"), 14 | ("collections"), 15 | ("communities"), 16 | ("games"), 17 | ("ingests"), 18 | ("search"), 19 | ("streams"), 20 | ("teams"), 21 | ("users"), 22 | ("videos"), 23 | ], 24 | ) 25 | def test_client_properties(prop): 26 | c = TwitchClient() 27 | assert getattr(c, "_{}".format(prop)) is None 28 | assert getattr(c, "{}".format(prop)) is not None 29 | assert getattr(c, "_{}".format(prop)) is not None 30 | 31 | 32 | def test_client_reads_credentials_from_file(monkeypatch): 33 | def mockreturn(path): 34 | return "tests/api/dummy_credentials.cfg" 35 | 36 | monkeypatch.setattr(os.path, "expanduser", mockreturn) 37 | 38 | c = TwitchClient() 39 | assert c._client_id == "spongebob" 40 | assert c._oauth_token == "squarepants" 41 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | python-version: ['3.6', '3.7', '3.8'] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Install dependencies 25 | run: | 26 | pip install .[test] 27 | - name: Lint 28 | run: | 29 | # stop the build if there are Python syntax errors or undefined names 30 | flake8 . 31 | black --check . 32 | - name: Test with pytest 33 | run: | 34 | python setup.py test --pytest-args "tests/ --cov=./ --cov-report=xml" 35 | - name: Upload coverage to Codecov 36 | uses: codecov/codecov-action@v1 37 | with: 38 | file: ./coverage.xml 39 | fail_ci_if_error: true 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2018] [Tomaz Sifrer] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /twitch/api/search.py: -------------------------------------------------------------------------------- 1 | from twitch.api.base import TwitchAPI 2 | from twitch.exceptions import TwitchAttributeException 3 | from twitch.resources import Channel, Game, Stream 4 | 5 | 6 | class Search(TwitchAPI): 7 | def channels(self, query, limit=25, offset=0): 8 | if limit > 100: 9 | raise TwitchAttributeException( 10 | "Maximum number of objects returned in one request is 100" 11 | ) 12 | 13 | params = {"query": query, "limit": limit, "offset": offset} 14 | response = self._request_get("search/channels", params=params) 15 | return [Channel.construct_from(x) for x in response["channels"] or []] 16 | 17 | def games(self, query, live=False): 18 | params = { 19 | "query": query, 20 | "live": live, 21 | } 22 | response = self._request_get("search/games", params=params) 23 | return [Game.construct_from(x) for x in response["games"] or []] 24 | 25 | def streams(self, query, limit=25, offset=0, hls=None): 26 | if limit > 100: 27 | raise TwitchAttributeException( 28 | "Maximum number of objects returned in one request is 100" 29 | ) 30 | 31 | params = {"query": query, "limit": limit, "offset": offset, "hls": hls} 32 | response = self._request_get("search/streams", params=params) 33 | return [Stream.construct_from(x) for x in response["streams"] or []] 34 | -------------------------------------------------------------------------------- /tests/api/test_clips.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import responses 4 | 5 | from twitch.client import TwitchClient 6 | from twitch.constants import BASE_URL 7 | from twitch.resources import Clip 8 | 9 | example_clip = { 10 | "broadcast_id": "25782478272", 11 | "title": "cold ace", 12 | "tracking_id": "102382269", 13 | "url": "https://clips.twitch.tv/OpenUglySnoodVoteNay?tt_medium=clips_api&tt_content=url", 14 | } 15 | 16 | example_clips = {"clips": [example_clip]} 17 | 18 | 19 | @responses.activate 20 | def test_get_by_slug(): 21 | slug = "OpenUglySnoodVoteNay" 22 | 23 | responses.add( 24 | responses.GET, 25 | "{}clips/{}".format(BASE_URL, slug), 26 | body=json.dumps(example_clip), 27 | status=200, 28 | content_type="application/json", 29 | ) 30 | 31 | client = TwitchClient("client id") 32 | clip = client.clips.get_by_slug(slug) 33 | 34 | assert isinstance(clip, Clip) 35 | assert clip.broadcast_id == example_clip["broadcast_id"] 36 | 37 | 38 | @responses.activate 39 | def test_get_top(): 40 | params = {"limit": 1, "period": "month"} 41 | 42 | responses.add( 43 | responses.GET, 44 | "{}clips/top".format(BASE_URL), 45 | body=json.dumps(example_clips), 46 | status=200, 47 | content_type="application/json", 48 | ) 49 | 50 | client = TwitchClient("client id") 51 | clips = client.clips.get_top(**params) 52 | 53 | assert len(clips) == len(example_clips) 54 | assert isinstance(clips[0], Clip) 55 | assert clips[0].broadcast_id == example_clips["clips"][0]["broadcast_id"] 56 | -------------------------------------------------------------------------------- /docs/v5/videos.rst: -------------------------------------------------------------------------------- 1 | Videos 2 | ====== 3 | 4 | .. currentmodule:: twitch.api.videos 5 | 6 | .. class:: Videos() 7 | 8 | This class provides methods for easy access to `Twitch Videos API`_. 9 | 10 | .. classmethod:: get_by_id(video_id) 11 | 12 | Gets a Video object based on specified video ID. 13 | 14 | :param 'string video_id: Video ID 15 | 16 | 17 | .. classmethod:: get_top(limit, offset, game, period, broadcast_type) 18 | 19 | Gets a list of top videos. 20 | 21 | :param int limit: Maximum number of objects to return. Default 10. Maximum 100. 22 | :param int offset: Object offset for pagination of result. Default 0. 23 | :param string game: Name of the game. 24 | :param string period: Window of time to search. Default PERIOD_WEEK. 25 | :param string broadcast_type: Type of broadcast returned. Default BROADCAST_TYPE_HIGHLIGHT. 26 | 27 | 28 | .. classmethod:: get_followed_videos(limit, offset, broadcast_type) 29 | 30 | Gets a list of followed videos based on a specified OAuth token. 31 | 32 | :param int limit: Maximum number of objects to return. Default 10. Maximum 100. 33 | :param int offset: Object offset for pagination of result. Default 0. 34 | :param string broadcast_type: Type of broadcast returned. Default BROADCAST_TYPE_HIGHLIGHT. 35 | 36 | 37 | .. code-block:: python 38 | 39 | >>> from twitch import TwitchClient 40 | >>> client = TwitchClient('', '') 41 | >>> videos = client.videos.get_followed_videos() 42 | 43 | 44 | 45 | .. _`Twitch Videos API`: https://dev.twitch.tv/docs/v5/reference/videos/ 46 | -------------------------------------------------------------------------------- /samples/helix.py: -------------------------------------------------------------------------------- 1 | from itertools import islice 2 | 3 | from twitch import TwitchHelix 4 | 5 | 6 | def clips(): 7 | client = TwitchHelix() 8 | clip = client.get_clips(clip_ids=["AwkwardHelplessSalamanderSwiftRage"]) 9 | print(clip) 10 | 11 | 12 | def games(): 13 | client = TwitchHelix() 14 | games = client.get_games(game_ids=[493057], names=["World of Warcraft"]) 15 | print(games) 16 | 17 | 18 | def streams(): 19 | client = TwitchHelix() 20 | streams_iterator = client.get_streams() 21 | print(streams_iterator.next()) 22 | 23 | 24 | def first_500_streams(): 25 | client = TwitchHelix() 26 | streams_iterator = client.get_streams(page_size=100) 27 | for stream in islice(streams_iterator, 0, 500): 28 | print(stream) 29 | 30 | 31 | def top_games(): 32 | client = TwitchHelix() 33 | games_iterator = client.get_top_games(page_size=3) 34 | for game in islice(games_iterator, 0, 6): 35 | print(game) 36 | 37 | 38 | def videos(): 39 | client = TwitchHelix() 40 | videos_iterator = client.get_videos(game_id=493057, page_size=5) 41 | for video in islice(videos_iterator, 0, 10): 42 | print(video) 43 | 44 | 45 | def streams_metadata(): 46 | client = TwitchHelix() 47 | streams_metadata_iterator = client.get_streams_metadata() 48 | for metadata in islice(streams_metadata_iterator, 0, 10): 49 | print(metadata) 50 | 51 | 52 | def user_follows(): 53 | client = TwitchHelix() 54 | user_follows_iterator = client.get_user_follows(to_id=23161357) 55 | print("Total: {}".format(user_follows_iterator.total)) 56 | for user_follow in islice(user_follows_iterator, 0, 10): 57 | print(user_follow) 58 | -------------------------------------------------------------------------------- /docs/v5/search.rst: -------------------------------------------------------------------------------- 1 | Search 2 | ====== 3 | 4 | .. currentmodule:: twitch.api.search 5 | 6 | .. class:: Search() 7 | 8 | This class provides methods for easy access to `Twitch Search API`_. 9 | 10 | .. classmethod:: channels(query, limit, offset) 11 | 12 | Searches for channels based on a specified query parameter. 13 | 14 | :param string query: Search query 15 | :param int limit: Maximum number of objects to return. Default 25. Maximum 100. 16 | :param int offset: Object offset for pagination of result. Default 0. 17 | 18 | .. code-block:: python 19 | 20 | >>> from twitch import TwitchClient 21 | >>> client = TwitchClient('') 22 | >>> channels = client.search.channels('lirik', limit=69, offset=420) 23 | 24 | 25 | .. classmethod:: games(query, live) 26 | 27 | Searches for games based on a specified query parameter. 28 | 29 | :param string query: Search query 30 | :param boolean live: If `True`, returns only games that are live on at least one channel. 31 | Default: `False`. 32 | 33 | .. classmethod:: streams(query, limit, offset, hls) 34 | 35 | Searches for streams based on a specified query parameter. 36 | 37 | :param string query: Search query 38 | :param int limit: Maximum number of objects to return. Default 25. Maximum 100. 39 | :param int offset: Object offset for pagination of result. Default 0. 40 | :param boolean hls: If `True`, returns only HLS streams; if `False`, only RTMP streams; 41 | if `None`, both HLS and RTMP streams. 42 | 43 | 44 | 45 | .. _`Twitch Search API`: https://dev.twitch.tv/docs/v5/reference/search/ 46 | -------------------------------------------------------------------------------- /docs/v5/communities.rst: -------------------------------------------------------------------------------- 1 | Communities 2 | =========== 3 | 4 | .. currentmodule:: twitch.api.communities 5 | 6 | .. class:: Communities() 7 | 8 | This class provides methods for easy access to `Twitch Communities API`_. 9 | 10 | .. classmethod:: get_by_name(community_name) 11 | 12 | Gets a specified community. 13 | 14 | :param string community_name: Name of the community 15 | 16 | 17 | .. classmethod:: get_by_id(community_id) 18 | 19 | Gets a Community object based on specified user id. 20 | 21 | :param string community_id: Community ID 22 | 23 | 24 | .. classmethod:: create(name, summary, description, rules) 25 | 26 | Creates a community. 27 | 28 | :param string name: Community name. 29 | :param string summary: Short description of the community. 30 | :param string description: Long description of the community. 31 | :param string rules: Rules displayed when viewing a community page. 32 | 33 | 34 | .. classmethod:: update(community_id, summary, description, rules, email) 35 | 36 | Updates a community. 37 | 38 | :param string community_id: Community ID 39 | :param string summary: Short description of the community. 40 | :param string description: Long description of the community. 41 | :param string rules: Rules displayed when viewing a community page. 42 | :param string email: Email address of the community owner. 43 | 44 | 45 | .. code-block:: python 46 | 47 | >>> from twitch import TwitchClient 48 | >>> client = TwitchClient('', '') 49 | >>> community = client.communities.update(12345, 'foo', 'bar') 50 | 51 | 52 | 53 | .. _`Twitch Communities API`: https://dev.twitch.tv/docs/v5/reference/communities/ 54 | -------------------------------------------------------------------------------- /twitch/api/clips.py: -------------------------------------------------------------------------------- 1 | from twitch.api.base import TwitchAPI 2 | from twitch.constants import PERIODS 3 | from twitch.decorators import oauth_required 4 | from twitch.exceptions import TwitchAttributeException 5 | from twitch.resources import Clip 6 | 7 | 8 | class Clips(TwitchAPI): 9 | def get_by_slug(self, slug): 10 | response = self._request_get("clips/{}".format(slug)) 11 | return Clip.construct_from(response) 12 | 13 | def get_top( 14 | self, 15 | channel=None, 16 | cursor=None, 17 | game=None, 18 | language=None, 19 | limit=10, 20 | period="week", 21 | trending=False, 22 | ): 23 | if limit > 100: 24 | raise TwitchAttributeException( 25 | "Maximum number of objects returned in one request is 100" 26 | ) 27 | 28 | if period not in PERIODS: 29 | raise TwitchAttributeException( 30 | "Period is not valid. Valid values are {}".format(PERIODS) 31 | ) 32 | 33 | params = { 34 | "channel": channel, 35 | "cursor": cursor, 36 | "game": game, 37 | "language": language, 38 | "limit": limit, 39 | "period": period, 40 | "trending": str(trending).lower(), 41 | } 42 | 43 | response = self._request_get("clips/top", params=params) 44 | return [Clip.construct_from(x) for x in response["clips"]] 45 | 46 | @oauth_required 47 | def followed(self, limit=10, cursor=None, trending=False): 48 | if limit > 100: 49 | raise TwitchAttributeException( 50 | "Maximum number of objects returned in one request is 100" 51 | ) 52 | 53 | params = {"limit": limit, "cursor": cursor, "trending": trending} 54 | 55 | response = self._request_get("clips/followed", params=params) 56 | return [Clip.construct_from(x) for x in response["clips"]] 57 | -------------------------------------------------------------------------------- /tests/api/test_teams.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | import responses 5 | 6 | from twitch.client import TwitchClient 7 | from twitch.constants import BASE_URL 8 | from twitch.exceptions import TwitchAttributeException 9 | from twitch.resources import Team 10 | 11 | example_team_response = { 12 | "_id": 10, 13 | "name": "staff", 14 | } 15 | 16 | example_all_response = {"teams": [example_team_response]} 17 | 18 | 19 | @responses.activate 20 | def test_get(): 21 | team_name = "spongebob" 22 | responses.add( 23 | responses.GET, 24 | "{}teams/{}".format(BASE_URL, team_name), 25 | body=json.dumps(example_team_response), 26 | status=200, 27 | content_type="application/json", 28 | ) 29 | 30 | client = TwitchClient("client id", "oauth token") 31 | 32 | team = client.teams.get(team_name) 33 | 34 | assert len(responses.calls) == 1 35 | assert isinstance(team, Team) 36 | assert team.id == example_team_response["_id"] 37 | assert team.name == example_team_response["name"] 38 | 39 | 40 | @responses.activate 41 | def test_get_all(): 42 | responses.add( 43 | responses.GET, 44 | "{}teams".format(BASE_URL), 45 | body=json.dumps(example_all_response), 46 | status=200, 47 | content_type="application/json", 48 | ) 49 | 50 | client = TwitchClient("client id") 51 | 52 | teams = client.teams.get_all() 53 | 54 | assert len(responses.calls) == 1 55 | assert len(teams) == 1 56 | team = teams[0] 57 | assert isinstance(team, Team) 58 | assert team.id == example_team_response["_id"] 59 | assert team.name == example_team_response["name"] 60 | 61 | 62 | @responses.activate 63 | @pytest.mark.parametrize("param,value", [("limit", 101)]) 64 | def test_get_all_raises_if_wrong_params_are_passed_in(param, value): 65 | client = TwitchClient("client id") 66 | kwargs = {param: value} 67 | with pytest.raises(TwitchAttributeException): 68 | client.teams.get_all(**kwargs) 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | python-twitch-client 2 | ==================== 3 | 4 | [![Latest docs][docs-img]][docs] 5 | [![Latest version][pypi-img]][pypi] 6 | [![Latest build][ci-img]][gh-actions] 7 | [![Coverage][codecov-img]][codecov] 8 | 9 | 10 | 11 | 12 | `python-twitch-client` is an easy to use Python library for accessing the 13 | Twitch API 14 | 15 | You can find more information in the [documentation][docs] or for support, you can join the [Discord Server](https://discord.me/twitch-api). 16 | 17 | 18 | Note 19 | ============================================== 20 | 21 | `python-twitch-client` currently supports Twitch API v5 and the new Helix API. 22 | 23 | If you find a missing endpoint or a bug please raise an [issue][issues] or 24 | contribute and open a [pull request][pulls]. 25 | 26 | 27 | Basic Usage 28 | ============================================== 29 | Helix API 30 | 31 | ```python 32 | from itertools import islice 33 | from twitch import TwitchHelix 34 | 35 | client = TwitchHelix(client_id='') 36 | streams_iterator = client.get_streams(page_size=100) 37 | for stream in islice(streams_iterator, 0, 500): 38 | print(stream) 39 | ``` 40 | 41 | 42 | Twitch API v5 43 | ```python 44 | from twitch import TwitchClient 45 | 46 | client = TwitchClient(client_id='') 47 | channel = client.channels.get_by_id(44322889) 48 | 49 | print(channel.id) 50 | print(channel.name) 51 | print(channel.display_name) 52 | ``` 53 | 54 | [docs]: http://python-twitch-client.rtfd.io 55 | [docs-img]: https://readthedocs.org/projects/python-twitch-client/badge/?version=latest (Latest docs) 56 | [pulls]: https://github.com/tsifrer/python-twitch-client/pulls 57 | [issues]: https://github.com/tsifrer/python-twitch-client/issues 58 | [pypi]: https://pypi.python.org/pypi/python-twitch-client/ 59 | [pypi-img]: https://img.shields.io/pypi/v/python-twitch-client.svg 60 | [codecov]: https://codecov.io/gh/tsifrer/python-twitch-client 61 | [codecov-img]: https://codecov.io/gh/tsifrer/python-twitch-client/branch/master/graph/badge.svg 62 | [gh-actions]: https://github.com/tsifrer/python-twitch-client/actions 63 | [ci-img]: https://github.com/tsifrer/python-twitch-client/workflows/CI/badge.svg 64 | -------------------------------------------------------------------------------- /docs/v5/clips.rst: -------------------------------------------------------------------------------- 1 | Clips 2 | ===== 3 | 4 | .. currentmodule:: twitch.api.clips 5 | 6 | .. class:: Clips() 7 | 8 | This class provides methods for easy access to `Twitch Clips API`_. 9 | 10 | .. classmethod:: get_by_slug(slug) 11 | 12 | Gets a clip object based on the slug provided 13 | 14 | :param string slug: Twitch Slug. 15 | 16 | 17 | .. classmethod:: get_top(channel, cursor, game, language, limit, period, trending) 18 | 19 | Gets all clips emoticons in one or more specified sets. 20 | 21 | :param string channel: Channel name. If this is specified, top clips for only this channel are returned; otherwise, top clips for all channels are returned. If both channel and game are specified, game is ignored. 22 | :param string cursor: Tells the server where to start fetching the next set of results, in a multi-page response. 23 | :param string game: Game name. (Game names can be retrieved with the Search Games endpoint.) If this is specified, top clips for only this game are returned; otherwise, top clips for all games are returned. If both channel and game are specified, game is ignored. 24 | :param string language: Comma-separated list of languages, which constrains the languages of videos returned. Examples: es, en,es,th. If no language is specified, all languages are returned. 25 | :param int limit: Maximum number of most-recent objects to return. Default: 10. Maximum: 100. 26 | :param string period: The window of time to search for clips. Valid values: day, week, month, all. Default: week. 27 | :param boolean trending: If True, the clips returned are ordered by popularity; otherwise, by viewcount. Default: False. 28 | 29 | 30 | .. classmethod:: followed() 31 | 32 | Gets top clips for games followed by the user identified by OAuth token. Results are ordered by popularity. 33 | 34 | :param int limit: Maximum number of most-recent objects to return. Default: 10. Maximum: 100. 35 | :param string cursor: Tells the server where to start fetching the next set of results, in a multi-page response. 36 | :param boolean trending: If true, the clips returned are ordered by popularity; otherwise, by viewcount. Default: false. 37 | 38 | 39 | .. _`Twitch Clips API`: https://dev.twitch.tv/docs/v5/reference/clips 40 | -------------------------------------------------------------------------------- /tests/api/test_chat.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import responses 4 | 5 | from twitch.client import TwitchClient 6 | from twitch.constants import BASE_URL 7 | 8 | example_emote = {"code": "TwitchLit", "id": 115390} 9 | 10 | 11 | @responses.activate 12 | def test_get_badges_by_channel(): 13 | channel_id = 7236692 14 | response = { 15 | "admin": { 16 | "alpha": "https://static-cdn.jtvnw.net/chat-badges/admin-alpha.png", 17 | "image": "https://static-cdn.jtvnw.net/chat-badges/admin.png", 18 | "svg": "https://static-cdn.jtvnw.net/chat-badges/admin.svg", 19 | } 20 | } 21 | responses.add( 22 | responses.GET, 23 | "{}chat/{}/badges".format(BASE_URL, channel_id), 24 | body=json.dumps(response), 25 | status=200, 26 | content_type="application/json", 27 | ) 28 | 29 | client = TwitchClient("client id") 30 | 31 | badges = client.chat.get_badges_by_channel(channel_id) 32 | 33 | assert len(responses.calls) == 1 34 | assert isinstance(badges, dict) 35 | assert badges["admin"] == response["admin"] 36 | 37 | 38 | @responses.activate 39 | def test_get_emoticons_by_set(): 40 | response = {"emoticon_sets": {"19151": [example_emote]}} 41 | responses.add( 42 | responses.GET, 43 | "{}chat/emoticon_images".format(BASE_URL), 44 | body=json.dumps(response), 45 | status=200, 46 | content_type="application/json", 47 | ) 48 | 49 | client = TwitchClient("client id") 50 | 51 | emoticon_sets = client.chat.get_emoticons_by_set() 52 | 53 | assert len(responses.calls) == 1 54 | assert isinstance(emoticon_sets, dict) 55 | assert emoticon_sets["emoticon_sets"] == response["emoticon_sets"] 56 | assert emoticon_sets["emoticon_sets"]["19151"][0] == example_emote 57 | 58 | 59 | @responses.activate 60 | def test_get_all_emoticons(): 61 | response = {"emoticons": [example_emote]} 62 | responses.add( 63 | responses.GET, 64 | "{}chat/emoticons".format(BASE_URL), 65 | body=json.dumps(response), 66 | status=200, 67 | content_type="application/json", 68 | ) 69 | 70 | client = TwitchClient("client id") 71 | 72 | emoticon_sets = client.chat.get_all_emoticons() 73 | 74 | assert len(responses.calls) == 1 75 | assert isinstance(emoticon_sets, dict) 76 | assert emoticon_sets["emoticons"] == response["emoticons"] 77 | assert emoticon_sets["emoticons"][0] == example_emote 78 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | 2 | Welcome to ``python-twitch-client`` 3 | =================================== 4 | 5 | An easy to use Python library for accessing the Twitch API 6 | 7 | .. warning:: 8 | This documentation is a work in progress 9 | 10 | .. note:: 11 | ``python-twitch-client`` currently supports Helix API and Twitch API v5. 12 | 13 | Helix API integration is a work in progress and some endpoints might be missing. 14 | 15 | If you find a missing endpoint or a bug please raise an issue_ or contribute and open a 16 | `pull request`_. 17 | 18 | 19 | Installation 20 | ============ 21 | 22 | You can install ``python-twitch-client`` with ``pip``: 23 | 24 | .. code-block:: console 25 | 26 | $ pip install python-twitch-client 27 | 28 | 29 | ``python-twitch-client`` is currently only tested and confirmed working on Linux and Mac. If you're 30 | on a Windows machine and getting any bugs, please open a bug and help us find a solution. 31 | 32 | 33 | Authentication 34 | ============== 35 | 36 | Before you can use Twitch API you need to get the client ID. To get one, you should follow the 37 | steps on `Twitch Authentication page`_. 38 | 39 | Some of the endpoints also require OAuth token. To get one for testing purposes, you can use the 40 | free `tokengen tool`_ or use TwitchHelix's ``get_oauth`` method. 41 | 42 | There are two ways to pass credentials into the TwitchClient. The first and easiest way is to 43 | just pass the credentials as an argument: 44 | 45 | .. code-block:: python 46 | 47 | client = TwitchClient(client_id='', oauth_token='') 48 | 49 | Other option is to create a config file `~/.twitch.cfg` which is a text file formatted as .ini 50 | configuration file. 51 | 52 | An example of the config file might look like: 53 | 54 | .. code-block:: none 55 | 56 | [Credentials] 57 | client_id = 58 | oauth_token = 59 | 60 | .. note:: 61 | You only need to provide ``oauth_token`` if you're calling endpoints that need it. 62 | 63 | If you call functions that require ``oauth_token`` and you did not provide it, functions will 64 | raise ``TwitchAuthException`` exception. 65 | 66 | 67 | Contents: 68 | --------- 69 | 70 | .. toctree:: 71 | :maxdepth: 0 72 | 73 | basic_usage 74 | helix 75 | v5/index 76 | 77 | 78 | .. _issue: https://github.com/tsifrer/python-twitch-client/issues 79 | .. _`pull request`: https://github.com/tsifrer/python-twitch-client/pulls 80 | .. _`tokengen tool`: https://twitchapps.com/tokengen/ 81 | .. _`Twitch Authentication page`: https://dev.twitch.tv/docs/v5/guides/authentication/ 82 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | from codecs import open 4 | 5 | import setuptools 6 | from setuptools.command.test import test as TestCommand 7 | 8 | 9 | class PyTest(TestCommand): 10 | user_options = [("pytest-args=", "a", "Arguments to pass into pytest")] 11 | 12 | def initialize_options(self): 13 | TestCommand.initialize_options(self) 14 | self.pytest_args = "" 15 | 16 | def finalize_options(self): 17 | TestCommand.finalize_options(self) 18 | self.test_args = [] 19 | self.test_suite = True 20 | 21 | def run_tests(self): 22 | import pytest 23 | 24 | errno = pytest.main(self.pytest_args.split(" ")) 25 | sys.exit(errno) 26 | 27 | 28 | cmdclass = {"test": PyTest} 29 | 30 | if "build_docs" in sys.argv: 31 | from sphinx.setup_command import BuildDoc 32 | 33 | cmdclass["build_docs"] = BuildDoc 34 | 35 | requires = ["requests>=2.23.0"] 36 | 37 | test_requirements = [ 38 | "black==20.8b1", 39 | "codecov>=2.1.10", 40 | "flake8-isort>=4.0.0", 41 | "flake8>=3.8.4", 42 | "isort>=5.6.4", 43 | "pytest-cov>=2.10.1", 44 | "pytest>=6.1.2", 45 | "responses>=0.12.1", 46 | ] 47 | 48 | doc_reqs = ["Sphinx==3.3.1", "sphinx_rtd_theme==0.5.0"] 49 | 50 | extras_require = { 51 | "doc": doc_reqs, 52 | "test": test_requirements, 53 | } 54 | 55 | if int(setuptools.__version__.split(".")[0]) < 18: 56 | extras_require = {} 57 | if sys.version_info < (3, 2): 58 | requires.append("configparser") 59 | 60 | 61 | with open("twitch/__init__.py", "r") as f: 62 | version = re.search( 63 | r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', f.read(), re.MULTILINE 64 | ).group(1) 65 | 66 | if not version: 67 | raise RuntimeError("Cannot find version information") 68 | 69 | setuptools.setup( 70 | name="python-twitch-client", 71 | version=version, 72 | description="Easy to use Python library for accessing the Twitch API", 73 | author="Tomaz Sifrer", 74 | author_email="tomazz.sifrer@gmail.com", 75 | url="https://github.com/tsifrer/python-twitch-client", 76 | packages=setuptools.find_packages(include=["twitch", "twitch.*"]), 77 | cmdclass=cmdclass, 78 | install_requires=requires, 79 | tests_require=test_requirements, 80 | extras_require=extras_require, 81 | python_requires=">=3.6, <4", 82 | license="MIT", 83 | zip_safe=False, 84 | classifiers=[ 85 | "Intended Audience :: Developers", 86 | "License :: OSI Approved :: MIT License", 87 | "Natural Language :: English", 88 | "Programming Language :: Python :: 3", 89 | "Programming Language :: Python :: 3.6", 90 | "Programming Language :: Python :: 3.7", 91 | "Programming Language :: Python :: 3.8", 92 | "Topic :: Internet", 93 | "Topic :: Software Development :: Libraries :: Python Modules", 94 | ], 95 | ) 96 | -------------------------------------------------------------------------------- /docs/v5/streams.rst: -------------------------------------------------------------------------------- 1 | Streams 2 | ======= 3 | 4 | .. currentmodule:: twitch.api.streams 5 | 6 | .. class:: Streams() 7 | 8 | This class provides methods for easy access to `Twitch Streams API`_. 9 | 10 | .. classmethod:: get_stream_by_user(channel_id, stream_type) 11 | 12 | Gets stream information for a specified user. 13 | 14 | :param string channel_id: ID of the channel you want to get information of 15 | :param string stream_type: Constrains the type of streams returned. 16 | Default STREAM_TYPE_LIVE. 17 | 18 | 19 | .. classmethod:: get_live_streams(channel, game, language, stream_type, limit, offset) 20 | 21 | Gets a list of live streams. 22 | 23 | :param string channel: Comma-separated list of channel IDs you want to get 24 | :param string game: Game of the streams returned 25 | :param string language: Constrains the language of the streams returned 26 | :param string stream_type: Constrains the type of streams returned. 27 | Default STREAM_TYPE_LIVE. 28 | :param int limit: Maximum number of objects to return. Default 25. Maximum 100. 29 | :param int offset: Object offset for pagination of result. Default 0. 30 | 31 | 32 | .. classmethod:: get_summary(game) 33 | 34 | Gets a list of summaries of live streams. 35 | 36 | :param string game: Game of the streams returned 37 | 38 | 39 | .. classmethod:: get_featured(limit, offset) 40 | 41 | Gets a list of all featured live streams. 42 | 43 | :param int limit: Maximum number of objects to return. Default 25. Maximum 100. 44 | :param int offset: Object offset for pagination of result. Default 0. 45 | 46 | 47 | .. classmethod:: get_followed(stream_type, limit, offset) 48 | 49 | Gets a list of online streams a user is following, based on a specified OAuth token. 50 | 51 | :param string stream_type: Constrains the type of streams returned. 52 | Default STREAM_TYPE_LIVE. 53 | :param int limit: Maximum number of objects to return. Default 25. Maximum 100. 54 | :param int offset: Object offset for pagination of result. Default 0. 55 | 56 | 57 | .. code-block:: python 58 | 59 | >>> from twitch import TwitchClient 60 | >>> client = TwitchClient('', '') 61 | >>> followed = client.streams.get_followed() 62 | 63 | 64 | .. classmethod:: get_streams_in_community(community_id) 65 | 66 | Gets a list of streams in a community. (From Twitch forum `Communities API Release`_) 67 | 68 | :param string community_id: Community ID 69 | 70 | 71 | .. code-block:: python 72 | 73 | >>> from twitch import TwitchClient 74 | >>> client = TwitchClient('') 75 | >>> streams = client.streams.get_streams_in_community('5181e78f-2280-42a6-873d-758e25a7c313') 76 | 77 | 78 | .. _`Twitch Streams API`: https://dev.twitch.tv/docs/v5/reference/streams/ 79 | .. _`Communities API Release`: https://discuss.dev.twitch.tv/t/communities-api-release/9211 80 | -------------------------------------------------------------------------------- /twitch/api/collections.py: -------------------------------------------------------------------------------- 1 | from twitch.api.base import TwitchAPI 2 | from twitch.decorators import oauth_required 3 | from twitch.exceptions import TwitchAttributeException 4 | from twitch.resources import Collection, Item 5 | 6 | 7 | class Collections(TwitchAPI): 8 | def get_metadata(self, collection_id): 9 | response = self._request_get("collections/{}".format(collection_id)) 10 | return Collection.construct_from(response) 11 | 12 | def get(self, collection_id, include_all_items=False): 13 | params = {"include_all_items": include_all_items} 14 | response = self._request_get( 15 | "collections/{}/items".format(collection_id), params=params 16 | ) 17 | return [Item.construct_from(x) for x in response["items"]] 18 | 19 | def get_by_channel(self, channel_id, limit=10, cursor=None, containing_item=None): 20 | if limit > 100: 21 | raise TwitchAttributeException( 22 | "Maximum number of objects returned in one request is 100" 23 | ) 24 | params = { 25 | "limit": limit, 26 | "cursor": cursor, 27 | } 28 | if containing_item: 29 | params["containing_item"] = containing_item 30 | response = self._request_get("channels/{}/collections".format(channel_id)) 31 | return [Collection.construct_from(x) for x in response["collections"]] 32 | 33 | @oauth_required 34 | def create(self, channel_id, title): 35 | data = { 36 | "title": title, 37 | } 38 | response = self._request_post( 39 | "channels/{}/collections".format(channel_id), data=data 40 | ) 41 | return Collection.construct_from(response) 42 | 43 | @oauth_required 44 | def update(self, collection_id, title): 45 | data = { 46 | "title": title, 47 | } 48 | self._request_put("collections/{}".format(collection_id), data=data) 49 | 50 | @oauth_required 51 | def create_thumbnail(self, collection_id, item_id): 52 | data = { 53 | "item_id": item_id, 54 | } 55 | self._request_put("collections/{}/thumbnail".format(collection_id), data=data) 56 | 57 | @oauth_required 58 | def delete(self, collection_id): 59 | self._request_delete("collections/{}".format(collection_id)) 60 | 61 | @oauth_required 62 | def add_item(self, collection_id, item_id, item_type): 63 | data = {"id": item_id, "type": item_type} 64 | response = self._request_put( 65 | "collections/{}/items".format(collection_id), data=data 66 | ) 67 | return Item.construct_from(response) 68 | 69 | @oauth_required 70 | def delete_item(self, collection_id, collection_item_id): 71 | url = "collections/{}/items/{}".format(collection_id, collection_item_id) 72 | self._request_delete(url) 73 | 74 | @oauth_required 75 | def move_item(self, collection_id, collection_item_id, position): 76 | data = {"position": position} 77 | url = "collections/{}/items/{}".format(collection_id, collection_item_id) 78 | self._request_put(url, data=data) 79 | -------------------------------------------------------------------------------- /twitch/api/videos.py: -------------------------------------------------------------------------------- 1 | from twitch.api.base import TwitchAPI 2 | from twitch.constants import ( 3 | BROADCAST_TYPE_HIGHLIGHT, 4 | BROADCAST_TYPES, 5 | PERIOD_WEEK, 6 | PERIODS, 7 | VOD_FETCH_URL, 8 | ) 9 | from twitch.decorators import oauth_required 10 | from twitch.exceptions import TwitchAttributeException 11 | from twitch.resources import Video 12 | 13 | 14 | class Videos(TwitchAPI): 15 | def get_by_id(self, video_id): 16 | response = self._request_get("videos/{}".format(video_id)) 17 | return Video.construct_from(response) 18 | 19 | def get_top( 20 | self, 21 | limit=10, 22 | offset=0, 23 | game=None, 24 | period=PERIOD_WEEK, 25 | broadcast_type=BROADCAST_TYPE_HIGHLIGHT, 26 | ): 27 | if limit > 100: 28 | raise TwitchAttributeException( 29 | "Maximum number of objects returned in one request is 100" 30 | ) 31 | if period not in PERIODS: 32 | raise TwitchAttributeException( 33 | "Period is not valid. Valid values are {}".format(PERIODS) 34 | ) 35 | 36 | if broadcast_type not in BROADCAST_TYPES: 37 | raise TwitchAttributeException( 38 | "Broadcast type is not valid. Valid values are {}".format( 39 | BROADCAST_TYPES 40 | ) 41 | ) 42 | 43 | params = { 44 | "limit": limit, 45 | "offset": offset, 46 | "game": game, 47 | "period": period, 48 | "broadcast_type": ",".join(broadcast_type), 49 | } 50 | 51 | response = self._request_get("videos/top", params=params) 52 | return [Video.construct_from(x) for x in response["vods"]] 53 | 54 | @oauth_required 55 | def get_followed_videos( 56 | self, limit=10, offset=0, broadcast_type=BROADCAST_TYPE_HIGHLIGHT 57 | ): 58 | if limit > 100: 59 | raise TwitchAttributeException( 60 | "Maximum number of objects returned in one request is 100" 61 | ) 62 | 63 | if broadcast_type not in BROADCAST_TYPES: 64 | raise TwitchAttributeException( 65 | "Broadcast type is not valid. Valid values are {}".format( 66 | BROADCAST_TYPES 67 | ) 68 | ) 69 | 70 | params = {"limit": limit, "offset": offset, "broadcast_type": broadcast_type} 71 | 72 | response = self._request_get("videos/followed", params=params) 73 | return [Video.construct_from(x) for x in response["videos"]] 74 | 75 | def download_vod(self, video_id): 76 | """ 77 | This will return a byte string of the M3U8 playlist data 78 | (which contains more links to segments of the vod) 79 | """ 80 | vod_id = video_id[1:] 81 | token = self._request_get( 82 | "vods/{}/access_token".format(vod_id), url="https://api.twitch.tv/api/" 83 | ) 84 | params = {"nauthsig": token["sig"], "nauth": token["token"]} 85 | m3u8 = self._request_get( 86 | "vod/{}".format(vod_id), url=VOD_FETCH_URL, params=params, json=False 87 | ) 88 | return m3u8.content 89 | -------------------------------------------------------------------------------- /twitch/api/base.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import requests 4 | from requests.compat import urljoin 5 | 6 | from twitch.conf import backoff_config 7 | from twitch.constants import BASE_URL 8 | 9 | DEFAULT_TIMEOUT = 10 10 | 11 | 12 | class TwitchAPI(object): 13 | """Twitch API client.""" 14 | 15 | def __init__(self, client_id, oauth_token=None): 16 | """Initialize the API.""" 17 | super(TwitchAPI, self).__init__() 18 | self._client_id = client_id 19 | self._oauth_token = oauth_token 20 | self._initial_backoff, self._max_retries = backoff_config() 21 | 22 | def _get_request_headers(self): 23 | """Prepare the headers for the requests.""" 24 | headers = { 25 | "Accept": "application/vnd.twitchtv.v5+json", 26 | "Client-ID": self._client_id, 27 | } 28 | 29 | if self._oauth_token: 30 | headers["Authorization"] = "OAuth {}".format(self._oauth_token) 31 | 32 | return headers 33 | 34 | def _request_get(self, path, params=None, json=True, url=BASE_URL): 35 | """Perform a HTTP GET request.""" 36 | url = urljoin(url, path) 37 | headers = self._get_request_headers() 38 | 39 | response = requests.get(url, params=params, headers=headers) 40 | if response.status_code >= 500: 41 | 42 | backoff = self._initial_backoff 43 | for _ in range(self._max_retries): 44 | time.sleep(backoff) 45 | backoff_response = requests.get( 46 | url, params=params, headers=headers, timeout=DEFAULT_TIMEOUT 47 | ) 48 | if backoff_response.status_code < 500: 49 | response = backoff_response 50 | break 51 | backoff *= 2 52 | 53 | response.raise_for_status() 54 | if json: 55 | return response.json() 56 | else: 57 | return response 58 | 59 | def _request_post(self, path, data=None, params=None, url=BASE_URL): 60 | """Perform a HTTP POST request..""" 61 | url = urljoin(url, path) 62 | 63 | headers = self._get_request_headers() 64 | 65 | response = requests.post( 66 | url, json=data, params=params, headers=headers, timeout=DEFAULT_TIMEOUT 67 | ) 68 | response.raise_for_status() 69 | if response.status_code == 200: 70 | return response.json() 71 | 72 | def _request_put(self, path, data=None, params=None, url=BASE_URL): 73 | """Perform a HTTP PUT request.""" 74 | url = urljoin(url, path) 75 | 76 | headers = self._get_request_headers() 77 | response = requests.put( 78 | url, json=data, params=params, headers=headers, timeout=DEFAULT_TIMEOUT 79 | ) 80 | response.raise_for_status() 81 | if response.status_code == 200: 82 | return response.json() 83 | 84 | def _request_delete(self, path, params=None, url=BASE_URL): 85 | """Perform a HTTP DELETE request.""" 86 | url = urljoin(url, path) 87 | 88 | headers = self._get_request_headers() 89 | 90 | response = requests.delete( 91 | url, params=params, headers=headers, timeout=DEFAULT_TIMEOUT 92 | ) 93 | response.raise_for_status() 94 | if response.status_code == 200: 95 | return response.json() 96 | -------------------------------------------------------------------------------- /twitch/api/streams.py: -------------------------------------------------------------------------------- 1 | from twitch.api.base import TwitchAPI 2 | from twitch.constants import STREAM_TYPE_LIVE, STREAM_TYPES 3 | from twitch.decorators import oauth_required 4 | from twitch.exceptions import TwitchAttributeException 5 | from twitch.resources import Featured, Stream 6 | 7 | 8 | class Streams(TwitchAPI): 9 | def get_stream_by_user(self, channel_id, stream_type=STREAM_TYPE_LIVE): 10 | if stream_type not in STREAM_TYPES: 11 | raise TwitchAttributeException( 12 | "Stream type is not valid. Valid values are {}".format(STREAM_TYPES) 13 | ) 14 | 15 | params = { 16 | "stream_type": stream_type, 17 | } 18 | response = self._request_get("streams/{}".format(channel_id), params=params) 19 | 20 | if not response["stream"]: 21 | return None 22 | return Stream.construct_from(response["stream"]) 23 | 24 | def get_live_streams( 25 | self, 26 | channel=None, 27 | game=None, 28 | language=None, 29 | stream_type=STREAM_TYPE_LIVE, 30 | limit=25, 31 | offset=0, 32 | ): 33 | if limit > 100: 34 | raise TwitchAttributeException( 35 | "Maximum number of objects returned in one request is 100" 36 | ) 37 | 38 | params = {"stream_type": stream_type, "limit": limit, "offset": offset} 39 | if channel is not None: 40 | params["channel"] = channel 41 | if game is not None: 42 | params["game"] = game 43 | if language is not None: 44 | params["language"] = language 45 | response = self._request_get("streams", params=params) 46 | return [Stream.construct_from(x) for x in response["streams"]] 47 | 48 | def get_summary(self, game=None): 49 | params = {} 50 | if game is not None: 51 | params["game"] = game 52 | response = self._request_get("streams/summary", params=params) 53 | return response 54 | 55 | def get_featured(self, limit=25, offset=0): 56 | if limit > 100: 57 | raise TwitchAttributeException( 58 | "Maximum number of objects returned in one request is 100" 59 | ) 60 | 61 | params = {"limit": limit, "offset": offset} 62 | response = self._request_get("streams/featured", params=params) 63 | return [Featured.construct_from(x) for x in response["featured"]] 64 | 65 | @oauth_required 66 | def get_followed(self, stream_type=STREAM_TYPE_LIVE, limit=25, offset=0): 67 | if stream_type not in STREAM_TYPES: 68 | raise TwitchAttributeException( 69 | "Stream type is not valid. Valid values are {}".format(STREAM_TYPES) 70 | ) 71 | if limit > 100: 72 | raise TwitchAttributeException( 73 | "Maximum number of objects returned in one request is 100" 74 | ) 75 | 76 | params = {"stream_type": stream_type, "limit": limit, "offset": offset} 77 | response = self._request_get("streams/followed", params=params) 78 | return [Stream.construct_from(x) for x in response["streams"]] 79 | 80 | def get_streams_in_community(self, community_id): 81 | response = self._request_get("streams?community_id={}".format(community_id)) 82 | 83 | return [Stream.construct_from(x) for x in response["streams"]] 84 | -------------------------------------------------------------------------------- /twitch/constants.py: -------------------------------------------------------------------------------- 1 | BASE_URL = "https://api.twitch.tv/kraken/" 2 | BASE_HELIX_URL = "https://api.twitch.tv/helix/" 3 | BASE_OAUTH_URL = "https://id.twitch.tv/oauth2/" 4 | VOD_FETCH_URL = "https://usher.ttvnw.net/" 5 | 6 | CONFIG_FILE_PATH = "~/.twitch.cfg" 7 | 8 | PERIOD_DAY = "day" 9 | PERIOD_WEEK = "week" 10 | PERIOD_MONTH = "month" 11 | PERIOD_ALL = "all" 12 | PERIODS = [PERIOD_DAY, PERIOD_WEEK, PERIOD_MONTH, PERIOD_ALL] 13 | 14 | BROADCAST_TYPE_ARCHIVE = "archive" 15 | BROADCAST_TYPE_HIGHLIGHT = "highlight" 16 | BROADCAST_TYPE_UPLOAD = "upload" 17 | BROADCAST_TYPE_ARCHIVE_UPLOAD = "archive,upload" 18 | BROADCAST_TYPE_ARCHIVE_HIGHLIGHT = "archive,highlight" 19 | BROADCAST_TYPE_HIGHLIGHT_UPLOAD = "highlight,upload" 20 | BROADCAST_TYPE_ALL = "" 21 | 22 | BROADCAST_TYPES = [ 23 | BROADCAST_TYPE_ARCHIVE, 24 | BROADCAST_TYPE_HIGHLIGHT, 25 | BROADCAST_TYPE_UPLOAD, 26 | BROADCAST_TYPE_ARCHIVE_UPLOAD, 27 | BROADCAST_TYPE_ARCHIVE_HIGHLIGHT, 28 | BROADCAST_TYPE_HIGHLIGHT_UPLOAD, 29 | BROADCAST_TYPE_ALL, 30 | ] 31 | 32 | VIDEO_SORT_TIME = "time" 33 | VIDEO_SORT_TRENDING = "trending" 34 | VIDEO_SORT_VIEWS = "views" 35 | VIDEO_SORTS = [VIDEO_SORT_TIME, VIDEO_SORT_TRENDING, VIDEO_SORT_VIEWS] 36 | 37 | USERS_SORT_BY_CREATED_AT = "created_at" 38 | USERS_SORT_BY_LAST_BROADCAST = "last_broadcast" 39 | USERS_SORT_BY_LOGIN = "login" 40 | USERS_SORT_BY = [ 41 | USERS_SORT_BY_CREATED_AT, 42 | USERS_SORT_BY_LAST_BROADCAST, 43 | USERS_SORT_BY_LOGIN, 44 | ] 45 | MAX_FOLLOWS_LIMIT = 100 46 | 47 | DIRECTION_ASC = "asc" 48 | DIRECTION_DESC = "desc" 49 | DIRECTIONS = [DIRECTION_ASC, DIRECTION_DESC] 50 | 51 | STREAM_TYPE_LIVE = "live" 52 | STREAM_TYPE_PLAYLIST = "playlist" 53 | STREAM_TYPE_ALL = "all" 54 | STREAM_TYPES = [STREAM_TYPE_LIVE, STREAM_TYPE_PLAYLIST, STREAM_TYPE_ALL] 55 | 56 | VIDEO_TYPE_ALL = "all" 57 | VIDEO_TYPE_UPLAOD = "upload" 58 | VIDEO_TYPE_ARCHIVE = "archive" 59 | VIDEO_TYPE_HIGHLIGHT = "highlight" 60 | VIDEO_TYPES = [ 61 | VIDEO_TYPE_ALL, 62 | VIDEO_TYPE_UPLAOD, 63 | VIDEO_TYPE_ARCHIVE, 64 | VIDEO_TYPE_HIGHLIGHT, 65 | ] 66 | 67 | OAUTH_SCOPE_ANALYTICS_READ_EXTENSIONS = "analytics:read:extensions" 68 | OAUTH_SCOPE_ANALYTICS_READ_GAMES = "analytics:read:games" 69 | OAUTH_SCOPE_BITS_READ = "bits:read" 70 | OAUTH_SCOPE_CHANNEL_EDIT_COMMERICAL = "channel:edit:commercial" 71 | OAUTH_SCOPE_CHANNEL_MANAGE_BROADCAST = "channel:manage:broadcast" 72 | OAUTH_SCOPE_CHANNEL_MANAGE_EXTENSION = "channel:manage:extension" 73 | OAUTH_SCOPE_CHANNEL_READ_HYPE_TRAIN = "channel:read:hype_train" 74 | OAUTH_SCOPE_CHANNEL_READ_STREAM_KEY = "channel:read:stream_key" 75 | OAUTH_SCOPE_CHANNEL_READ_HYPE_SUBSCRIPTIONS = "channel:read:subscriptions" 76 | OAUTH_SCOPE_CLIPS_EDIT = "clips:edit" 77 | OAUTH_SCOPE_USER_EDIT = "user:edit" 78 | OAUTH_SCOPE_USER_EDIT_FOLLOWS = "user:edit:follows" 79 | OAUTH_SCOPE_USER_READ_BROADCAST = "user:read:broadcast" 80 | OAUTH_SCOPE_USER_READ_EMAIL = "user:read:email" 81 | 82 | OAUTH_SCOPES = [ 83 | OAUTH_SCOPE_ANALYTICS_READ_EXTENSIONS, 84 | OAUTH_SCOPE_ANALYTICS_READ_GAMES, 85 | OAUTH_SCOPE_BITS_READ, 86 | OAUTH_SCOPE_CHANNEL_EDIT_COMMERICAL, 87 | OAUTH_SCOPE_CHANNEL_MANAGE_BROADCAST, 88 | OAUTH_SCOPE_CHANNEL_MANAGE_EXTENSION, 89 | OAUTH_SCOPE_CHANNEL_READ_HYPE_TRAIN, 90 | OAUTH_SCOPE_CHANNEL_READ_STREAM_KEY, 91 | OAUTH_SCOPE_CHANNEL_READ_HYPE_SUBSCRIPTIONS, 92 | OAUTH_SCOPE_CLIPS_EDIT, 93 | OAUTH_SCOPE_USER_EDIT_FOLLOWS, 94 | OAUTH_SCOPE_USER_READ_BROADCAST, 95 | OAUTH_SCOPE_USER_READ_EMAIL, 96 | ] 97 | -------------------------------------------------------------------------------- /tests/api/test_games.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | import responses 5 | 6 | from twitch.client import TwitchClient 7 | from twitch.constants import BASE_URL 8 | from twitch.exceptions import TwitchAttributeException 9 | from twitch.resources import Game, TopGame 10 | 11 | example_top_games_response = { 12 | "_total": 1157, 13 | "top": [ 14 | { 15 | "channels": 953, 16 | "viewers": 171708, 17 | "game": { 18 | "_id": 32399, 19 | "box": { 20 | "large": ( 21 | "https://static-cdn.jtvnw.net/ttv-boxart/" 22 | "Counter-Strike:%20Global%20Offensive-272x380.jpg" 23 | ), 24 | "medium": ( 25 | "https://static-cdn.jtvnw.net/ttv-boxart/" 26 | "Counter-Strike:%20Global%20Offensive-136x190.jpg" 27 | ), 28 | "small": ( 29 | "https://static-cdn.jtvnw.net/ttv-boxart/" 30 | "Counter-Strike:%20Global%20Offensive-52x72.jpg" 31 | ), 32 | "template": ( 33 | "https://static-cdn.jtvnw.net/ttv-boxart/Counter-St" 34 | "rike:%20Global%20Offensive-{width}x{height}.jpg" 35 | ), 36 | }, 37 | "giantbomb_id": 36113, 38 | "logo": { 39 | "large": ( 40 | "https://static-cdn.jtvnw.net/ttv-logoart/" 41 | "Counter-Strike:%20Global%20Offensive-240x144.jpg" 42 | ), 43 | "medium": ( 44 | "https://static-cdn.jtvnw.net/ttv-logoart/" 45 | "Counter-Strike:%20Global%20Offensive-120x72.jpg" 46 | ), 47 | "small": ( 48 | "https://static-cdn.jtvnw.net/ttv-logoart/" 49 | "Counter-Strike:%20Global%20Offensive-60x36.jpg" 50 | ), 51 | "template": ( 52 | "https://static-cdn.jtvnw.net/ttv-logoart/Counter-" 53 | "Strike:%20Global%20Offensive-{width}x{height}.jpg" 54 | ), 55 | }, 56 | "name": "Counter-Strike: Global Offensive", 57 | "popularity": 170487, 58 | }, 59 | } 60 | ], 61 | } 62 | 63 | 64 | @responses.activate 65 | def test_get_top(): 66 | responses.add( 67 | responses.GET, 68 | "{}games/top".format(BASE_URL), 69 | body=json.dumps(example_top_games_response), 70 | status=200, 71 | content_type="application/json", 72 | ) 73 | 74 | client = TwitchClient("abcd") 75 | 76 | games = client.games.get_top() 77 | 78 | assert len(responses.calls) == 1 79 | assert len(games) == 1 80 | assert isinstance(games[0], TopGame) 81 | game = games[0].game 82 | assert isinstance(game, Game) 83 | assert game.id == example_top_games_response["top"][0]["game"]["_id"] 84 | 85 | 86 | @responses.activate 87 | @pytest.mark.parametrize("param,value", [("limit", 101)]) 88 | def test_get_top_raises_if_wrong_params_are_passed_in(param, value): 89 | client = TwitchClient("client id") 90 | kwargs = {param: value} 91 | with pytest.raises(TwitchAttributeException): 92 | client.games.get_top(**kwargs) 93 | -------------------------------------------------------------------------------- /docs/v5/collections.rst: -------------------------------------------------------------------------------- 1 | Collections 2 | =========== 3 | 4 | .. currentmodule:: twitch.api.collections 5 | 6 | .. class:: Collections() 7 | 8 | This class provides methods for easy access to `Twitch Collections API`_. 9 | 10 | .. classmethod:: get_metadata(collection_id) 11 | 12 | Gets summary information about a specified collection. 13 | 14 | :param string collection_id: Collection ID 15 | 16 | .. code-block:: python 17 | 18 | >>> from twitch import TwitchClient 19 | >>> client = TwitchClient('') 20 | >>> collection = client.collections.get_metadata('12345') 21 | 22 | 23 | .. classmethod:: get(collection_id, include_all_items) 24 | 25 | Gets all items in a specified collection. 26 | 27 | :param string collection_id: Collection ID 28 | :param boolean include_all_items: If True, unwatchable VODs are included in the response. 29 | Default: false. 30 | 31 | 32 | .. classmethod:: get_by_channel(channel_id, limit, cursor, containig_item) 33 | 34 | Gets all collections owned by a specified channel. 35 | 36 | :param string channel_id: Channel ID 37 | :param int limit: Maximum number of objects to return. Default 10. Maximum 100. 38 | :param string cursor: Cursor of the next page 39 | :param string containig_item: Returns only collections containing the specified video. 40 | `Example: video:89917098.` 41 | 42 | 43 | .. classmethod:: create(channel_id, title) 44 | 45 | Creates a new collection owned by a specified channel. 46 | 47 | :param string channel_id: Channel ID 48 | :param string title: Collection title 49 | 50 | 51 | .. classmethod:: update(collection_id, title) 52 | 53 | Updates the title of a specified collection. 54 | 55 | :param string collection_id: Collection ID 56 | :param string title: Collection title 57 | 58 | 59 | .. classmethod:: create_thumbnail(collection_id, item_id) 60 | 61 | Adds the thumbnail of a specified collection item as the thumbnail for the specified 62 | collection. 63 | 64 | :param string collection_id: Collection ID 65 | :param string item_id: Item ID 66 | 67 | 68 | .. classmethod:: delete(collection_id) 69 | 70 | Deletes a specified collection. 71 | 72 | :param string collection_id: Collection ID 73 | 74 | 75 | .. classmethod:: add_item(collection_id, item_id, item_type) 76 | 77 | Adds a specified item to a specified collection. 78 | 79 | :param string collection_id: Collection ID 80 | :param string item_id: Item ID 81 | :param string item_type: Type of the item. Example: `video`. 82 | 83 | 84 | .. classmethod:: delete_item(collection_id, collection_item_id) 85 | 86 | Deletes a specified collection item from a specified collection. 87 | 88 | :param string collection_id: Collection ID 89 | :param string collection_item_id: Collection Item ID 90 | 91 | 92 | .. classmethod:: move_item(collection_id, collection_item_id, position) 93 | 94 | Deletes a specified collection item from a specified collection. 95 | 96 | :param string collection_id: Collection ID 97 | :param string collection_item_id: Collection Item ID 98 | :param int position: New item position 99 | 100 | 101 | 102 | .. _`Twitch Collections API`: https://dev.twitch.tv/docs/v5/reference/collections/ 103 | -------------------------------------------------------------------------------- /twitch/resources.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | 4 | def convert_to_twitch_object(name, data): 5 | types = { 6 | "channel": Channel, 7 | "videos": Video, 8 | "user": User, 9 | "game": Game, 10 | "stream": Stream, 11 | "comments": Comment, 12 | "owner": User, 13 | } 14 | 15 | special_types = { 16 | "created_at": _DateTime, 17 | "updated_at": _DateTime, 18 | "published_at": _DateTime, 19 | "started_at": _DateTime, 20 | "followed_at": _DateTime, 21 | } 22 | 23 | if isinstance(data, list): 24 | return [convert_to_twitch_object(name, x) for x in data] 25 | 26 | if name in special_types: 27 | obj = special_types.get(name) 28 | return obj.construct_from(data) 29 | 30 | if isinstance(data, dict) and name in types: 31 | obj = types.get(name) 32 | return obj.construct_from(data) 33 | 34 | return data 35 | 36 | 37 | class TwitchObject(dict): 38 | def __setattr__(self, name, value): 39 | if name[0] == "_" or name in self.__dict__: 40 | return super(TwitchObject, self).__setattr__(name, value) 41 | 42 | self[name] = value 43 | 44 | def __getattr__(self, name): 45 | return self[name] 46 | 47 | def __delattr__(self, name): 48 | if name[0] == "_": 49 | return super(TwitchObject, self).__delattr__(name) 50 | 51 | del self[name] 52 | 53 | def __setitem__(self, key, value): 54 | key = key.lstrip("_") 55 | super(TwitchObject, self).__setitem__(key, value) 56 | 57 | @classmethod 58 | def construct_from(cls, values): 59 | instance = cls() 60 | instance.refresh_from(values) 61 | return instance 62 | 63 | def refresh_from(self, values): 64 | for key, value in values.copy().items(): 65 | self.__setitem__(key, convert_to_twitch_object(key, value)) 66 | 67 | 68 | class _DateTime(object): 69 | @classmethod 70 | def construct_from(cls, value): 71 | if value is None: 72 | return None 73 | try: 74 | dt = datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ") 75 | except ValueError: 76 | dt = datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%fZ") 77 | 78 | return dt 79 | 80 | 81 | class Channel(TwitchObject): 82 | pass 83 | 84 | 85 | class Clip(TwitchObject): 86 | pass 87 | 88 | 89 | class Collection(TwitchObject): 90 | pass 91 | 92 | 93 | class Comment(TwitchObject): 94 | pass 95 | 96 | 97 | class Community(TwitchObject): 98 | pass 99 | 100 | 101 | class Featured(TwitchObject): 102 | pass 103 | 104 | 105 | class Follow(TwitchObject): 106 | pass 107 | 108 | 109 | class Game(TwitchObject): 110 | pass 111 | 112 | 113 | class Ingest(TwitchObject): 114 | pass 115 | 116 | 117 | class Item(TwitchObject): 118 | pass 119 | 120 | 121 | class Post(TwitchObject): 122 | pass 123 | 124 | 125 | class Stream(TwitchObject): 126 | pass 127 | 128 | 129 | class StreamMetadata(TwitchObject): 130 | pass 131 | 132 | 133 | class Subscription(TwitchObject): 134 | pass 135 | 136 | 137 | class Tag(TwitchObject): 138 | pass 139 | 140 | 141 | class Team(TwitchObject): 142 | pass 143 | 144 | 145 | class TopGame(TwitchObject): 146 | pass 147 | 148 | 149 | class User(TwitchObject): 150 | pass 151 | 152 | 153 | class UserBlock(TwitchObject): 154 | pass 155 | 156 | 157 | class Video(TwitchObject): 158 | pass 159 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Master 4 | 5 | - Added TwitchHelix.get_tags() for fetching all tags 6 | - Added logging to TwitchAPIMixing to be able to see what's going on 7 | 8 | ## Version 0.7.1 - 2020-12-04 9 | 10 | - Fixed 0.7.0 release to include submodules 11 | 12 | 13 | ## Version 0.7.0 - 2020-12-04 14 | 15 | - Moved test requirements to setup.py 16 | - Updated dependencies to newer versions 17 | - Removed support for Python 2, Python 3.4 and Python 3.5 18 | - Added support for Python 3.7 and 3.8 19 | - Added new code formatting called black 20 | - Changed import orders to now be formatted via isort 21 | - Added TwitchHelix.get_oauth() for fetching OAuth access token 22 | - Updated docs 23 | - Replaced Travis CI with GitHub Actions 24 | 25 | 26 | ## Version 0.6.0 - 2018-09-10 27 | 28 | - Added default request timeout to all requests for API v5 29 | - Removed support for Python 3.3 30 | - Added TwitchHelix class which adds support for the new Twitch Helix API. 31 | - Added Twitch Helix API support for the following endpoints: 32 | * Get Clips `twitch.helix.api.TwitchHelix.get_clips` 33 | * Get Games `twitch.helix.api.TwitchHelix.get_games` 34 | * Get Streams `twitch.helix.api.TwitchHelix.get_streams` 35 | * Get Streams Metadata `twitch.helix.api.TwitchHelix.get_streams_metadata` 36 | * Get Top Games `twitch.helix.api.TwitchHelix.get_top_games` 37 | * Get User Follows `twitch.helix.api.TwitchHelix.get_user_follows` 38 | * Get Videos `twitch.helix.api.TwitchHelix.get_videos` 39 | - Changed how config file is read and moved the corresponding code to twitch/conf.py 40 | - Changed old string formatting to the new one 41 | 42 | 43 | ## Version 0.5.1 - 2018-03-03 44 | 45 | - Fixed `configparser` import for Python<3.2 46 | 47 | 48 | ## Version 0.5.0 - 2018-02-06 49 | 50 | - Added VOD download support 51 | 52 | 53 | ## Version 0.4.0 - 2017-11-17 54 | 55 | - Added clips endpoints 56 | 57 | 58 | ## Version 0.3.1 - 2017-10-26 59 | 60 | - Fixed Twitch.channels.get_subscribers function to obey filters 61 | - Added support for `~/.twitch.cfg` file for storing credentials 62 | - Added retry logic for all >=500 requests with a backoff functionality 63 | - Added discord server to the docs 64 | - Bumped up requirements: six from 1.10.0 to 1.11.0 and requests from 2.18.1 to 2.18.4 65 | 66 | 67 | ## Version 0.3.0 - 2017-06-01 68 | 69 | - Fixed all post and put methods to pass data in json format to Twitch API rather than in form format 70 | - Fixed Channels.update() method to correctly pass data to Twitch 71 | - Fixed Channels.get() method to call 'channel' endpoint instead of 'channels' 72 | - Fixed a bad implementation of _DateTime resource 73 | - Added Streams.get_streams_in_community endpoint 74 | - Added support for python 2.7 75 | - Added six as a requirement 76 | 77 | 78 | ## Version 0.2.1 - 2017-5-2 79 | 80 | - Fixed Streams.get_stream_by_user which raised exception if stream was offline. Now returns None 81 | if stream is offline. 82 | 83 | 84 | ## Version 0.2.0 - 2017-3-10 85 | 86 | - Added pypi image to README.md 87 | - Added CHANGELOG.md 88 | - Added docs for `twitch.api.users`, `twitch.api.chat`, `twitch.api.communities`, 89 | `twitch.api.games`, `twitch.api.ingests`, `twitch.api.streams`, `twitch.api.teams`, 90 | `twitch.api.videos` 91 | - Added Travis-CI 92 | - Added Codecov 93 | - Added a LOT of tests 94 | - Added the rest of community endpoints 95 | - Added loads of missing tests 96 | - Added search endpoints 97 | - Added channel feed endpoints 98 | - Added collections endpoints 99 | - Changed some function names for channels (will break stuff if you're using version 0.1.0 already) 100 | - Fixed output from streams 101 | - Fixed passing parameters to video endpoints 102 | - Introduced TwitchException, TwitchAuthException and TwitchAttributeException 103 | - `created_at` fields are now converted to datetime objects 104 | - Removed 'create' method on communities endpoint 105 | 106 | 107 | ## Version 0.1.0 - 2017-2-25 108 | 109 | Initial release 110 | -------------------------------------------------------------------------------- /docs/v5/users.rst: -------------------------------------------------------------------------------- 1 | Users 2 | ===== 3 | 4 | .. currentmodule:: twitch.api.users 5 | 6 | .. class:: Users() 7 | 8 | This class provides methods for easy access to `Twitch Users API`_. 9 | 10 | .. classmethod:: get() 11 | 12 | Gets a user object based on the OAuth token provided. 13 | 14 | 15 | .. classmethod:: get_by_id(user_id) 16 | 17 | Gets a user object based on specified user id. 18 | 19 | :param 'string user_id: User ID 20 | 21 | 22 | .. classmethod:: get_emotes(user_id) 23 | 24 | Gets a list of the emojis and emoticons that the specified user can use in chat 25 | 26 | :param 'string user_id: User ID 27 | 28 | 29 | 30 | .. classmethod:: check_subscribed_to_channel(user_id, channel_id) 31 | 32 | Checks if a specified user is subscribed to a specified channel. 33 | 34 | :param 'string user_id: User ID 35 | :param 'string channel_id: ID of the channel you want to check if user is 36 | subscribed to 37 | 38 | 39 | .. classmethod:: get_follows(user_id, limit, offset, direction, sort_by) 40 | 41 | Gets a list of all channels followed by a specified user. 42 | 43 | :param 'string user_id: User ID 44 | :param int limit: Maximum number of objects to return. Default 25. Maximum 100. 45 | :param int offset: Object offset for pagination of result. Default 0. 46 | :param string direction: Sorting direction. Default DIRECTION_DESC. 47 | :param string sort_by: Sorting key. Default USERS_SORT_BY_CREATED_AT. 48 | 49 | 50 | .. classmethod:: check_follows_channel(user_id, channel_id) 51 | 52 | Checks if a specified user follows a specified channel. 53 | 54 | :param 'string user_id: User ID 55 | :param 'string channel_id: ID of the channel you want to check if user is following 56 | 57 | 58 | .. classmethod:: follow_channel(user_id, channel_id, notifications) 59 | 60 | Adds a specified user to the followers of a specified channel. 61 | 62 | :param 'string user_id: User ID 63 | :param 'string channel_id: ID of the channel you want user to follow 64 | :param boolean notifications: If true, the user gets email or push notifications when the 65 | channel goes live. Default False. 66 | 67 | 68 | .. classmethod:: unfollow_channel(user_id, channel_id) 69 | 70 | Deletes a specified user from the followers of a specified channel. 71 | 72 | :param 'string user_id: User ID 73 | :param 'string channel_id: ID of the channel you want user to unfollow 74 | 75 | 76 | .. classmethod:: get_user_block_list(user_id, limit, offset) 77 | 78 | Gets a user’s block list. 79 | 80 | :param 'string user_id: User ID 81 | :param int limit: Maximum number of objects to return. Default 25. Maximum 100. 82 | :param int offset: Object offset for pagination of result. Default 0. 83 | 84 | 85 | .. classmethod:: block_user(user_id, blocked_user_id) 86 | 87 | Blocks a user. 88 | 89 | :param 'string user_id: User ID 90 | :param 'string blocked_user_id: ID of the user you wish to block 91 | 92 | 93 | .. classmethod:: unblock_user(user_id, blocked_user_id) 94 | 95 | Unblocks a user. 96 | 97 | :param 'string user_id: User ID 98 | :param 'string blocked_user_id: ID of the user you wish to unblock 99 | 100 | 101 | .. classmethod:: translate_usernames_to_ids(usernames) 102 | 103 | Translates a list of usernames to user ID's. 104 | 105 | :param list[string] usernames: List of usernames you wish to get ID's of 106 | 107 | 108 | .. code-block:: python 109 | 110 | >>> from twitch import TwitchClient 111 | >>> client = TwitchClient('') 112 | >>> users = client.users.translate_usernames_to_ids(['lirik', 'giantwaffle']) 113 | >>> 114 | >>> for user in users: 115 | >>> print('{}: {}'.format(user.name, user.id)) 116 | 'lirik: 23161357' 117 | 'giantwaffle: 22552479' 118 | 119 | 120 | 121 | .. _`Twitch Users API`: https://dev.twitch.tv/docs/v5/reference/users/ 122 | -------------------------------------------------------------------------------- /twitch/client.py: -------------------------------------------------------------------------------- 1 | from .api import ( 2 | ChannelFeed, 3 | Channels, 4 | Chat, 5 | Clips, 6 | Collections, 7 | Communities, 8 | Games, 9 | Ingests, 10 | Search, 11 | Streams, 12 | Teams, 13 | Users, 14 | Videos, 15 | ) 16 | from .conf import credentials_from_config_file 17 | 18 | 19 | class TwitchClient(object): 20 | """ 21 | Twitch API v5 [kraken] 22 | """ 23 | 24 | def __init__(self, client_id=None, oauth_token=None): 25 | self._client_id = client_id 26 | self._oauth_token = oauth_token 27 | 28 | if not client_id: 29 | self._client_id, self._oauth_token = credentials_from_config_file() 30 | 31 | self._clips = None 32 | self._channel_feed = None 33 | self._channels = None 34 | self._chat = None 35 | self._collections = None 36 | self._communities = None 37 | self._games = None 38 | self._ingests = None 39 | self._search = None 40 | self._streams = None 41 | self._teams = None 42 | self._users = None 43 | self._videos = None 44 | 45 | @property 46 | def channel_feed(self): 47 | if not self._channel_feed: 48 | self._channel_feed = ChannelFeed( 49 | client_id=self._client_id, oauth_token=self._oauth_token 50 | ) 51 | return self._channel_feed 52 | 53 | @property 54 | def clips(self): 55 | if not self._clips: 56 | self._clips = Clips( 57 | client_id=self._client_id, oauth_token=self._oauth_token 58 | ) 59 | return self._clips 60 | 61 | @property 62 | def channels(self): 63 | if not self._channels: 64 | self._channels = Channels( 65 | client_id=self._client_id, oauth_token=self._oauth_token 66 | ) 67 | return self._channels 68 | 69 | @property 70 | def chat(self): 71 | if not self._chat: 72 | self._chat = Chat(client_id=self._client_id, oauth_token=self._oauth_token) 73 | return self._chat 74 | 75 | @property 76 | def collections(self): 77 | if not self._collections: 78 | self._collections = Collections( 79 | client_id=self._client_id, oauth_token=self._oauth_token 80 | ) 81 | return self._collections 82 | 83 | @property 84 | def communities(self): 85 | if not self._communities: 86 | self._communities = Communities( 87 | client_id=self._client_id, oauth_token=self._oauth_token 88 | ) 89 | return self._communities 90 | 91 | @property 92 | def games(self): 93 | if not self._games: 94 | self._games = Games( 95 | client_id=self._client_id, oauth_token=self._oauth_token 96 | ) 97 | return self._games 98 | 99 | @property 100 | def ingests(self): 101 | if not self._ingests: 102 | self._ingests = Ingests( 103 | client_id=self._client_id, oauth_token=self._oauth_token 104 | ) 105 | return self._ingests 106 | 107 | @property 108 | def search(self): 109 | if not self._search: 110 | self._search = Search( 111 | client_id=self._client_id, oauth_token=self._oauth_token 112 | ) 113 | return self._search 114 | 115 | @property 116 | def streams(self): 117 | if not self._streams: 118 | self._streams = Streams( 119 | client_id=self._client_id, oauth_token=self._oauth_token 120 | ) 121 | return self._streams 122 | 123 | @property 124 | def teams(self): 125 | if not self._teams: 126 | self._teams = Teams( 127 | client_id=self._client_id, oauth_token=self._oauth_token 128 | ) 129 | return self._teams 130 | 131 | @property 132 | def users(self): 133 | if not self._users: 134 | self._users = Users( 135 | client_id=self._client_id, oauth_token=self._oauth_token 136 | ) 137 | return self._users 138 | 139 | @property 140 | def videos(self): 141 | if not self._videos: 142 | self._videos = Videos( 143 | client_id=self._client_id, oauth_token=self._oauth_token 144 | ) 145 | return self._videos 146 | -------------------------------------------------------------------------------- /docs/v5/channel_feed.rst: -------------------------------------------------------------------------------- 1 | Channel feed 2 | ============ 3 | 4 | .. currentmodule:: twitch.api.channel_feed 5 | 6 | .. class:: ChannelFeed() 7 | 8 | This class provides methods for easy access to `Twitch Channel Feed API`_. 9 | 10 | .. classmethod:: get_posts(channel_id, limit, cursor, comments) 11 | 12 | Gets posts from a specified channel feed. 13 | 14 | :param string channel_id: Channel ID 15 | :param int limit: Maximum number of objects to return. Default 10. Maximum 100. 16 | :param string cursor: Cursor of the next page 17 | :param int comments: Number of comments to return. Default 5. Maximum 5. 18 | 19 | 20 | .. classmethod:: get_post(channel_id, post_id, comments) 21 | 22 | Gets a specified post from a specified channel feed. 23 | 24 | :param string channel_id: Channel ID 25 | :param string post_id: Post ID 26 | :param int comments: Number of comments to return. Default 5. Maximum 5. 27 | 28 | .. code-block:: python 29 | 30 | >>> from twitch import TwitchClient 31 | >>> client = TwitchClient('') 32 | >>> post = client.channel_feed.get_post('12345', '12345', comments=0) 33 | 34 | 35 | .. classmethod:: create_post(channel_id, content, share) 36 | 37 | Creates a post in a specified channel feed. 38 | 39 | :param string channel_id: Channel ID 40 | :param string content: Content of the post 41 | :param boolean share: When set to true, the post is shared on the channel’s Twitter feed. 42 | 43 | 44 | .. classmethod:: delete_post(channel_id, post_id) 45 | 46 | Deletes a specified post in a specified channel feed. 47 | 48 | :param string channel_id: Channel ID 49 | :param string post_id: Post ID 50 | 51 | 52 | .. classmethod:: create_reaction_to_post(channel_id, post_id, emote_id) 53 | 54 | Creates a reaction to a specified post in a specified channel feed. 55 | 56 | :param string channel_id: Channel ID 57 | :param string post_id: Post ID 58 | :param string emote_id: Emote ID 59 | 60 | 61 | .. classmethod:: delete_reaction_to_post(channel_id, post_id, emote_id) 62 | 63 | Deletes a specified reaction to a specified post in a specified channel feed. 64 | 65 | :param string channel_id: Channel ID 66 | :param string post_id: Post ID 67 | :param string emote_id: Emote ID 68 | 69 | 70 | .. classmethod:: get_post_comments(channel_id, post_id, limit, cursor) 71 | 72 | Gets all comments on a specified post in a specified channel feed. 73 | 74 | :param string channel_id: Channel ID 75 | :param string post_id: Post ID 76 | :param int limit: Maximum number of objects to return. Default 10. Maximum 100. 77 | :param string cursor: Cursor of the next page 78 | 79 | 80 | .. classmethod:: create_post_comment(channel_id, post_id, content) 81 | 82 | Creates a comment to a specified post in a specified channel feed. 83 | 84 | :param string channel_id: Channel ID 85 | :param string post_id: Post ID 86 | :param string content: Content of the comment 87 | 88 | 89 | .. classmethod:: delete_post_comment(channel_id, post_id, comment_id) 90 | 91 | Deletes a specified comment on a specified post in a specified channel feed. 92 | 93 | :param string channel_id: Channel ID 94 | :param string post_id: Post ID 95 | :param string comment_id: Comment ID 96 | 97 | 98 | .. classmethod:: create_reaction_to_comment(channel_id, post_id, comment_id, emote_id) 99 | 100 | Creates a reaction to a specified comment on a specified post in a specified channel feed. 101 | 102 | :param string channel_id: Channel ID 103 | :param string post_id: Post ID 104 | :param string comment_id: Comment ID 105 | :param string emote_id: Emote ID 106 | 107 | 108 | .. classmethod:: delete_reaction_to_comment(channel_id, post_id, comment_id, emote_id) 109 | 110 | Deletes a reaction to a specified comment on a specified post in a specified channel feed. 111 | 112 | :param string channel_id: Channel ID 113 | :param string post_id: Post ID 114 | :param string comment_id: Comment ID 115 | :param string emote_id: Emote ID 116 | 117 | 118 | .. _`Twitch Channel Feed API`: https://dev.twitch.tv/docs/v5/reference/channel-feed/ 119 | -------------------------------------------------------------------------------- /twitch/api/channel_feed.py: -------------------------------------------------------------------------------- 1 | from twitch.api.base import TwitchAPI 2 | from twitch.decorators import oauth_required 3 | from twitch.exceptions import TwitchAttributeException 4 | from twitch.resources import Comment, Post 5 | 6 | 7 | class ChannelFeed(TwitchAPI): 8 | def get_posts(self, channel_id, limit=10, cursor=None, comments=5): 9 | if limit > 100: 10 | raise TwitchAttributeException( 11 | "Maximum number of objects returned in one request is 100" 12 | ) 13 | if comments > 5: 14 | raise TwitchAttributeException( 15 | "Maximum number of comments returned in one request is 5" 16 | ) 17 | 18 | params = {"limit": limit, "cursor": cursor, "comments": comments} 19 | response = self._request_get("feed/{}/posts".format(channel_id), params=params) 20 | return [Post.construct_from(x) for x in response["posts"]] 21 | 22 | def get_post(self, channel_id, post_id, comments=5): 23 | if comments > 5: 24 | raise TwitchAttributeException( 25 | "Maximum number of comments returned in one request is 5" 26 | ) 27 | 28 | params = {"comments": comments} 29 | response = self._request_get( 30 | "feed/{}/posts/{}".format(channel_id, post_id), params=params 31 | ) 32 | return Post.construct_from(response) 33 | 34 | @oauth_required 35 | def create_post(self, channel_id, content, share=None): 36 | data = {"content": content} 37 | params = {"share": share} 38 | response = self._request_post( 39 | "feed/{}/posts".format(channel_id), data, params=params 40 | ) 41 | return Post.construct_from(response["post"]) 42 | 43 | @oauth_required 44 | def delete_post(self, channel_id, post_id): 45 | response = self._request_delete("feed/{}/posts/{}".format(channel_id, post_id)) 46 | return Post.construct_from(response) 47 | 48 | @oauth_required 49 | def create_reaction_to_post(self, channel_id, post_id, emote_id): 50 | params = {"emote_id": emote_id} 51 | url = "feed/{}/posts/{}/reactions".format(channel_id, post_id) 52 | response = self._request_post(url, params=params) 53 | return response 54 | 55 | @oauth_required 56 | def delete_reaction_to_post(self, channel_id, post_id, emote_id): 57 | params = {"emote_id": emote_id} 58 | url = "feed/{}/posts/{}/reactions".format(channel_id, post_id) 59 | response = self._request_delete(url, params=params) 60 | return response 61 | 62 | def get_post_comments(self, channel_id, post_id, limit=10, cursor=None): 63 | if limit > 100: 64 | raise TwitchAttributeException( 65 | "Maximum number of objects returned in one request is 100" 66 | ) 67 | 68 | params = { 69 | "limit": limit, 70 | "cursor": cursor, 71 | } 72 | url = "feed/{}/posts/{}/comments".format(channel_id, post_id) 73 | response = self._request_get(url, params=params) 74 | return [Comment.construct_from(x) for x in response["comments"]] 75 | 76 | @oauth_required 77 | def create_post_comment(self, channel_id, post_id, content): 78 | data = {"content": content} 79 | url = "feed/{}/posts/{}/comments".format(channel_id, post_id) 80 | response = self._request_post(url, data) 81 | return Comment.construct_from(response) 82 | 83 | @oauth_required 84 | def delete_post_comment(self, channel_id, post_id, comment_id): 85 | url = "feed/{}/posts/{}/comments/{}".format(channel_id, post_id, comment_id) 86 | response = self._request_delete(url) 87 | return Comment.construct_from(response) 88 | 89 | @oauth_required 90 | def create_reaction_to_comment(self, channel_id, post_id, comment_id, emote_id): 91 | params = {"emote_id": emote_id} 92 | url = "feed/{}/posts/{}/comments/{}/reactions".format( 93 | channel_id, post_id, comment_id 94 | ) 95 | response = self._request_post(url, params=params) 96 | return response 97 | 98 | @oauth_required 99 | def delete_reaction_to_comment(self, channel_id, post_id, comment_id, emote_id): 100 | params = {"emote_id": emote_id} 101 | url = "feed/{}/posts/{}/comments/{}/reactions".format( 102 | channel_id, post_id, comment_id 103 | ) 104 | response = self._request_delete(url, params=params) 105 | return response 106 | -------------------------------------------------------------------------------- /tests/api/test_videos.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | import responses 5 | 6 | from twitch.client import TwitchClient 7 | from twitch.constants import BASE_URL, VOD_FETCH_URL 8 | from twitch.exceptions import TwitchAttributeException 9 | from twitch.resources import Video 10 | 11 | example_video_response = { 12 | "_id": "v106400740", 13 | "description": "Protect your chat with AutoMod!", 14 | "fps": {"1080p": 23.9767661758746}, 15 | } 16 | 17 | example_top_response = {"vods": [example_video_response]} 18 | 19 | example_followed_response = {"videos": [example_video_response]} 20 | 21 | example_download_vod_token_response = {"sig": "sig", "token": "token"} 22 | 23 | 24 | @responses.activate 25 | def test_get_by_id(): 26 | video_id = "v106400740" 27 | responses.add( 28 | responses.GET, 29 | "{}videos/{}".format(BASE_URL, video_id), 30 | body=json.dumps(example_video_response), 31 | status=200, 32 | content_type="application/json", 33 | ) 34 | 35 | client = TwitchClient("client id") 36 | 37 | video = client.videos.get_by_id(video_id) 38 | 39 | assert len(responses.calls) == 1 40 | assert isinstance(video, Video) 41 | assert video.id == example_video_response["_id"] 42 | assert video.description == example_video_response["description"] 43 | assert video.fps["1080p"] == example_video_response["fps"]["1080p"] 44 | 45 | 46 | @responses.activate 47 | def test_get_top(): 48 | responses.add( 49 | responses.GET, 50 | "{}videos/top".format(BASE_URL), 51 | body=json.dumps(example_top_response), 52 | status=200, 53 | content_type="application/json", 54 | ) 55 | 56 | client = TwitchClient("client id") 57 | 58 | videos = client.videos.get_top() 59 | 60 | assert len(responses.calls) == 1 61 | assert len(videos) == 1 62 | assert isinstance(videos[0], Video) 63 | video = videos[0] 64 | assert isinstance(video, Video) 65 | assert video.id == example_video_response["_id"] 66 | assert video.description == example_video_response["description"] 67 | assert video.fps["1080p"] == example_video_response["fps"]["1080p"] 68 | 69 | 70 | @responses.activate 71 | @pytest.mark.parametrize( 72 | "param,value", [("limit", 101), ("period", "abcd"), ("broadcast_type", "abcd")] 73 | ) 74 | def test_get_top_raises_if_wrong_params_are_passed_in(param, value): 75 | client = TwitchClient("client id") 76 | kwargs = {param: value} 77 | with pytest.raises(TwitchAttributeException): 78 | client.videos.get_top(**kwargs) 79 | 80 | 81 | @responses.activate 82 | def test_get_followed_videos(): 83 | responses.add( 84 | responses.GET, 85 | "{}videos/followed".format(BASE_URL), 86 | body=json.dumps(example_followed_response), 87 | status=200, 88 | content_type="application/json", 89 | ) 90 | 91 | client = TwitchClient("client id", "oauth token") 92 | 93 | videos = client.videos.get_followed_videos() 94 | 95 | assert len(responses.calls) == 1 96 | assert len(videos) == 1 97 | assert isinstance(videos[0], Video) 98 | video = videos[0] 99 | assert isinstance(video, Video) 100 | assert video.id == example_video_response["_id"] 101 | assert video.description == example_video_response["description"] 102 | assert video.fps["1080p"] == example_video_response["fps"]["1080p"] 103 | 104 | 105 | @responses.activate 106 | @pytest.mark.parametrize("param,value", [("limit", 101), ("broadcast_type", "abcd")]) 107 | def test_get_followed_videos_raises_if_wrong_params_are_passed_in(param, value): 108 | client = TwitchClient("client id", "oauth token") 109 | kwargs = {param: value} 110 | with pytest.raises(TwitchAttributeException): 111 | client.videos.get_followed_videos(**kwargs) 112 | 113 | 114 | @responses.activate 115 | def test_download_vod(): 116 | video_id = "v106400740" 117 | vod_id = "106400740" 118 | responses.add( 119 | responses.GET, 120 | "{}vods/{}/access_token".format("https://api.twitch.tv/api/", vod_id), 121 | body=json.dumps(example_download_vod_token_response), 122 | status=200, 123 | content_type="application/json", 124 | ) 125 | responses.add( 126 | responses.GET, 127 | "{}vod/{}".format(VOD_FETCH_URL, vod_id), 128 | body=b"", 129 | status=200, 130 | content_type="application/x-mpegURL", 131 | ) 132 | client = TwitchClient("client id") 133 | vod = client.videos.download_vod(video_id) 134 | 135 | assert len(responses.calls) == 2 136 | assert vod == b"" 137 | -------------------------------------------------------------------------------- /docs/v5/channels.rst: -------------------------------------------------------------------------------- 1 | Channels 2 | ======== 3 | 4 | .. currentmodule:: twitch.api.channels 5 | 6 | .. class:: Channels() 7 | 8 | This class provides methods for easy access to `Twitch Channels API`_. 9 | 10 | .. classmethod:: get() 11 | 12 | Gets a channel object based on the OAuth token. 13 | 14 | .. code-block:: python 15 | 16 | >>> from twitch import TwitchClient 17 | >>> client = TwitchClient('', '') 18 | >>> channel = client.channels.get() 19 | 20 | 21 | .. classmethod:: get_by_id(channel_id) 22 | 23 | Gets a specified channel object. 24 | 25 | :param string channel_id: Channel ID 26 | 27 | 28 | .. classmethod:: update(channel_id, status, game, delay, channel_feed_enabled) 29 | 30 | Updates specified properties of a specified channel. 31 | 32 | :param string channel_id: Channel ID 33 | :param string status: Description of the broadcaster’s status. 34 | :param string game: Name of game. 35 | :param int delay: Channel delay, in seconds. 36 | :param boolean channel_feed_enabled: If true, the channel’s feed is turned on. 37 | 38 | 39 | .. classmethod:: get_editors(channel_id) 40 | 41 | Gets a list of users who are editors for a specified channel. 42 | 43 | :param string channel_id: Channel ID 44 | 45 | 46 | .. classmethod:: get_followers(channel_id, limit, offset, cursor, direction) 47 | 48 | Gets a list of users who follow a specified channel. 49 | 50 | :param string channel_id: Channel ID 51 | :param int limit: Maximum number of objects to return. Default 25. Maximum 100. 52 | :param int offset: Object offset for pagination of result. Default 0. 53 | :param string cursor: Cursor of the next page. 54 | :param string direction: Direction of sorting. 55 | 56 | 57 | .. classmethod:: get_teams(channel_id) 58 | 59 | Gets a list of teams to which a specified channel belongs. 60 | 61 | :param string channel_id: Channel ID 62 | 63 | 64 | .. classmethod:: get_subscribers(channel_id, limit, offset, direction) 65 | 66 | Gets a list of users subscribed to a specified channel. 67 | 68 | :param string channel_id: Channel ID 69 | :param int limit: Maximum number of objects to return. Default 25. Maximum 100. 70 | :param int offset: Object offset for pagination of result. Default 0. 71 | :param string direction: Direction of sorting. 72 | 73 | 74 | .. classmethod:: check_subscription_by_user(channel_id, user_id) 75 | 76 | Checks if a specified channel has a specified user subscribed to it. 77 | 78 | :param string channel_id: Channel ID 79 | :param string user_id: User ID 80 | 81 | 82 | .. classmethod:: get_videos(channel_id, limit, offset, broadcast_type, language, sort) 83 | 84 | Gets a list of videos from a specified channel. 85 | 86 | :param string channel_id: Channel ID 87 | :param int limit: Maximum number of objects to return. Default 10. Maximum 100. 88 | :param int offset: Object offset for pagination of result. Default 0. 89 | :param string broadcast_type: Constrains the type of videos returned. 90 | :param string language: Constrains the language of the videos that are returned. 91 | :param string sort: Sorting order of the returned objects. 92 | 93 | 94 | .. classmethod:: start_commercial(channel_id, duration) 95 | 96 | Starts a commercial (advertisement) on a specified channel. 97 | 98 | :param string channel_id: Channel ID 99 | :param string duration: Duration of the commercial in seconds. Default 30. 100 | 101 | 102 | .. classmethod:: reset_stream_key(channel_id) 103 | 104 | Deletes the stream key for a specified channel. Stream key is automatically reset. 105 | 106 | :param string channel_id: Channel ID 107 | 108 | 109 | .. classmethod:: get_community(channel_id) 110 | 111 | Gets the community for a specified channel. 112 | 113 | :param string channel_id: Channel ID 114 | 115 | 116 | .. classmethod:: set_community(channel_id, community_id) 117 | 118 | Sets a specified channel to be in a specified community. 119 | 120 | :param string channel_id: Channel ID 121 | :param string community_id: Community ID 122 | 123 | 124 | .. classmethod:: delete_from_community(channel_id) 125 | 126 | Deletes a specified channel from its community. 127 | 128 | :param string channel_id: Channel ID 129 | 130 | 131 | 132 | .. _`Twitch Channels API`: https://dev.twitch.tv/docs/v5/reference/channels/ 133 | -------------------------------------------------------------------------------- /tests/api/test_search.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | import responses 5 | 6 | from twitch.client import TwitchClient 7 | from twitch.constants import BASE_URL 8 | from twitch.exceptions import TwitchAttributeException 9 | from twitch.resources import Channel, Game, Stream 10 | 11 | example_channel = { 12 | "_id": 44322889, 13 | "name": "dallas", 14 | } 15 | 16 | example_game = { 17 | "_id": 490422, 18 | "name": "StarCraft II", 19 | } 20 | 21 | example_stream = { 22 | "_id": 23932774784, 23 | "game": "BATMAN - The Telltale Series", 24 | "channel": example_channel, 25 | } 26 | 27 | 28 | @responses.activate 29 | def test_channels(): 30 | response = {"_total": 2147, "channels": [example_channel]} 31 | responses.add( 32 | responses.GET, 33 | "{}search/channels".format(BASE_URL), 34 | body=json.dumps(response), 35 | status=200, 36 | content_type="application/json", 37 | ) 38 | 39 | client = TwitchClient("client id") 40 | 41 | channels = client.search.channels("mah query") 42 | 43 | assert len(responses.calls) == 1 44 | assert len(channels) == 1 45 | channel = channels[0] 46 | assert isinstance(channel, Channel) 47 | assert channel.id == example_channel["_id"] 48 | assert channel.name == example_channel["name"] 49 | 50 | 51 | @responses.activate 52 | @pytest.mark.parametrize("param,value", [("limit", 101)]) 53 | def test_channels_raises_if_wrong_params_are_passed_in(param, value): 54 | client = TwitchClient("client id") 55 | kwargs = {param: value} 56 | with pytest.raises(TwitchAttributeException): 57 | client.search.channels("mah query", **kwargs) 58 | 59 | 60 | @responses.activate 61 | def test_channels_does_not_raise_if_no_channels_were_found(): 62 | response = {"channels": None} 63 | responses.add( 64 | responses.GET, 65 | "{}search/channels".format(BASE_URL), 66 | body=json.dumps(response), 67 | status=200, 68 | content_type="application/json", 69 | ) 70 | 71 | client = TwitchClient("client id") 72 | 73 | channels = client.search.channels("mah bad query") 74 | 75 | assert len(responses.calls) == 1 76 | assert len(channels) == 0 77 | 78 | 79 | @responses.activate 80 | def test_games(): 81 | response = {"_total": 2147, "games": [example_game]} 82 | responses.add( 83 | responses.GET, 84 | "{}search/games".format(BASE_URL), 85 | body=json.dumps(response), 86 | status=200, 87 | content_type="application/json", 88 | ) 89 | 90 | client = TwitchClient("client id") 91 | 92 | games = client.search.games("mah query") 93 | 94 | assert len(responses.calls) == 1 95 | assert len(games) == 1 96 | game = games[0] 97 | assert isinstance(game, Game) 98 | assert game.id == example_game["_id"] 99 | assert game.name == example_game["name"] 100 | 101 | 102 | @responses.activate 103 | def test_games_does_not_raise_if_no_games_were_found(): 104 | response = {"games": None} 105 | responses.add( 106 | responses.GET, 107 | "{}search/games".format(BASE_URL), 108 | body=json.dumps(response), 109 | status=200, 110 | content_type="application/json", 111 | ) 112 | 113 | client = TwitchClient("client id") 114 | 115 | games = client.search.games("mah bad query") 116 | 117 | assert len(responses.calls) == 1 118 | assert len(games) == 0 119 | 120 | 121 | @responses.activate 122 | def test_streams(): 123 | response = {"_total": 2147, "streams": [example_stream]} 124 | responses.add( 125 | responses.GET, 126 | "{}search/streams".format(BASE_URL), 127 | body=json.dumps(response), 128 | status=200, 129 | content_type="application/json", 130 | ) 131 | 132 | client = TwitchClient("client id") 133 | 134 | streams = client.search.streams("mah query") 135 | 136 | assert len(responses.calls) == 1 137 | assert len(streams) == 1 138 | stream = streams[0] 139 | assert isinstance(stream, Stream) 140 | assert stream.id == example_stream["_id"] 141 | assert stream.game == example_stream["game"] 142 | 143 | assert isinstance(stream.channel, Channel) 144 | assert stream.channel.id == example_channel["_id"] 145 | assert stream.channel.name == example_channel["name"] 146 | 147 | 148 | @responses.activate 149 | @pytest.mark.parametrize("param,value", [("limit", 101)]) 150 | def test_streams_raises_if_wrong_params_are_passed_in(param, value): 151 | client = TwitchClient("client id") 152 | kwargs = {param: value} 153 | with pytest.raises(TwitchAttributeException): 154 | client.search.streams("mah query", **kwargs) 155 | 156 | 157 | @responses.activate 158 | def test_streams_does_not_raise_if_no_streams_were_found(): 159 | response = {"streams": None} 160 | responses.add( 161 | responses.GET, 162 | "{}search/streams".format(BASE_URL), 163 | body=json.dumps(response), 164 | status=200, 165 | content_type="application/json", 166 | ) 167 | 168 | client = TwitchClient("client id") 169 | 170 | streams = client.search.streams("mah bad query") 171 | 172 | assert len(responses.calls) == 1 173 | assert len(streams) == 0 174 | -------------------------------------------------------------------------------- /twitch/api/users.py: -------------------------------------------------------------------------------- 1 | from twitch.api.base import TwitchAPI 2 | from twitch.constants import ( 3 | DIRECTION_DESC, 4 | DIRECTIONS, 5 | MAX_FOLLOWS_LIMIT, 6 | USERS_SORT_BY, 7 | USERS_SORT_BY_CREATED_AT, 8 | ) 9 | from twitch.decorators import oauth_required 10 | from twitch.exceptions import TwitchAttributeException 11 | from twitch.resources import Follow, Subscription, User, UserBlock 12 | 13 | 14 | class Users(TwitchAPI): 15 | @oauth_required 16 | def get(self): 17 | response = self._request_get("user") 18 | return User.construct_from(response) 19 | 20 | def get_by_id(self, user_id): 21 | response = self._request_get("users/{}".format(user_id)) 22 | return User.construct_from(response) 23 | 24 | @oauth_required 25 | def get_emotes(self, user_id): 26 | response = self._request_get("users/{}/emotes".format(user_id)) 27 | return response["emoticon_sets"] 28 | 29 | @oauth_required 30 | def check_subscribed_to_channel(self, user_id, channel_id): 31 | response = self._request_get( 32 | "users/{}/subscriptions/{}".format(user_id, channel_id) 33 | ) 34 | return Subscription.construct_from(response) 35 | 36 | def get_all_follows( 37 | self, user_id, direction=DIRECTION_DESC, sort_by=USERS_SORT_BY_CREATED_AT 38 | ): 39 | if direction not in DIRECTIONS: 40 | raise TwitchAttributeException( 41 | "Direction is not valid. Valid values are {}".format(DIRECTIONS) 42 | ) 43 | if sort_by not in USERS_SORT_BY: 44 | raise TwitchAttributeException( 45 | "Sort by is not valid. Valid values are {}".format(USERS_SORT_BY) 46 | ) 47 | offset = 0 48 | params = {"limit": MAX_FOLLOWS_LIMIT, "direction": direction} 49 | follows = [] 50 | while offset is not None: 51 | params.update({"offset": offset}) 52 | response = self._request_get( 53 | "users/{}/follows/channels".format(user_id), params=params 54 | ) 55 | offset = response.get("_offset") 56 | follows.extend(response["follows"]) 57 | return [Follow.construct_from(x) for x in follows] 58 | 59 | def get_follows( 60 | self, 61 | user_id, 62 | limit=25, 63 | offset=0, 64 | direction=DIRECTION_DESC, 65 | sort_by=USERS_SORT_BY_CREATED_AT, 66 | ): 67 | if limit > MAX_FOLLOWS_LIMIT: 68 | raise TwitchAttributeException( 69 | "Maximum number of objects returned in one request is 100" 70 | ) 71 | if direction not in DIRECTIONS: 72 | raise TwitchAttributeException( 73 | "Direction is not valid. Valid values are {}".format(DIRECTIONS) 74 | ) 75 | if sort_by not in USERS_SORT_BY: 76 | raise TwitchAttributeException( 77 | "Sort by is not valid. Valid values are {}".format(USERS_SORT_BY) 78 | ) 79 | params = {"limit": limit, "offset": offset, "direction": direction} 80 | response = self._request_get( 81 | "users/{}/follows/channels".format(user_id), params=params 82 | ) 83 | return [Follow.construct_from(x) for x in response["follows"]] 84 | 85 | def check_follows_channel(self, user_id, channel_id): 86 | response = self._request_get( 87 | "users/{}/follows/channels/{}".format(user_id, channel_id) 88 | ) 89 | return Follow.construct_from(response) 90 | 91 | @oauth_required 92 | def follow_channel(self, user_id, channel_id, notifications=False): 93 | data = {"notifications": notifications} 94 | response = self._request_put( 95 | "users/{}/follows/channels/{}".format(user_id, channel_id), data 96 | ) 97 | return Follow.construct_from(response) 98 | 99 | @oauth_required 100 | def unfollow_channel(self, user_id, channel_id): 101 | self._request_delete("users/{}/follows/channels/{}".format(user_id, channel_id)) 102 | 103 | @oauth_required 104 | def get_user_block_list(self, user_id, limit=25, offset=0): 105 | if limit > 100: 106 | raise TwitchAttributeException( 107 | "Maximum number of objects returned in one request is 100" 108 | ) 109 | 110 | params = {"limit": limit, "offset": offset} 111 | response = self._request_get("users/{}/blocks".format(user_id), params=params) 112 | return [UserBlock.construct_from(x) for x in response["blocks"]] 113 | 114 | @oauth_required 115 | def block_user(self, user_id, blocked_user_id): 116 | response = self._request_put( 117 | "users/{}/blocks/{}".format(user_id, blocked_user_id) 118 | ) 119 | return UserBlock.construct_from(response) 120 | 121 | @oauth_required 122 | def unblock_user(self, user_id, blocked_user_id): 123 | self._request_delete("users/{}/blocks/{}".format(user_id, blocked_user_id)) 124 | 125 | def translate_usernames_to_ids(self, usernames): 126 | if isinstance(usernames, list): 127 | usernames = ",".join(usernames) 128 | 129 | response = self._request_get("users?login={}".format(usernames)) 130 | return [User.construct_from(x) for x in response["users"]] 131 | -------------------------------------------------------------------------------- /twitch/api/communities.py: -------------------------------------------------------------------------------- 1 | from twitch.api.base import TwitchAPI 2 | from twitch.decorators import oauth_required 3 | from twitch.exceptions import TwitchAttributeException 4 | from twitch.resources import Community, User 5 | 6 | 7 | class Communities(TwitchAPI): 8 | def get_by_name(self, community_name): 9 | params = {"name": community_name} 10 | response = self._request_get("communities", params=params) 11 | return Community.construct_from(response) 12 | 13 | def get_by_id(self, community_id): 14 | response = self._request_get("communities/{}".format(community_id)) 15 | return Community.construct_from(response) 16 | 17 | def update( 18 | self, community_id, summary=None, description=None, rules=None, email=None 19 | ): 20 | data = { 21 | "summary": summary, 22 | "description": description, 23 | "rules": rules, 24 | "email": email, 25 | } 26 | self._request_put("communities/{}".format(community_id), data=data) 27 | 28 | def get_top(self, limit=10, cursor=None): 29 | if limit > 100: 30 | raise TwitchAttributeException( 31 | "Maximum number of objects returned in one request is 100" 32 | ) 33 | params = {"limit": limit, "cursor": cursor} 34 | response = self._request_get("communities/top", params=params) 35 | return [Community.construct_from(x) for x in response["communities"]] 36 | 37 | @oauth_required 38 | def get_banned_users(self, community_id, limit=10, cursor=None): 39 | if limit > 100: 40 | raise TwitchAttributeException( 41 | "Maximum number of objects returned in one request is 100" 42 | ) 43 | 44 | params = {"limit": limit, "cursor": cursor} 45 | response = self._request_get( 46 | "communities/{}/bans".format(community_id), params=params 47 | ) 48 | return [User.construct_from(x) for x in response["banned_users"]] 49 | 50 | @oauth_required 51 | def ban_user(self, community_id, user_id): 52 | self._request_put("communities/{}/bans/{}".format(community_id, user_id)) 53 | 54 | @oauth_required 55 | def unban_user(self, community_id, user_id): 56 | self._request_delete("communities/{}/bans/{}".format(community_id, user_id)) 57 | 58 | @oauth_required 59 | def create_avatar_image(self, community_id, avatar_image): 60 | data = { 61 | "avatar_image": avatar_image, 62 | } 63 | self._request_post( 64 | "communities/{}/images/avatar".format(community_id), data=data 65 | ) 66 | 67 | @oauth_required 68 | def delete_avatar_image(self, community_id): 69 | self._request_delete("communities/{}/images/avatar".format(community_id)) 70 | 71 | @oauth_required 72 | def create_cover_image(self, community_id, cover_image): 73 | data = { 74 | "cover_image": cover_image, 75 | } 76 | self._request_post( 77 | "communities/{}/images/cover".format(community_id), data=data 78 | ) 79 | 80 | @oauth_required 81 | def delete_cover_image(self, community_id): 82 | self._request_delete("communities/{}/images/cover".format(community_id)) 83 | 84 | def get_moderators(self, community_id): 85 | response = self._request_get("communities/{}/moderators".format(community_id)) 86 | return [User.construct_from(x) for x in response["moderators"]] 87 | 88 | @oauth_required 89 | def add_moderator(self, community_id, user_id): 90 | self._request_put("communities/{}/moderators/{}".format(community_id, user_id)) 91 | 92 | @oauth_required 93 | def delete_moderator(self, community_id, user_id): 94 | self._request_delete( 95 | "communities/{}/moderators/{}".format(community_id, user_id) 96 | ) 97 | 98 | @oauth_required 99 | def get_permissions(self, community_id): 100 | response = self._request_get("communities/{}/permissions".format(community_id)) 101 | return response 102 | 103 | @oauth_required 104 | def report_violation(self, community_id, channel_id): 105 | data = { 106 | "channel_id": channel_id, 107 | } 108 | self._request_post( 109 | "communities/{}/report_channel".format(community_id), data=data 110 | ) 111 | 112 | @oauth_required 113 | def get_timed_out_users(self, community_id, limit=10, cursor=None): 114 | if limit > 100: 115 | raise TwitchAttributeException( 116 | "Maximum number of objects returned in one request is 100" 117 | ) 118 | params = {"limit": limit, "cursor": cursor} 119 | response = self._request_get( 120 | "communities/{}/timeouts".format(community_id), params=params 121 | ) 122 | return [User.construct_from(x) for x in response["timed_out_users"]] 123 | 124 | @oauth_required 125 | def add_timed_out_user(self, community_id, user_id, duration, reason=None): 126 | data = { 127 | "duration": duration, 128 | "reason": reason, 129 | } 130 | self._request_put( 131 | "communities/{}/timeouts/{}".format(community_id, user_id), data=data 132 | ) 133 | 134 | @oauth_required 135 | def delete_timed_out_user(self, community_id, user_id): 136 | self._request_delete("communities/{}/timeouts/{}".format(community_id, user_id)) 137 | -------------------------------------------------------------------------------- /twitch/helix/base.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | 4 | import requests 5 | from requests import codes 6 | from requests.compat import urljoin 7 | 8 | from twitch.constants import BASE_HELIX_URL 9 | from twitch.exceptions import TwitchNotProvidedException 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | class TwitchAPIMixin(object): 15 | _rate_limit_resets = set() 16 | _rate_limit_remaining = 0 17 | 18 | def _wait_for_rate_limit_reset(self): 19 | if self._rate_limit_remaining == 0: 20 | current_time = int(time.time()) 21 | self._rate_limit_resets = set( 22 | x for x in self._rate_limit_resets if x > current_time 23 | ) 24 | if len(self._rate_limit_resets) > 0: 25 | logger.debug( 26 | "Waiting for rate limit reset", 27 | extra={ 28 | "rate_limit_remaining": self._rate_limit_remaining, 29 | "rate_limit_resets": self._rate_limit_resets, 30 | }, 31 | ) 32 | 33 | reset_time = list(self._rate_limit_resets)[0] 34 | 35 | # Calculate wait time and add 0.1s to the wait time to allow Twitch to reset 36 | # their counter 37 | wait_time = reset_time - current_time + 0.1 38 | time.sleep(wait_time) 39 | 40 | def _get_request_headers(self): 41 | headers = {"Client-ID": self._client_id} 42 | 43 | if self._oauth_token: 44 | headers["Authorization"] = "Bearer {}".format(self._oauth_token) 45 | 46 | return headers 47 | 48 | def _request_get(self, path, params=None): 49 | url = urljoin(BASE_HELIX_URL, path) 50 | headers = self._get_request_headers() 51 | 52 | self._wait_for_rate_limit_reset() 53 | 54 | response = requests.get(url, params=params, headers=headers) 55 | logger.debug( 56 | "Request to %s with params %s took %s", url, params, response.elapsed 57 | ) 58 | 59 | remaining = response.headers.get("Ratelimit-Remaining") 60 | if remaining: 61 | self._rate_limit_remaining = int(remaining) 62 | 63 | reset = response.headers.get("Ratelimit-Reset") 64 | if reset: 65 | self._rate_limit_resets.add(int(reset)) 66 | 67 | # If status code is 429, re-run _request_get which will wait for the appropriate time 68 | # to obey the rate limit 69 | if response.status_code == codes.TOO_MANY_REQUESTS: 70 | logger.debug( 71 | "Twitch responded with 429. Rate limit reached. Waiting for the cooldown." 72 | ) 73 | return self._request_get(path, params=params) 74 | 75 | response.raise_for_status() 76 | return response.json() 77 | 78 | 79 | class APICursor(TwitchAPIMixin): 80 | def __init__( 81 | self, client_id, path, resource, oauth_token=None, cursor=None, params=None 82 | ): 83 | super(APICursor, self).__init__() 84 | self._path = path 85 | self._queue = [] 86 | self._cursor = cursor 87 | self._resource = resource 88 | self._client_id = client_id 89 | self._oauth_token = oauth_token 90 | self._params = params 91 | self._total = None 92 | self._requests_count = 0 93 | 94 | # Pre-fetch the first page as soon as cursor is instantiated 95 | self.next_page() 96 | 97 | def __repr__(self): 98 | return str(self._queue) 99 | 100 | def __len__(self): 101 | return len(self._queue) 102 | 103 | def __iter__(self): 104 | return self 105 | 106 | def __next__(self): 107 | if not self._queue and not self.next_page(): 108 | raise StopIteration() 109 | 110 | return self._queue.pop(0) 111 | 112 | # Python 2 compatibility. 113 | next = __next__ 114 | 115 | def __getitem__(self, index): 116 | return self._queue[index] 117 | 118 | def next_page(self): 119 | # Twitch stops returning a cursor when you're on the last page. So if we've made 120 | # more than 1 request to their API and we don't get a cursor back, it means 121 | # we're on the last page, so return whatever's left in the queue. 122 | if self._requests_count > 0 and not self._cursor: 123 | return self._queue 124 | 125 | if self._cursor: 126 | self._params["after"] = self._cursor 127 | 128 | response = self._request_get(self._path, params=self._params) 129 | self._requests_count += 1 130 | self._queue = [self._resource.construct_from(data) for data in response["data"]] 131 | self._cursor = response["pagination"].get("cursor") 132 | self._total = response.get("total") 133 | return self._queue 134 | 135 | @property 136 | def cursor(self): 137 | return self._cursor 138 | 139 | @property 140 | def total(self): 141 | if not self._total: 142 | raise TwitchNotProvidedException() 143 | return self._total 144 | 145 | 146 | class APIGet(TwitchAPIMixin): 147 | def __init__(self, client_id, path, resource, oauth_token=None, params=None): 148 | super(APIGet, self).__init__() 149 | self._path = path 150 | self._resource = resource 151 | self._client_id = client_id 152 | self._oauth_token = oauth_token 153 | self._params = params 154 | 155 | def fetch(self): 156 | response = self._request_get(self._path, params=self._params) 157 | return [self._resource.construct_from(data) for data in response["data"]] 158 | -------------------------------------------------------------------------------- /tests/test_resources.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | import pytest 4 | 5 | from twitch.resources import ( 6 | Channel, 7 | Comment, 8 | Community, 9 | Featured, 10 | Follow, 11 | Game, 12 | Ingest, 13 | Stream, 14 | StreamMetadata, 15 | Subscription, 16 | Team, 17 | TopGame, 18 | TwitchObject, 19 | User, 20 | UserBlock, 21 | Video, 22 | convert_to_twitch_object, 23 | ) 24 | 25 | 26 | def test_convert_to_twitch_object_output_returns_string_for_string_input(): 27 | data = "squarepants" 28 | result = convert_to_twitch_object("spongebob", data) 29 | assert result == data 30 | 31 | 32 | def test_convert_to_twitch_object_output_returns_list_for_list_input(): 33 | data = ["squarepants", "patrick", "star"] 34 | result = convert_to_twitch_object("spongebob", data) 35 | assert isinstance(result, list) 36 | 37 | 38 | def test_convert_to_twitch_object_output_returns_list_of_objects_for_list_of_objects_input(): 39 | data = [{"spongebob": "squarepants"}, {"patrick": "star"}] 40 | result = convert_to_twitch_object("channel", data) 41 | assert isinstance(result, list) 42 | assert isinstance(result[0], Channel) 43 | assert result[0] == data[0] 44 | 45 | 46 | @pytest.mark.parametrize( 47 | "name,data,expected_type", 48 | [ 49 | ("channel", {"spongebob": "squarepants"}, Channel), 50 | ("videos", {"spongebob": "squarepants"}, Video), 51 | ("user", {"spongebob": "squarepants"}, User), 52 | ("game", {"spongebob": "squarepants"}, Game), 53 | ("stream", {"spongebob": "squarepants"}, Stream), 54 | ("comments", {"spongebob": "squarepants"}, Comment), 55 | ("owner", {"spongebob": "squarepants"}, User), 56 | ], 57 | ) 58 | def test_convert_to_twitch_object_output_returns_correct_object( 59 | name, data, expected_type 60 | ): 61 | result = convert_to_twitch_object(name, data) 62 | assert isinstance(result, expected_type) 63 | assert result == data 64 | 65 | 66 | @pytest.mark.parametrize( 67 | "name,data,expected", 68 | [ 69 | ("created_at", "2016-11-29T15:52:27Z", datetime(2016, 11, 29, 15, 52, 27)), 70 | ( 71 | "updated_at", 72 | "2017-03-06T18:40:51.855Z", 73 | datetime(2017, 3, 6, 18, 40, 51, 855000), 74 | ), 75 | ("published_at", "2017-02-14T22:27:54Z", datetime(2017, 2, 14, 22, 27, 54)), 76 | ("published_at", None, None), 77 | ], 78 | ) 79 | def test_datetimes_are_converted_correctly_to_datetime_objects(name, data, expected): 80 | result = convert_to_twitch_object(name, data) 81 | if result is not None: 82 | assert isinstance(result, datetime) 83 | assert result == expected 84 | 85 | 86 | class TestTwitchObject(object): 87 | def test_attributes_are_stored_and_fetched_from_dict(self): 88 | obj = TwitchObject() 89 | value = "spongebob" 90 | obj.spongebob = value 91 | 92 | assert "spongebob" in obj 93 | assert "_spongebob" not in obj.__dict__ 94 | assert "spongebob" not in obj.__dict__ 95 | assert obj["spongebob"] == value 96 | assert obj.spongebob == value 97 | 98 | del obj.spongebob 99 | 100 | assert "spongebob" not in obj 101 | assert "spongebob" not in obj.__dict__ 102 | 103 | def test_prefixed_attributes_are_stored_on_the_object(self): 104 | obj = TwitchObject() 105 | value = "spongebob" 106 | obj._spongebob = value 107 | 108 | obj._spongebob 109 | getattr(obj, "_spongebob") 110 | assert "spongebob" not in obj 111 | assert "_spongebob" not in obj 112 | assert "_spongebob" in obj.__dict__ 113 | assert obj._spongebob == value 114 | 115 | del obj._spongebob 116 | 117 | assert "spongebob" not in obj 118 | assert "spongebob" not in obj.__dict__ 119 | 120 | def test_setitem_sets_item_to_dict(self): 121 | obj = TwitchObject() 122 | value = "squarepants" 123 | obj["spongebob"] = value 124 | assert obj["spongebob"] == value 125 | 126 | def test_setitem_removes_underscore_prefix(self): 127 | obj = TwitchObject() 128 | value = "squarepants" 129 | obj["_spongebob"] = value 130 | assert obj["spongebob"] == value 131 | assert "_spongebob" not in obj 132 | assert "_spongebob" not in obj.__dict__ 133 | 134 | def test_construct_form_returns_class_with_set_values(self): 135 | obj = TwitchObject.construct_from({"spongebob": "squarepants"}) 136 | assert isinstance(obj, TwitchObject) 137 | assert obj.spongebob == "squarepants" 138 | 139 | def test_refresh_from_sets_all_values_to_object(self): 140 | obj = TwitchObject() 141 | obj.refresh_from({"spongebob": "squarepants", "patrick": "star", "_id": 1234}) 142 | 143 | assert obj.spongebob == "squarepants" 144 | assert obj["patrick"] == "star" 145 | assert obj.id == 1234 146 | 147 | 148 | @pytest.mark.parametrize( 149 | "resource", 150 | [ 151 | (Channel), 152 | (Community), 153 | (Featured), 154 | (Follow), 155 | (Game), 156 | (Ingest), 157 | (Stream), 158 | (StreamMetadata), 159 | (Subscription), 160 | (Team), 161 | (TopGame), 162 | (User), 163 | (UserBlock), 164 | (Video), 165 | ], 166 | ) 167 | def test_resource_gets_created_correctly(resource): 168 | obj = resource.construct_from({"spongebob": "squarepants", "_id": 1234}) 169 | assert isinstance(obj, resource) 170 | assert obj.id == 1234 171 | assert obj.spongebob == "squarepants" 172 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | 6 | # 7 | # python-twitch-client documentation build configuration file, created by 8 | # sphinx-quickstart on Wed Feb 15 20:51:23 2017. 9 | # 10 | # This file is execfile()d with the current directory set to its 11 | # containing dir. 12 | # 13 | # Note that not all possible configuration values are present in this 14 | # autogenerated file. 15 | # 16 | # All configuration values have a default; values that are commented out 17 | # serve to show the default. 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | # 23 | # import os 24 | # import sys 25 | # sys.path.insert(0, os.path.abspath('.')) 26 | 27 | 28 | # -- General configuration ------------------------------------------------ 29 | 30 | # If your documentation needs a minimal Sphinx version, state it here. 31 | # 32 | # needs_sphinx = '1.0' 33 | 34 | # Add any Sphinx extension module names here, as strings. They can be 35 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 36 | # ones. 37 | extensions = [] 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ["_templates"] 41 | 42 | # The suffix(es) of source filenames. 43 | # You can specify multiple suffix as a list of string: 44 | # 45 | # source_suffix = ['.rst', '.md'] 46 | source_suffix = ".rst" 47 | 48 | # The master toctree document. 49 | master_doc = "index" 50 | 51 | # General information about the project. 52 | project = "python-twitch-client" 53 | copyright = "2018-2020, Tomaz Sifrer" 54 | author = "Tomaz Sifrer" 55 | 56 | # The version info for the project you're documenting, acts as replacement for 57 | # |version| and |release|, also used in various other places throughout the 58 | # built documents. 59 | # 60 | # The short X.Y version. 61 | version = "0.1" 62 | # The full version, including alpha/beta/rc tags. 63 | release = "0.1" 64 | 65 | # The language for content autogenerated by Sphinx. Refer to documentation 66 | # for a list of supported languages. 67 | # 68 | # This is also used if you do content translation via gettext catalogs. 69 | # Usually you set "language" from the command line for these cases. 70 | language = None 71 | 72 | # List of patterns, relative to source directory, that match files and 73 | # directories to ignore when looking for source files. 74 | # This patterns also effect to html_static_path and html_extra_path 75 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 76 | 77 | # The name of the Pygments (syntax highlighting) style to use. 78 | pygments_style = "sphinx" 79 | 80 | # If true, `todo` and `todoList` produce output, else they produce nothing. 81 | todo_include_todos = False 82 | 83 | 84 | # -- Options for HTML output ---------------------------------------------- 85 | 86 | # The theme to use for HTML and HTML Help pages. See the documentation for 87 | # a list of builtin themes. 88 | 89 | html_theme = "default" 90 | 91 | # on_rtd is whether we are on readthedocs.org, this line of code grabbed from docs.readthedocs.org 92 | on_rtd = os.environ.get("READTHEDOCS", None) == "True" 93 | 94 | if not on_rtd: # only import and set the theme if we're building docs locally 95 | import sphinx_rtd_theme 96 | 97 | html_theme = "sphinx_rtd_theme" 98 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 99 | 100 | # Theme options are theme-specific and customize the look and feel of a theme 101 | # further. For a list of options available for each theme, see the 102 | # documentation. 103 | # 104 | # html_theme_options = {} 105 | 106 | # Add any paths that contain custom static files (such as style sheets) here, 107 | # relative to this directory. They are copied after the builtin static files, 108 | # so a file named "default.css" will overwrite the builtin "default.css". 109 | html_static_path = ["_static"] 110 | 111 | 112 | # -- Options for HTMLHelp output ------------------------------------------ 113 | 114 | # Output file base name for HTML help builder. 115 | htmlhelp_basename = "python-twitch-clientdoc" 116 | 117 | 118 | # -- Options for LaTeX output --------------------------------------------- 119 | 120 | latex_elements = { 121 | # The paper size ('letterpaper' or 'a4paper'). 122 | # 123 | # 'papersize': 'letterpaper', 124 | # The font size ('10pt', '11pt' or '12pt'). 125 | # 126 | # 'pointsize': '10pt', 127 | # Additional stuff for the LaTeX preamble. 128 | # 129 | # 'preamble': '', 130 | # Latex figure (float) alignment 131 | # 132 | # 'figure_align': 'htbp', 133 | } 134 | 135 | # Grouping the document tree into LaTeX files. List of tuples 136 | # (source start file, target name, title, 137 | # author, documentclass [howto, manual, or own class]). 138 | latex_documents = [ 139 | ( 140 | master_doc, 141 | "python-twitch-client.tex", 142 | "python-twitch-client Documentation", 143 | "Tomaz Sifrer", 144 | "manual", 145 | ), 146 | ] 147 | 148 | 149 | # -- Options for manual page output --------------------------------------- 150 | 151 | # One entry per manual page. List of tuples 152 | # (source start file, name, description, authors, manual section). 153 | man_pages = [ 154 | ( 155 | master_doc, 156 | "python-twitch-client", 157 | "python-twitch-client Documentation", 158 | [author], 159 | 1, 160 | ) 161 | ] 162 | 163 | 164 | # -- Options for Texinfo output ------------------------------------------- 165 | 166 | # Grouping the document tree into Texinfo files. List of tuples 167 | # (source start file, target name, title, author, 168 | # dir menu entry, description, category) 169 | texinfo_documents = [ 170 | ( 171 | master_doc, 172 | "python-twitch-client", 173 | "python-twitch-client Documentation", 174 | author, 175 | "python-twitch-client", 176 | "An easy to use Python library for accessing the Twitch API.", 177 | "Miscellaneous", 178 | ), 179 | ] 180 | -------------------------------------------------------------------------------- /twitch/api/channels.py: -------------------------------------------------------------------------------- 1 | from twitch.api.base import TwitchAPI 2 | from twitch.constants import ( 3 | BROADCAST_TYPE_HIGHLIGHT, 4 | BROADCAST_TYPES, 5 | DIRECTION_ASC, 6 | DIRECTION_DESC, 7 | DIRECTIONS, 8 | VIDEO_SORT_TIME, 9 | VIDEO_SORTS, 10 | ) 11 | from twitch.decorators import oauth_required 12 | from twitch.exceptions import TwitchAttributeException 13 | from twitch.resources import Channel, Community, Follow, Subscription, Team, User, Video 14 | 15 | 16 | class Channels(TwitchAPI): 17 | @oauth_required 18 | def get(self): 19 | response = self._request_get("channel") 20 | return Channel.construct_from(response) 21 | 22 | def get_by_id(self, channel_id): 23 | response = self._request_get("channels/{}".format(channel_id)) 24 | return Channel.construct_from(response) 25 | 26 | @oauth_required 27 | def update( 28 | self, channel_id, status=None, game=None, delay=None, channel_feed_enabled=None 29 | ): 30 | data = {} 31 | if status is not None: 32 | data["status"] = status 33 | if game is not None: 34 | data["game"] = game 35 | if delay is not None: 36 | data["delay"] = delay 37 | if channel_feed_enabled is not None: 38 | data["channel_feed_enabled"] = channel_feed_enabled 39 | 40 | post_data = {"channel": data} 41 | response = self._request_put("channels/{}".format(channel_id), post_data) 42 | return Channel.construct_from(response) 43 | 44 | @oauth_required 45 | def get_editors(self, channel_id): 46 | response = self._request_get("channels/{}/editors".format(channel_id)) 47 | return [User.construct_from(x) for x in response["users"]] 48 | 49 | def get_followers( 50 | self, channel_id, limit=25, offset=0, cursor=None, direction=DIRECTION_DESC 51 | ): 52 | if limit > 100: 53 | raise TwitchAttributeException( 54 | "Maximum number of objects returned in one request is 100" 55 | ) 56 | if direction not in DIRECTIONS: 57 | raise TwitchAttributeException( 58 | "Direction is not valid. Valid values are {}".format(DIRECTIONS) 59 | ) 60 | 61 | params = {"limit": limit, "offset": offset, "direction": direction} 62 | if cursor is not None: 63 | params["cursor"] = cursor 64 | response = self._request_get( 65 | "channels/{}/follows".format(channel_id), params=params 66 | ) 67 | return [Follow.construct_from(x) for x in response["follows"]] 68 | 69 | def get_teams(self, channel_id): 70 | response = self._request_get("channels/{}/teams".format(channel_id)) 71 | return [Team.construct_from(x) for x in response["teams"]] 72 | 73 | @oauth_required 74 | def get_subscribers(self, channel_id, limit=25, offset=0, direction=DIRECTION_ASC): 75 | if limit > 100: 76 | raise TwitchAttributeException( 77 | "Maximum number of objects returned in one request is 100" 78 | ) 79 | if direction not in DIRECTIONS: 80 | raise TwitchAttributeException( 81 | "Direction is not valid. Valid values are {}".format(DIRECTIONS) 82 | ) 83 | 84 | params = {"limit": limit, "offset": offset, "direction": direction} 85 | response = self._request_get( 86 | "channels/{}/subscriptions".format(channel_id), params=params 87 | ) 88 | return [Subscription.construct_from(x) for x in response["subscriptions"]] 89 | 90 | def check_subscription_by_user(self, channel_id, user_id): 91 | response = self._request_get( 92 | "channels/{}/subscriptions/{}".format(channel_id, user_id) 93 | ) 94 | return Subscription.construct_from(response) 95 | 96 | def get_videos( 97 | self, 98 | channel_id, 99 | limit=10, 100 | offset=0, 101 | broadcast_type=BROADCAST_TYPE_HIGHLIGHT, 102 | language=None, 103 | sort=VIDEO_SORT_TIME, 104 | ): 105 | if limit > 100: 106 | raise TwitchAttributeException( 107 | "Maximum number of objects returned in one request is 100" 108 | ) 109 | 110 | if broadcast_type not in BROADCAST_TYPES: 111 | raise TwitchAttributeException( 112 | "Broadcast type is not valid. Valid values are {}".format( 113 | BROADCAST_TYPES 114 | ) 115 | ) 116 | 117 | if sort not in VIDEO_SORTS: 118 | raise TwitchAttributeException( 119 | "Sort is not valid. Valid values are {}".format(VIDEO_SORTS) 120 | ) 121 | 122 | params = { 123 | "limit": limit, 124 | "offset": offset, 125 | "broadcast_type": broadcast_type, 126 | "sort": sort, 127 | } 128 | if language is not None: 129 | params["language"] = language 130 | response = self._request_get( 131 | "channels/{}/videos".format(channel_id), params=params 132 | ) 133 | return [Video.construct_from(x) for x in response["videos"]] 134 | 135 | @oauth_required 136 | def start_commercial(self, channel_id, duration=30): 137 | data = {"duration": duration} 138 | response = self._request_post( 139 | "channels/{}/commercial".format(channel_id), data=data 140 | ) 141 | return response 142 | 143 | @oauth_required 144 | def reset_stream_key(self, channel_id): 145 | response = self._request_delete("channels/{}/stream_key".format(channel_id)) 146 | return Channel.construct_from(response) 147 | 148 | def get_community(self, channel_id): 149 | response = self._request_get("channels/{}/community".format(channel_id)) 150 | return Community.construct_from(response) 151 | 152 | def set_community(self, channel_id, community_id): 153 | self._request_put("channels/{}/community/{}".format(channel_id, community_id)) 154 | 155 | def delete_from_community(self, channel_id): 156 | self._request_delete("channels/{}/community".format(channel_id)) 157 | -------------------------------------------------------------------------------- /tests/api/test_collections.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | import responses 5 | 6 | from twitch.client import TwitchClient 7 | from twitch.constants import BASE_URL 8 | from twitch.exceptions import TwitchAttributeException 9 | from twitch.resources import Collection, Item 10 | 11 | example_collection = { 12 | "_id": "myIbIFkZphQSbQ", 13 | "items_count": 3, 14 | "updated_at": "2017-03-06T18:40:51.855Z", 15 | } 16 | 17 | example_item = { 18 | "_id": "eyJ0eXBlIjoidmlkZW8iLCJpZCI6IjEyMjEzODk0OSJ9", 19 | "item_id": "122138949", 20 | "item_type": "video", 21 | "published_at": "2017-02-14T22:27:54Z", 22 | "title": "Fanboys Episode 1 w/ Gassy Mexican", 23 | } 24 | 25 | 26 | @responses.activate 27 | def test_get_metadata(): 28 | collection_id = "abcd" 29 | responses.add( 30 | responses.GET, 31 | "{}collections/{}".format(BASE_URL, collection_id), 32 | body=json.dumps(example_collection), 33 | status=200, 34 | content_type="application/json", 35 | ) 36 | 37 | client = TwitchClient("client id", "oauth token") 38 | 39 | collection = client.collections.get_metadata(collection_id) 40 | 41 | assert len(responses.calls) == 1 42 | assert isinstance(collection, Collection) 43 | assert collection.id == example_collection["_id"] 44 | assert collection.items_count == example_collection["items_count"] 45 | 46 | 47 | @responses.activate 48 | def test_get(): 49 | collection_id = "abcd" 50 | response = {"_id": "myIbIFkZphQSbQ", "items": [example_item]} 51 | responses.add( 52 | responses.GET, 53 | "{}collections/{}/items".format(BASE_URL, collection_id), 54 | body=json.dumps(response), 55 | status=200, 56 | content_type="application/json", 57 | ) 58 | 59 | client = TwitchClient("client id", "oauth token") 60 | 61 | items = client.collections.get(collection_id) 62 | 63 | assert len(responses.calls) == 1 64 | assert len(items) == 1 65 | item = items[0] 66 | assert isinstance(item, Item) 67 | assert item.id == example_item["_id"] 68 | assert item.title == example_item["title"] 69 | 70 | 71 | @responses.activate 72 | def test_get_by_channel(): 73 | channel_id = "abcd" 74 | response = {"_cursor": None, "collections": [example_collection]} 75 | responses.add( 76 | responses.GET, 77 | "{}channels/{}/collections".format(BASE_URL, channel_id), 78 | body=json.dumps(response), 79 | status=200, 80 | content_type="application/json", 81 | ) 82 | 83 | client = TwitchClient("client id", "oauth token") 84 | 85 | collections = client.collections.get_by_channel(channel_id) 86 | 87 | assert len(responses.calls) == 1 88 | assert len(collections) == 1 89 | collection = collections[0] 90 | assert isinstance(collection, Collection) 91 | assert collection.id == example_collection["_id"] 92 | assert collection.items_count == example_collection["items_count"] 93 | 94 | 95 | @responses.activate 96 | @pytest.mark.parametrize("param,value", [("limit", 101)]) 97 | def test_get_by_channel_raises_if_wrong_params_are_passed_in(param, value): 98 | client = TwitchClient("client id", "oauth token") 99 | kwargs = {param: value} 100 | with pytest.raises(TwitchAttributeException): 101 | client.collections.get_by_channel("1234", **kwargs) 102 | 103 | 104 | @responses.activate 105 | def test_create(): 106 | channel_id = "abcd" 107 | responses.add( 108 | responses.POST, 109 | "{}channels/{}/collections".format(BASE_URL, channel_id), 110 | body=json.dumps(example_collection), 111 | status=200, 112 | content_type="application/json", 113 | ) 114 | 115 | client = TwitchClient("client id", "oauth client") 116 | 117 | collection = client.collections.create(channel_id, "this is title") 118 | 119 | assert len(responses.calls) == 1 120 | assert isinstance(collection, Collection) 121 | assert collection.id == example_collection["_id"] 122 | assert collection.items_count == example_collection["items_count"] 123 | 124 | 125 | @responses.activate 126 | def test_update(): 127 | collection_id = "abcd" 128 | responses.add( 129 | responses.PUT, 130 | "{}collections/{}".format(BASE_URL, collection_id), 131 | status=204, 132 | content_type="application/json", 133 | ) 134 | 135 | client = TwitchClient("client id", "oauth client") 136 | 137 | client.collections.update(collection_id, "this is title") 138 | 139 | assert len(responses.calls) == 1 140 | 141 | 142 | @responses.activate 143 | def test_create_thumbnail(): 144 | collection_id = "abcd" 145 | responses.add( 146 | responses.PUT, 147 | "{}collections/{}/thumbnail".format(BASE_URL, collection_id), 148 | status=204, 149 | content_type="application/json", 150 | ) 151 | 152 | client = TwitchClient("client id", "oauth client") 153 | 154 | client.collections.create_thumbnail(collection_id, "1234") 155 | 156 | assert len(responses.calls) == 1 157 | 158 | 159 | @responses.activate 160 | def test_delete(): 161 | collection_id = "abcd" 162 | responses.add( 163 | responses.DELETE, 164 | "{}collections/{}".format(BASE_URL, collection_id), 165 | status=204, 166 | content_type="application/json", 167 | ) 168 | 169 | client = TwitchClient("client id", "oauth client") 170 | 171 | client.collections.delete(collection_id) 172 | 173 | assert len(responses.calls) == 1 174 | 175 | 176 | @responses.activate 177 | def test_add_item(): 178 | collection_id = "abcd" 179 | responses.add( 180 | responses.PUT, 181 | "{}collections/{}/items".format(BASE_URL, collection_id), 182 | body=json.dumps(example_item), 183 | status=200, 184 | content_type="application/json", 185 | ) 186 | 187 | client = TwitchClient("client id", "oauth client") 188 | 189 | item = client.collections.add_item(collection_id, "1234", "video") 190 | 191 | assert len(responses.calls) == 1 192 | assert isinstance(item, Item) 193 | assert item.id == example_item["_id"] 194 | assert item.title == example_item["title"] 195 | 196 | 197 | @responses.activate 198 | def test_delete_item(): 199 | collection_id = "abcd" 200 | collection_item_id = "1234" 201 | responses.add( 202 | responses.DELETE, 203 | "{}collections/{}/items/{}".format(BASE_URL, collection_id, collection_item_id), 204 | status=204, 205 | content_type="application/json", 206 | ) 207 | 208 | client = TwitchClient("client id", "oauth client") 209 | 210 | client.collections.delete_item(collection_id, collection_item_id) 211 | 212 | assert len(responses.calls) == 1 213 | 214 | 215 | @responses.activate 216 | def test_move_item(): 217 | collection_id = "abcd" 218 | collection_item_id = "1234" 219 | responses.add( 220 | responses.PUT, 221 | "{}collections/{}/items/{}".format(BASE_URL, collection_id, collection_item_id), 222 | status=204, 223 | content_type="application/json", 224 | ) 225 | 226 | client = TwitchClient("client id", "oauth client") 227 | 228 | client.collections.move_item(collection_id, collection_item_id, 3) 229 | 230 | assert len(responses.calls) == 1 231 | -------------------------------------------------------------------------------- /tests/api/test_streams.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | import responses 5 | 6 | from twitch.client import TwitchClient 7 | from twitch.constants import BASE_URL 8 | from twitch.exceptions import TwitchAttributeException 9 | from twitch.resources import Channel, Featured, Stream 10 | 11 | example_stream = { 12 | "_id": 23932774784, 13 | "game": "BATMAN - The Telltale Series", 14 | "channel": {"_id": 7236692, "name": "dansgaming"}, 15 | } 16 | 17 | example_stream_response = {"stream": example_stream} 18 | 19 | example_streams_response = {"_total": 1295, "streams": [example_stream]} 20 | 21 | example_featured_response = { 22 | "featured": [ 23 | { 24 | "title": "Bethesda Plays The Elder Scrolls: Legends | More Arena & Deckbuilding", 25 | "stream": example_stream, 26 | } 27 | ] 28 | } 29 | 30 | 31 | @responses.activate 32 | def test_get_stream_by_user(): 33 | channel_id = 7236692 34 | responses.add( 35 | responses.GET, 36 | "{}streams/{}".format(BASE_URL, channel_id), 37 | body=json.dumps(example_stream_response), 38 | status=200, 39 | content_type="application/json", 40 | ) 41 | 42 | client = TwitchClient("client id") 43 | 44 | stream = client.streams.get_stream_by_user(channel_id) 45 | 46 | assert len(responses.calls) == 1 47 | assert isinstance(stream, Stream) 48 | assert stream.id == example_stream_response["stream"]["_id"] 49 | assert stream.game == example_stream_response["stream"]["game"] 50 | 51 | assert isinstance(stream.channel, Channel) 52 | assert stream.channel.id == example_stream_response["stream"]["channel"]["_id"] 53 | assert stream.channel.name == example_stream_response["stream"]["channel"]["name"] 54 | 55 | 56 | @responses.activate 57 | def test_get_stream_by_user_returns_none_if_stream_is_offline(): 58 | channel_id = 7236692 59 | responses.add( 60 | responses.GET, 61 | "{}streams/{}".format(BASE_URL, channel_id), 62 | body=json.dumps({"stream": None}), 63 | status=200, 64 | content_type="application/json", 65 | ) 66 | 67 | client = TwitchClient("client id") 68 | 69 | stream = client.streams.get_stream_by_user(channel_id) 70 | 71 | assert len(responses.calls) == 1 72 | assert stream is None 73 | 74 | 75 | @responses.activate 76 | @pytest.mark.parametrize("param,value", [("stream_type", "abcd")]) 77 | def test_get_stream_by_user_raises_if_wrong_params_are_passed_in(param, value): 78 | client = TwitchClient("client id") 79 | kwargs = {param: value} 80 | with pytest.raises(TwitchAttributeException): 81 | client.streams.get_stream_by_user("1234", **kwargs) 82 | 83 | 84 | @responses.activate 85 | def test_get_live_streams(): 86 | responses.add( 87 | responses.GET, 88 | "{}streams".format(BASE_URL), 89 | body=json.dumps(example_streams_response), 90 | status=200, 91 | content_type="application/json", 92 | ) 93 | 94 | client = TwitchClient("client id") 95 | 96 | streams = client.streams.get_live_streams() 97 | 98 | assert len(responses.calls) == 1 99 | assert len(streams) == 1 100 | stream = streams[0] 101 | assert isinstance(stream, Stream) 102 | assert stream.id == example_stream_response["stream"]["_id"] 103 | assert stream.game == example_stream_response["stream"]["game"] 104 | 105 | assert isinstance(stream.channel, Channel) 106 | assert stream.channel.id == example_stream_response["stream"]["channel"]["_id"] 107 | assert stream.channel.name == example_stream_response["stream"]["channel"]["name"] 108 | 109 | 110 | @responses.activate 111 | @pytest.mark.parametrize("param,value", [("limit", 101)]) 112 | def test_get_live_streams_raises_if_wrong_params_are_passed_in(param, value): 113 | client = TwitchClient("client id") 114 | kwargs = {param: value} 115 | with pytest.raises(TwitchAttributeException): 116 | client.streams.get_live_streams(**kwargs) 117 | 118 | 119 | @responses.activate 120 | def test_get_summary(): 121 | response = {"channels": 1417, "viewers": 19973} 122 | responses.add( 123 | responses.GET, 124 | "{}streams/summary".format(BASE_URL), 125 | body=json.dumps(response), 126 | status=200, 127 | content_type="application/json", 128 | ) 129 | 130 | client = TwitchClient("client id") 131 | 132 | summary = client.streams.get_summary() 133 | 134 | assert len(responses.calls) == 1 135 | assert isinstance(summary, dict) 136 | assert summary["channels"] == response["channels"] 137 | assert summary["viewers"] == response["viewers"] 138 | 139 | 140 | @responses.activate 141 | def test_get_featured(): 142 | responses.add( 143 | responses.GET, 144 | "{}streams/featured".format(BASE_URL), 145 | body=json.dumps(example_featured_response), 146 | status=200, 147 | content_type="application/json", 148 | ) 149 | 150 | client = TwitchClient("client id") 151 | 152 | featured = client.streams.get_featured() 153 | 154 | assert len(responses.calls) == 1 155 | assert len(featured) == 1 156 | feature = featured[0] 157 | assert isinstance(feature, Featured) 158 | assert feature.title == example_featured_response["featured"][0]["title"] 159 | stream = feature.stream 160 | assert isinstance(stream, Stream) 161 | assert stream.id == example_stream_response["stream"]["_id"] 162 | assert stream.game == example_stream_response["stream"]["game"] 163 | 164 | 165 | @responses.activate 166 | @pytest.mark.parametrize("param,value", [("limit", 101)]) 167 | def test_get_featured_raises_if_wrong_params_are_passed_in(param, value): 168 | client = TwitchClient("client id") 169 | kwargs = {param: value} 170 | with pytest.raises(TwitchAttributeException): 171 | client.streams.get_featured(**kwargs) 172 | 173 | 174 | @responses.activate 175 | def test_get_followed(): 176 | responses.add( 177 | responses.GET, 178 | "{}streams/followed".format(BASE_URL), 179 | body=json.dumps(example_streams_response), 180 | status=200, 181 | content_type="application/json", 182 | ) 183 | 184 | client = TwitchClient("client id", "oauth token") 185 | 186 | streams = client.streams.get_followed() 187 | 188 | assert len(responses.calls) == 1 189 | assert len(streams) == 1 190 | stream = streams[0] 191 | assert isinstance(stream, Stream) 192 | assert stream.id == example_stream_response["stream"]["_id"] 193 | assert stream.game == example_stream_response["stream"]["game"] 194 | 195 | assert isinstance(stream.channel, Channel) 196 | assert stream.channel.id == example_stream_response["stream"]["channel"]["_id"] 197 | assert stream.channel.name == example_stream_response["stream"]["channel"]["name"] 198 | 199 | 200 | @responses.activate 201 | @pytest.mark.parametrize("param,value", [("limit", 101), ("stream_type", "abcd")]) 202 | def test_get_followed_raises_if_wrong_params_are_passed_in(param, value): 203 | client = TwitchClient("client id", "oauth token") 204 | kwargs = {param: value} 205 | with pytest.raises(TwitchAttributeException): 206 | client.streams.get_followed(**kwargs) 207 | 208 | 209 | @responses.activate 210 | def test_get_streams_in_community(): 211 | community_id = "abcd" 212 | responses.add( 213 | responses.GET, 214 | "{}streams".format(BASE_URL), 215 | body=json.dumps(example_streams_response), 216 | status=200, 217 | content_type="application/json", 218 | ) 219 | 220 | client = TwitchClient("client id") 221 | 222 | streams = client.streams.get_streams_in_community(community_id) 223 | 224 | assert len(responses.calls) == 1 225 | assert len(streams) == 1 226 | stream = streams[0] 227 | assert isinstance(stream, Stream) 228 | assert stream.id == example_stream_response["stream"]["_id"] 229 | assert stream.game == example_stream_response["stream"]["game"] 230 | 231 | assert isinstance(stream.channel, Channel) 232 | assert stream.channel.id == example_stream_response["stream"]["channel"]["_id"] 233 | assert stream.channel.name == example_stream_response["stream"]["channel"]["name"] 234 | -------------------------------------------------------------------------------- /tests/api/test_base.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | import pytest 5 | import responses 6 | from requests import exceptions 7 | 8 | from twitch.api.base import BASE_URL, TwitchAPI 9 | 10 | dummy_data = {"spongebob": "squarepants"} 11 | 12 | 13 | def test_get_request_headers_include_version_and_client_id(): 14 | client_id = "abcd" 15 | api = TwitchAPI(client_id=client_id) 16 | 17 | headers = api._get_request_headers() 18 | 19 | assert len(headers) == 2 20 | assert headers["Accept"] == "application/vnd.twitchtv.v5+json" 21 | assert headers["Client-ID"] == client_id 22 | 23 | 24 | def test_get_request_headers_includes_authorization(): 25 | client_id = "client" 26 | oauth_token = "token" 27 | api = TwitchAPI(client_id=client_id, oauth_token=oauth_token) 28 | 29 | headers = api._get_request_headers() 30 | 31 | assert len(headers) == 3 32 | assert headers["Accept"] == "application/vnd.twitchtv.v5+json" 33 | assert headers["Client-ID"] == client_id 34 | assert headers["Authorization"] == "OAuth {}".format(oauth_token) 35 | 36 | 37 | @responses.activate 38 | def test_request_get_returns_dictionary_if_successful(): 39 | responses.add( 40 | responses.GET, 41 | BASE_URL, 42 | body=json.dumps(dummy_data), 43 | status=200, 44 | content_type="application/json", 45 | ) 46 | 47 | api = TwitchAPI(client_id="client") 48 | response = api._request_get("") 49 | 50 | assert isinstance(response, dict) 51 | assert response == dummy_data 52 | 53 | 54 | @responses.activate 55 | def test_request_get_sends_headers_with_the_request(): 56 | responses.add( 57 | responses.GET, 58 | BASE_URL, 59 | body=json.dumps(dummy_data), 60 | status=200, 61 | content_type="application/json", 62 | ) 63 | 64 | api = TwitchAPI(client_id="client") 65 | api._request_get("") 66 | 67 | assert "Client-ID" in responses.calls[0].request.headers 68 | assert "Accept" in responses.calls[0].request.headers 69 | 70 | 71 | @responses.activate 72 | def test_request_get_binary_body(): 73 | responses.add( 74 | responses.GET, 75 | BASE_URL, 76 | body=b"binary", 77 | status=200, 78 | content_type="application/octet-stream", 79 | ) 80 | 81 | api = TwitchAPI(client_id="client") 82 | response = api._request_get("", json=False) 83 | 84 | assert response.content == b"binary" 85 | 86 | 87 | @responses.activate 88 | @pytest.mark.parametrize("status", [(500), (400)]) 89 | def test_request_get_raises_exception_if_not_200_response(status, monkeypatch): 90 | responses.add( 91 | responses.GET, BASE_URL, status=status, content_type="application/json" 92 | ) 93 | 94 | def mockreturn(path): 95 | return "tests/api/dummy_credentials.cfg" 96 | 97 | monkeypatch.setattr(os.path, "expanduser", mockreturn) 98 | 99 | api = TwitchAPI(client_id="client") 100 | 101 | with pytest.raises(exceptions.HTTPError): 102 | api._request_get("") 103 | 104 | 105 | @responses.activate 106 | def test_request_put_returns_dictionary_if_successful(): 107 | responses.add( 108 | responses.PUT, 109 | BASE_URL, 110 | body=json.dumps(dummy_data), 111 | status=200, 112 | content_type="application/json", 113 | ) 114 | 115 | api = TwitchAPI(client_id="client") 116 | response = api._request_put("", dummy_data) 117 | 118 | assert isinstance(response, dict) 119 | assert response == dummy_data 120 | 121 | 122 | @responses.activate 123 | def test_request_put_sends_headers_with_the_request(): 124 | responses.add(responses.PUT, BASE_URL, status=204, content_type="application/json") 125 | 126 | api = TwitchAPI(client_id="client") 127 | api._request_put("", dummy_data) 128 | 129 | assert "Client-ID" in responses.calls[0].request.headers 130 | assert "Accept" in responses.calls[0].request.headers 131 | 132 | 133 | @responses.activate 134 | def test_request_put_does_not_raise_exception_if_successful_and_returns_json(): 135 | responses.add( 136 | responses.PUT, 137 | BASE_URL, 138 | body=json.dumps(dummy_data), 139 | status=200, 140 | content_type="application/json", 141 | ) 142 | 143 | api = TwitchAPI(client_id="client") 144 | response = api._request_put("", dummy_data) 145 | assert response == dummy_data 146 | 147 | 148 | @responses.activate 149 | @pytest.mark.parametrize("status", [(500), (400)]) 150 | def test_request_put_raises_exception_if_not_200_response(status): 151 | responses.add( 152 | responses.PUT, BASE_URL, status=status, content_type="application/json" 153 | ) 154 | 155 | api = TwitchAPI(client_id="client") 156 | 157 | with pytest.raises(exceptions.HTTPError): 158 | api._request_put("", dummy_data) 159 | 160 | 161 | @responses.activate 162 | def test_request_delete_does_not_raise_exception_if_successful(): 163 | responses.add( 164 | responses.DELETE, BASE_URL, status=204, content_type="application/json" 165 | ) 166 | 167 | api = TwitchAPI(client_id="client") 168 | api._request_delete("") 169 | 170 | 171 | @responses.activate 172 | def test_request_delete_does_not_raise_exception_if_successful_and_returns_json(): 173 | responses.add( 174 | responses.DELETE, 175 | BASE_URL, 176 | body=json.dumps(dummy_data), 177 | status=200, 178 | content_type="application/json", 179 | ) 180 | 181 | api = TwitchAPI(client_id="client") 182 | response = api._request_delete("") 183 | assert response == dummy_data 184 | 185 | 186 | @responses.activate 187 | def test_request_delete_sends_headers_with_the_request(): 188 | responses.add( 189 | responses.DELETE, BASE_URL, status=204, content_type="application/json" 190 | ) 191 | 192 | api = TwitchAPI(client_id="client") 193 | api._request_delete("") 194 | 195 | assert "Client-ID" in responses.calls[0].request.headers 196 | assert "Accept" in responses.calls[0].request.headers 197 | 198 | 199 | @responses.activate 200 | @pytest.mark.parametrize("status", [(500), (400)]) 201 | def test_request_delete_raises_exception_if_not_200_response(status): 202 | responses.add( 203 | responses.DELETE, BASE_URL, status=status, content_type="application/json" 204 | ) 205 | 206 | api = TwitchAPI(client_id="client") 207 | 208 | with pytest.raises(exceptions.HTTPError): 209 | api._request_delete("") 210 | 211 | 212 | @responses.activate 213 | def test_request_post_returns_dictionary_if_successful(): 214 | responses.add( 215 | responses.POST, 216 | BASE_URL, 217 | body=json.dumps(dummy_data), 218 | status=200, 219 | content_type="application/json", 220 | ) 221 | 222 | api = TwitchAPI(client_id="client") 223 | response = api._request_post("", dummy_data) 224 | 225 | assert isinstance(response, dict) 226 | assert response == dummy_data 227 | 228 | 229 | @responses.activate 230 | def test_request_post_does_not_raise_exception_if_successful(): 231 | responses.add(responses.POST, BASE_URL, status=204, content_type="application/json") 232 | 233 | api = TwitchAPI(client_id="client") 234 | api._request_post("") 235 | 236 | 237 | @responses.activate 238 | def test_request_post_sends_headers_with_the_request(): 239 | responses.add( 240 | responses.POST, 241 | BASE_URL, 242 | body=json.dumps(dummy_data), 243 | status=200, 244 | content_type="application/json", 245 | ) 246 | 247 | api = TwitchAPI(client_id="client") 248 | api._request_post("", dummy_data) 249 | 250 | assert "Client-ID" in responses.calls[0].request.headers 251 | assert "Accept" in responses.calls[0].request.headers 252 | 253 | 254 | @responses.activate 255 | @pytest.mark.parametrize("status", [(500), (400)]) 256 | def test_request_post_raises_exception_if_not_200_response(status): 257 | responses.add( 258 | responses.POST, BASE_URL, status=status, content_type="application/json" 259 | ) 260 | 261 | api = TwitchAPI(client_id="client") 262 | 263 | with pytest.raises(exceptions.HTTPError): 264 | api._request_post("", dummy_data) 265 | 266 | 267 | def test_base_reads_backoff_config_from_file(monkeypatch): 268 | def mockreturn(path): 269 | return "tests/api/dummy_credentials.cfg" 270 | 271 | monkeypatch.setattr(os.path, "expanduser", mockreturn) 272 | 273 | base = TwitchAPI(client_id="client") 274 | 275 | assert isinstance(base._initial_backoff, float) 276 | assert isinstance(base._max_retries, int) 277 | assert base._initial_backoff == 0.01 278 | assert base._max_retries == 1 279 | -------------------------------------------------------------------------------- /tests/api/test_channel_feed.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | import responses 5 | 6 | from twitch.client import TwitchClient 7 | from twitch.constants import BASE_URL 8 | from twitch.exceptions import TwitchAttributeException 9 | from twitch.resources import Comment, Post 10 | 11 | # Note: comment doesn't have id prefixed with _ for some reason 12 | example_comment = { 13 | "id": "132629", 14 | "body": "Hey there! KappaHD", 15 | "created_at": "2016-11-29T15:52:27Z", 16 | } 17 | 18 | # Note: post doesn't have id prefixed with _ for some reason 19 | example_post = { 20 | "id": "443228891479487861", 21 | "body": "News feed post!", 22 | "comments": { 23 | "_cursor": "1480434747093939000", 24 | "_total": 1, 25 | "comments": [example_comment], 26 | }, 27 | "created_at": "2016-11-18T16:51:01Z", 28 | } 29 | 30 | 31 | @responses.activate 32 | def test_get_posts(): 33 | channel_id = "1234" 34 | response = { 35 | "_cursor": "1479487861147094000", 36 | "_topic": "feeds.channel.44322889", 37 | "posts": [example_post], 38 | } 39 | responses.add( 40 | responses.GET, 41 | "{}feed/{}/posts".format(BASE_URL, channel_id), 42 | body=json.dumps(response), 43 | status=200, 44 | content_type="application/json", 45 | ) 46 | 47 | client = TwitchClient("client id") 48 | 49 | posts = client.channel_feed.get_posts(channel_id) 50 | 51 | assert len(responses.calls) == 1 52 | assert len(posts) == 1 53 | post = posts[0] 54 | assert isinstance(post, Post) 55 | assert post.id == example_post["id"] 56 | assert post.body == example_post["body"] 57 | 58 | 59 | @responses.activate 60 | @pytest.mark.parametrize("param,value", [("limit", 101), ("comments", 6)]) 61 | def test_get_posts_raises_if_wrong_params_are_passed_in(param, value): 62 | client = TwitchClient("client id") 63 | kwargs = {param: value} 64 | with pytest.raises(TwitchAttributeException): 65 | client.channel_feed.get_posts("1234", **kwargs) 66 | 67 | 68 | @responses.activate 69 | def test_get_post(): 70 | channel_id = "1234" 71 | post_id = example_post["id"] 72 | responses.add( 73 | responses.GET, 74 | "{}feed/{}/posts/{}".format(BASE_URL, channel_id, post_id), 75 | body=json.dumps(example_post), 76 | status=200, 77 | content_type="application/json", 78 | ) 79 | 80 | client = TwitchClient("client id") 81 | 82 | post = client.channel_feed.get_post(channel_id, post_id) 83 | 84 | assert len(responses.calls) == 1 85 | assert isinstance(post, Post) 86 | assert post.id == example_post["id"] 87 | assert post.body == example_post["body"] 88 | 89 | 90 | @responses.activate 91 | @pytest.mark.parametrize("param,value", [("comments", 6)]) 92 | def test_get_post_raises_if_wrong_params_are_passed_in(param, value): 93 | client = TwitchClient("client id") 94 | kwargs = {param: value} 95 | with pytest.raises(TwitchAttributeException): 96 | client.channel_feed.get_post("1234", example_post["id"], **kwargs) 97 | 98 | 99 | @responses.activate 100 | def test_create_post(): 101 | channel_id = "1234" 102 | response = {"post": example_post, "tweet": None} 103 | responses.add( 104 | responses.POST, 105 | "{}feed/{}/posts".format(BASE_URL, channel_id), 106 | body=json.dumps(response), 107 | status=200, 108 | content_type="application/json", 109 | ) 110 | 111 | client = TwitchClient("client id", "oauth token") 112 | 113 | post = client.channel_feed.create_post(channel_id, "abcd") 114 | 115 | assert len(responses.calls) == 1 116 | assert isinstance(post, Post) 117 | assert post.id == example_post["id"] 118 | assert post.body == example_post["body"] 119 | 120 | 121 | @responses.activate 122 | def test_delete_post(): 123 | channel_id = "1234" 124 | post_id = example_post["id"] 125 | responses.add( 126 | responses.DELETE, 127 | "{}feed/{}/posts/{}".format(BASE_URL, channel_id, post_id), 128 | body=json.dumps(example_post), 129 | status=200, 130 | content_type="application/json", 131 | ) 132 | 133 | client = TwitchClient("client id", "oauth token") 134 | 135 | post = client.channel_feed.delete_post(channel_id, post_id) 136 | 137 | assert len(responses.calls) == 1 138 | assert isinstance(post, Post) 139 | assert post.id == example_post["id"] 140 | 141 | 142 | @responses.activate 143 | def test_create_reaction_to_post(): 144 | channel_id = "1234" 145 | post_id = example_post["id"] 146 | response = { 147 | "id": "24989127", 148 | "emote_id": "25", 149 | "user": {}, 150 | "created_at": "2016-11-29T15:51:12Z", 151 | } 152 | responses.add( 153 | responses.POST, 154 | "{}feed/{}/posts/{}/reactions".format(BASE_URL, channel_id, post_id), 155 | body=json.dumps(response), 156 | status=200, 157 | content_type="application/json", 158 | ) 159 | 160 | client = TwitchClient("client id", "oauth token") 161 | 162 | response = client.channel_feed.create_reaction_to_post( 163 | channel_id, post_id, response["emote_id"] 164 | ) 165 | 166 | assert len(responses.calls) == 1 167 | assert response["id"] 168 | 169 | 170 | @responses.activate 171 | def test_delete_reaction_to_post(): 172 | channel_id = "1234" 173 | post_id = example_post["id"] 174 | body = {"deleted": True} 175 | responses.add( 176 | responses.DELETE, 177 | "{}feed/{}/posts/{}/reactions".format(BASE_URL, channel_id, post_id), 178 | body=json.dumps(body), 179 | status=200, 180 | content_type="application/json", 181 | ) 182 | 183 | client = TwitchClient("client id", "oauth token") 184 | 185 | response = client.channel_feed.delete_reaction_to_post(channel_id, post_id, "1234") 186 | 187 | assert len(responses.calls) == 1 188 | assert response["deleted"] 189 | 190 | 191 | @responses.activate 192 | def test_get_post_comments(): 193 | channel_id = "1234" 194 | post_id = example_post["id"] 195 | response = { 196 | "_cursor": "1480651694954867000", 197 | "_total": 1, 198 | "comments": [example_comment], 199 | } 200 | responses.add( 201 | responses.GET, 202 | "{}feed/{}/posts/{}/comments".format(BASE_URL, channel_id, post_id), 203 | body=json.dumps(response), 204 | status=200, 205 | content_type="application/json", 206 | ) 207 | 208 | client = TwitchClient("client id") 209 | 210 | comments = client.channel_feed.get_post_comments(channel_id, post_id) 211 | 212 | assert len(responses.calls) == 1 213 | assert len(comments) == 1 214 | comment = comments[0] 215 | assert isinstance(comment, Comment) 216 | assert comment.id == example_comment["id"] 217 | assert comment.body == example_comment["body"] 218 | 219 | 220 | @responses.activate 221 | @pytest.mark.parametrize("param,value", [("limit", 101)]) 222 | def test_get_post_comments_raises_if_wrong_params_are_passed_in(param, value): 223 | client = TwitchClient("client id") 224 | kwargs = {param: value} 225 | with pytest.raises(TwitchAttributeException): 226 | client.channel_feed.get_post_comments("1234", example_post["id"], **kwargs) 227 | 228 | 229 | @responses.activate 230 | def test_create_post_comment(): 231 | channel_id = "1234" 232 | post_id = example_post["id"] 233 | responses.add( 234 | responses.POST, 235 | "{}feed/{}/posts/{}/comments".format(BASE_URL, channel_id, post_id), 236 | body=json.dumps(example_comment), 237 | status=200, 238 | content_type="application/json", 239 | ) 240 | 241 | client = TwitchClient("client id", "oauth token") 242 | 243 | comment = client.channel_feed.create_post_comment(channel_id, post_id, "abcd") 244 | 245 | assert len(responses.calls) == 1 246 | assert isinstance(comment, Comment) 247 | assert comment.id == example_comment["id"] 248 | assert comment.body == example_comment["body"] 249 | 250 | 251 | @responses.activate 252 | def test_delete_post_comment(): 253 | channel_id = "1234" 254 | post_id = example_post["id"] 255 | comment_id = example_comment["id"] 256 | responses.add( 257 | responses.DELETE, 258 | "{}feed/{}/posts/{}/comments/{}".format( 259 | BASE_URL, channel_id, post_id, comment_id 260 | ), 261 | body=json.dumps(example_comment), 262 | status=200, 263 | content_type="application/json", 264 | ) 265 | 266 | client = TwitchClient("client id", "oauth token") 267 | 268 | comment = client.channel_feed.delete_post_comment(channel_id, post_id, comment_id) 269 | 270 | assert len(responses.calls) == 1 271 | assert isinstance(comment, Comment) 272 | assert comment.id == example_comment["id"] 273 | 274 | 275 | @responses.activate 276 | def test_create_reaction_to_comment(): 277 | channel_id = "1234" 278 | post_id = example_post["id"] 279 | comment_id = example_comment["id"] 280 | body = { 281 | "created_at": "2016-12-02T04:26:47Z", 282 | "emote_id": "1", 283 | "id": "1341393b-e872-4554-9f6f-acd5f8b669fc", 284 | "user": {}, 285 | } 286 | url = "{}feed/{}/posts/{}/comments/{}/reactions".format( 287 | BASE_URL, channel_id, post_id, comment_id 288 | ) 289 | responses.add( 290 | responses.POST, 291 | url, 292 | body=json.dumps(body), 293 | status=200, 294 | content_type="application/json", 295 | ) 296 | 297 | client = TwitchClient("client id", "oauth token") 298 | 299 | response = client.channel_feed.create_reaction_to_comment( 300 | channel_id, post_id, comment_id, "1" 301 | ) 302 | 303 | assert len(responses.calls) == 1 304 | assert response["id"] 305 | 306 | 307 | @responses.activate 308 | def test_delete_reaction_to_comment(): 309 | channel_id = "1234" 310 | post_id = example_post["id"] 311 | comment_id = example_comment["id"] 312 | body = {"deleted": True} 313 | url = "{}feed/{}/posts/{}/comments/{}/reactions".format( 314 | BASE_URL, channel_id, post_id, comment_id 315 | ) 316 | responses.add( 317 | responses.DELETE, 318 | url, 319 | body=json.dumps(body), 320 | status=200, 321 | content_type="application/json", 322 | ) 323 | 324 | client = TwitchClient("client id", "oauth token") 325 | 326 | response = client.channel_feed.delete_reaction_to_comment( 327 | channel_id, post_id, comment_id, "1" 328 | ) 329 | 330 | assert len(responses.calls) == 1 331 | assert response["deleted"] 332 | -------------------------------------------------------------------------------- /docs/helix.rst: -------------------------------------------------------------------------------- 1 | Twitch Helix 2 | ============ 3 | 4 | Helix is the latest version of Twitch API 5 | 6 | .. currentmodule:: twitch.helix 7 | 8 | .. class:: TwitchHelix(client_id=None, oauth_token=None client_secret=None, scopes=None) 9 | 10 | This class provides methods for easy access to `Twitch Helix API`_. 11 | 12 | :param string client_id: Client ID you get from your registered app on Twitch 13 | :param string oauth_token: OAuth token, if you already have it, otherwise use ``client_secret`` and ``scopes`` then call ``get_oauth`` to generate it 14 | :param string client_secret: Client secret. Only used by ``get_oauth`` and should only be present if oauth_token is not set 15 | :param string scopes: Twitch scopes that we want the OAuth token to have. Only used by ``get_oauth`` and should only be present if oauth_token is not set 16 | 17 | 18 | Basic usage with oauth_token set: 19 | 20 | .. code-block:: python 21 | 22 | import twitch 23 | client = twitch.TwitchHelix(client_id='', oauth_token='') 24 | client.get_streams() 25 | 26 | 27 | Basic usage with fetching the oauth token on initialization: 28 | 29 | .. code-block:: python 30 | 31 | import twitch 32 | client = twitch.TwitchHelix(client_id='', client_secret='', scopes=[twitch.constants.OAUTH_SCOPE_ANALYTICS_READ_EXTENSIONS]) 33 | client.get_oauth() 34 | client.get_streams() 35 | 36 | 37 | .. classmethod:: get_oauth(clip_id) 38 | 39 | Gets access token with access to the requested scopes and stores the token on the class for future usage 40 | 41 | 42 | .. classmethod:: get_clips(broadcaster_id=None, game_id=None, clip_ids=None, after=None, before=None, started_at=None, ended_at=None, page_size=20) 43 | 44 | Gets clip information by clip ID (one or more), broadcaster ID (one only), or game ID (one only). 45 | 46 | :param string broadcaster_id: Broadcaster ID for whom clips are returned. The number of clips returned is determined by the ``page_size`` parameter (Default: 20 Max: 100). Results are ordered by view count. 47 | :param string game_id: Game ID for which clips are returned. The number of clips returned is determined by the ``page_size`` parameter (Default: 20 Max: 100). Results are ordered by view count. 48 | :param list clip_ids: List of clip IDS being queried. Limit: 100. 49 | :param string after (optional): Cursor for forward pagination: tells the server where to start fetching the next set of results, in a multi-page response. 50 | :param string before (optional): Cursor for backward pagination: tells the server where to start fetching the next set of results, in a multi-page response. 51 | :param string started_at (optional): Starting date/time for returned clips, in RFC3339 format. (The seconds value is ignored.) If this is specified, ended_at also should be specified; otherwise, the ended_at date/time will be 1 week after the started_at value. 52 | :param string ended_at (optional): Ending date/time for returned clips, in RFC3339 format. (Note that the seconds value is ignored.) If this is specified, started_at also must be specified; otherwise, the time period is ignored. 53 | :param integer page_size (optional): Number of objects returned in one call. Maximum: 100. Default: 20. 54 | :return: :class:`~twitch.helix.APICursor` if ``broadcaster_id`` or ``game_ids`` are provided, returns list of :class:`~twitch.resources.Clip` objects instead. 55 | 56 | For response fields of ``get_clips`` and official documentation check `Twitch Helix Get Clips`_. 57 | 58 | 59 | .. classmethod:: get_games(game_ids=None, names=None) 60 | 61 | Gets game information by game ID or name. 62 | 63 | :param list game_ids: List of Game IDs. At most 100 id values can be specified. 64 | :param list names: List of Game names. The name must be an exact match. For instance, "Pokemon" will not return a list of Pokemon games; instead, query the specific Pokemon game(s) in which you are interested. At most 100 name values can be specified. 65 | :return: :class:`~twitch.helix.APICursor` containing :class:`~twitch.resources.Game` objects 66 | 67 | For response fields of ``get_games`` and official documentation check `Twitch Helix Get Games`_. 68 | 69 | 70 | .. classmethod:: get_streams(after=None, before=None, community_ids=None, page_size=20, game_ids=None, languages=None, stream_type=None, user_ids=None, user_logins=None) 71 | 72 | Gets information about active streams. Streams are returned sorted by number of current viewers, in descending order. Across multiple pages of results, there may be duplicate or missing streams, as viewers join and leave streams. 73 | 74 | :param string after: Cursor for forward pagination: tells the server where to start fetching the next set of results, in a multi-page response. 75 | :param string before: Cursor for backward pagination: tells the server where to start fetching the next set of results, in a multi-page response. 76 | :param list community_ids: Returns streams in a specified community ID. You can specify up to 100 IDs. 77 | :param integer page_size: Number of objects returned in one call. Maximum: 100. Default: 20. 78 | :param list game_ids: Returns streams broadcasting a specified game ID. You can specify up to 100 IDs. 79 | :param list languages: Stream language. You can specify up to 100 languages 80 | :param list user_ids: Returns streams broadcast by one or more specified user IDs. You can specify up to 100 IDs. 81 | :param list user_logins: Returns streams broadcast by one or more specified user login names. You can specify up to 100 names. 82 | :return: :class:`~twitch.helix.APICursor` containing :class:`~twitch.resources.Stream` objects 83 | 84 | For response fields of ``get_streams`` and official documentation check `Twitch Helix Get Streams`_. 85 | 86 | 87 | .. classmethod:: get_top_games(after=None, before=None, page_size=20) 88 | 89 | Gets games sorted by number of current viewers on Twitch, most popular first. 90 | 91 | :param string after: Cursor for forward pagination: tells the server where to start fetching the next set of results, in a multi-page response. 92 | :param string before: Cursor for backward pagination: tells the server where to start fetching the next set of results, in a multi-page response. 93 | :param integer page_size: Number of objects returned in one call. Maximum: 100. Default: 20. 94 | :return: :class:`~twitch.helix.APICursor` containing :class:`~twitch.resources.Game` objects 95 | 96 | For response fields of ``get_top_games`` and official documentation check `Twitch Helix Get Top Games`_. 97 | 98 | 99 | .. classmethod:: get_videos(video_ids=None, user_id=None, game_id=None, after=None, before=None, page_size=20, language=None, period=None, sort=None, video_type=None) 100 | 101 | Gets video information by video ID (one or more), user ID (one only), or game ID (one only). 102 | 103 | :param list video_ids: List of Video IDs. Limit: 100. If this is specified, you cannot use any of the optional query string parameters below. 104 | :param string user_id: User ID who owns the videos. 105 | :param int game_id: Game ID of the videos. 106 | :param string after (optional): Cursor for forward pagination: tells the server where to start fetching the next set of results, in a multi-page response. 107 | :param string before (optional): Cursor for backward pagination: tells the server where to start fetching the next set of results, in a multi-page response. 108 | :param integer page_size (optional): Number of objects returned in one call. Maximum: 100. Default: 20. 109 | :param string language (optional): Language of the video being queried. 110 | :param string period (optional): Period during which the video was created. Valid values: ``VIDEO_PERIODS``. Default: ``VIDEO_PERIOD_ALL`` 111 | :param string sort (optional): Sort order of the videos. Valid values: ``VIDEO_SORTS``. Default: ``VIDEO_SORT_TIME`` 112 | :param string type (optional): Type of video. Valid values: ``VIDEO_TYPES``. Default: ``VIDEO_TYPE_ALL`` 113 | 114 | :return: :class:`~twitch.helix.APICursor` if ``user_id`` or ``game_id`` are provided, returns list of :class:`~twitch.resources.Video` objects instead. 115 | 116 | For response fields of ``get_videos`` and official documentation check `Twitch Helix Get Videos`_. 117 | 118 | 119 | .. classmethod:: get_streams_metadata(after=None, before=None, community_ids=None, page_size=20, game_ids=None, languages=None, stream_type=None, user_ids=None, user_logins=None) 120 | 121 | Gets metadata information about active streams playing Overwatch or Hearthstone. Streams are sorted by number of current viewers, in descending order. Across multiple pages of results, there may be duplicate or missing streams, as viewers join and leave streams. 122 | 123 | :param string after: Cursor for forward pagination: tells the server where to start fetching the next set of results, in a multi-page response. 124 | :param string before: Cursor for backward pagination: tells the server where to start fetching the next set of results, in a multi-page response. 125 | :param list community_ids: Returns streams in a specified community ID. You can specify up to 100 IDs. 126 | :param integer page_size: Number of objects returned in one call. Maximum: 100. Default: 20. 127 | :param list game_ids: Returns streams broadcasting a specified game ID. You can specify up to 100 IDs. 128 | :param list languages: Stream language. You can specify up to 100 languages 129 | :param list user_ids: Returns streams broadcast by one or more specified user IDs. You can specify up to 100 IDs. 130 | :param list user_logins: Returns streams broadcast by one or more specified user login names. You can specify up to 100 names. 131 | :return: :class:`~twitch.helix.APICursor` containing :class:`~twitch.resources.StreamMetadata` objects 132 | 133 | For response fields of ``get_streams`` and official documentation check `Twitch Helix Get Streams Metadata`_. 134 | 135 | 136 | .. classmethod:: get_user_follows(after=None, page_size=20, from_id=None, to_id=None) 137 | 138 | Gets information on follow relationships between two Twitch users. Information returned is sorted in order, most recent follow first. This can return information like “who is lirik following,” “who is following lirik,” or “is user X following user Y.” 139 | 140 | :param string after: Cursor for forward pagination: tells the server where to start fetching the next set of results, in a multi-page response. 141 | :param integer page_size: Number of objects returned in one call. Maximum: 100. Default: 20. 142 | :param list from_id: User ID. The request returns information about users who are being followed by the ``from_id`` user. 143 | :param list to_id: User ID. The request returns information about users who are following the ``to_id`` user. 144 | :return: :class:`~twitch.helix.APICursor` containing :class:`~twitch.resources.Follow` objects 145 | 146 | For response fields of ``get_streams`` and official documentation check `Twitch Helix Get Users Follows`_. 147 | 148 | 149 | .. _`Twitch Helix API`: https://dev.twitch.tv/docs/api/reference 150 | .. _`Twitch Helix Get Streams`: https://dev.twitch.tv/docs/api/reference/#get-streams 151 | .. _`Twitch Helix Get Games`: https://dev.twitch.tv/docs/api/reference/#get-games 152 | .. _`Twitch Helix Get Clips`: https://dev.twitch.tv/docs/api/reference/#get-clips 153 | .. _`Twitch Helix Get Top Games`: https://dev.twitch.tv/docs/api/reference/#get-top-games 154 | .. _`Twitch Helix Get Videos`: https://dev.twitch.tv/docs/api/reference/#get-videos 155 | .. _`Twitch Helix Get Streams Metadata`: https://dev.twitch.tv/docs/api/reference/#get-streams-metadata 156 | .. _`Twitch Helix Get Users Follows`: https://dev.twitch.tv/docs/api/reference/#get-users-follows 157 | -------------------------------------------------------------------------------- /tests/api/test_users.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | import responses 5 | 6 | from twitch.client import TwitchClient 7 | from twitch.constants import BASE_URL 8 | from twitch.exceptions import TwitchAttributeException 9 | from twitch.resources import Channel, Follow, Subscription, User, UserBlock 10 | 11 | example_user = { 12 | "_id": "44322889", 13 | "name": "dallas", 14 | } 15 | 16 | example_channel = { 17 | "_id": 121059319, 18 | "name": "moonmoon_ow", 19 | } 20 | 21 | example_follow = { 22 | "created_at": "2016-09-16T20:37:39Z", 23 | "notifications": False, 24 | "channel": example_channel, 25 | } 26 | 27 | example_block = { 28 | "_id": 34105660, 29 | "updated_at": "2016-12-15T18:58:11Z", 30 | "user": example_user, 31 | } 32 | 33 | 34 | @responses.activate 35 | def test_get(): 36 | responses.add( 37 | responses.GET, 38 | "{}user".format(BASE_URL), 39 | body=json.dumps(example_user), 40 | status=200, 41 | content_type="application/json", 42 | ) 43 | 44 | client = TwitchClient("client id", "oauth token") 45 | 46 | user = client.users.get() 47 | 48 | assert len(responses.calls) == 1 49 | assert isinstance(user, User) 50 | assert user.id == example_user["_id"] 51 | assert user.name == example_user["name"] 52 | 53 | 54 | @responses.activate 55 | def test_get_by_id(): 56 | user_id = 1234 57 | responses.add( 58 | responses.GET, 59 | "{}users/{}".format(BASE_URL, user_id), 60 | body=json.dumps(example_user), 61 | status=200, 62 | content_type="application/json", 63 | ) 64 | 65 | client = TwitchClient("client id") 66 | 67 | user = client.users.get_by_id(user_id) 68 | 69 | assert len(responses.calls) == 1 70 | assert isinstance(user, User) 71 | assert user.id == example_user["_id"] 72 | assert user.name == example_user["name"] 73 | 74 | 75 | @responses.activate 76 | def test_get_emotes(): 77 | user_id = 1234 78 | response = {"emoticon_sets": {"17937": [{"code": "Kappa", "id": 25}]}} 79 | responses.add( 80 | responses.GET, 81 | "{}users/{}/emotes".format(BASE_URL, user_id), 82 | body=json.dumps(response), 83 | status=200, 84 | content_type="application/json", 85 | ) 86 | 87 | client = TwitchClient("client id", "oauth token") 88 | 89 | emotes = client.users.get_emotes(user_id) 90 | 91 | assert len(responses.calls) == 1 92 | assert isinstance(emotes, dict) 93 | assert emotes["17937"] == response["emoticon_sets"]["17937"] 94 | 95 | 96 | @responses.activate 97 | def test_check_subscribed_to_channel(): 98 | user_id = 1234 99 | channel_id = 12345 100 | response = { 101 | "_id": "c660cb408bc3b542f5bdbba52f3e638e652756b4", 102 | "created_at": "2016-12-12T15:52:52Z", 103 | "channel": example_channel, 104 | } 105 | responses.add( 106 | responses.GET, 107 | "{}users/{}/subscriptions/{}".format(BASE_URL, user_id, channel_id), 108 | body=json.dumps(response), 109 | status=200, 110 | content_type="application/json", 111 | ) 112 | 113 | client = TwitchClient("client id", "oauth token") 114 | 115 | subscription = client.users.check_subscribed_to_channel(user_id, channel_id) 116 | 117 | assert len(responses.calls) == 1 118 | assert isinstance(subscription, Subscription) 119 | assert subscription.id == response["_id"] 120 | assert isinstance(subscription.channel, Channel) 121 | assert subscription.channel.id == example_channel["_id"] 122 | assert subscription.channel.name == example_channel["name"] 123 | 124 | 125 | @responses.activate 126 | def test_get_follows(): 127 | user_id = 1234 128 | response = {"_total": 27, "follows": [example_follow]} 129 | responses.add( 130 | responses.GET, 131 | "{}users/{}/follows/channels".format(BASE_URL, user_id), 132 | body=json.dumps(response), 133 | status=200, 134 | content_type="application/json", 135 | ) 136 | 137 | client = TwitchClient("client id") 138 | 139 | follows = client.users.get_follows(user_id) 140 | 141 | assert len(responses.calls) == 1 142 | assert len(follows) == 1 143 | follow = follows[0] 144 | assert isinstance(follow, Follow) 145 | assert follow.notifications == example_follow["notifications"] 146 | assert isinstance(follow.channel, Channel) 147 | assert follow.channel.id == example_channel["_id"] 148 | assert follow.channel.name == example_channel["name"] 149 | 150 | 151 | @responses.activate 152 | @pytest.mark.parametrize( 153 | "param,value", [("limit", 101), ("direction", "abcd"), ("sort_by", "abcd")] 154 | ) 155 | def test_get_follows_raises_if_wrong_params_are_passed_in(param, value): 156 | client = TwitchClient("client id") 157 | kwargs = {param: value} 158 | with pytest.raises(TwitchAttributeException): 159 | client.users.get_follows("1234", **kwargs) 160 | 161 | 162 | @responses.activate 163 | def test_get_all_follows(): 164 | user_id = 1234 165 | response_with_offset = {"_total": 27, "_offset": 1234, "follows": [example_follow]} 166 | response_without_offset = {"_total": 27, "follows": [example_follow]} 167 | responses.add( 168 | responses.GET, 169 | "{}users/{}/follows/channels".format(BASE_URL, user_id), 170 | body=json.dumps(response_with_offset), 171 | status=200, 172 | content_type="application/json", 173 | ) 174 | responses.add( 175 | responses.GET, 176 | "{}users/{}/follows/channels".format(BASE_URL, user_id), 177 | body=json.dumps(response_without_offset), 178 | status=200, 179 | content_type="application/json", 180 | ) 181 | 182 | client = TwitchClient("client id") 183 | 184 | follows = client.users.get_all_follows( 185 | user_id, direction="desc", sort_by="last_broadcast" 186 | ) 187 | 188 | assert len(responses.calls) == 2 189 | assert len(follows) == 2 190 | follow = follows[0] 191 | assert isinstance(follow, Follow) 192 | assert follow.notifications == example_follow["notifications"] 193 | assert isinstance(follow.channel, Channel) 194 | assert follow.channel.id == example_channel["_id"] 195 | assert follow.channel.name == example_channel["name"] 196 | 197 | 198 | @responses.activate 199 | @pytest.mark.parametrize("param,value", [("direction", "abcd"), ("sort_by", "abcd")]) 200 | def test_get_all_follows_raises_if_wrong_params_are_passed_in(param, value): 201 | client = TwitchClient("client id") 202 | kwargs = {param: value} 203 | with pytest.raises(TwitchAttributeException): 204 | client.users.get_all_follows("1234", **kwargs) 205 | 206 | 207 | @responses.activate 208 | def test_check_follows_channel(): 209 | user_id = 1234 210 | channel_id = 12345 211 | responses.add( 212 | responses.GET, 213 | "{}users/{}/follows/channels/{}".format(BASE_URL, user_id, channel_id), 214 | body=json.dumps(example_follow), 215 | status=200, 216 | content_type="application/json", 217 | ) 218 | 219 | client = TwitchClient("client id") 220 | 221 | follow = client.users.check_follows_channel(user_id, channel_id) 222 | 223 | assert len(responses.calls) == 1 224 | assert isinstance(follow, Follow) 225 | assert follow.notifications == example_follow["notifications"] 226 | assert isinstance(follow.channel, Channel) 227 | assert follow.channel.id == example_channel["_id"] 228 | assert follow.channel.name == example_channel["name"] 229 | 230 | 231 | @responses.activate 232 | def test_follow_channel(): 233 | user_id = 1234 234 | channel_id = 12345 235 | responses.add( 236 | responses.PUT, 237 | "{}users/{}/follows/channels/{}".format(BASE_URL, user_id, channel_id), 238 | body=json.dumps(example_follow), 239 | status=200, 240 | content_type="application/json", 241 | ) 242 | 243 | client = TwitchClient("client id", "oauth token") 244 | 245 | follow = client.users.follow_channel(user_id, channel_id) 246 | 247 | assert len(responses.calls) == 1 248 | assert isinstance(follow, Follow) 249 | assert follow.notifications == example_follow["notifications"] 250 | assert isinstance(follow.channel, Channel) 251 | assert follow.channel.id == example_channel["_id"] 252 | assert follow.channel.name == example_channel["name"] 253 | 254 | 255 | @responses.activate 256 | def test_unfollow_channel(): 257 | user_id = 1234 258 | channel_id = 12345 259 | responses.add( 260 | responses.DELETE, 261 | "{}users/{}/follows/channels/{}".format(BASE_URL, user_id, channel_id), 262 | status=204, 263 | content_type="application/json", 264 | ) 265 | 266 | client = TwitchClient("client id", "oauth token") 267 | 268 | client.users.unfollow_channel(user_id, channel_id) 269 | 270 | assert len(responses.calls) == 1 271 | 272 | 273 | @responses.activate 274 | def test_get_user_block_list(): 275 | user_id = 1234 276 | response = {"_total": 4, "blocks": [example_block]} 277 | responses.add( 278 | responses.GET, 279 | "{}users/{}/blocks".format(BASE_URL, user_id), 280 | body=json.dumps(response), 281 | status=200, 282 | content_type="application/json", 283 | ) 284 | 285 | client = TwitchClient("client id", "oauth token") 286 | 287 | block_list = client.users.get_user_block_list(user_id) 288 | 289 | assert len(responses.calls) == 1 290 | assert len(block_list) == 1 291 | block = block_list[0] 292 | assert isinstance(block, UserBlock) 293 | assert block.id == example_block["_id"] 294 | assert isinstance(block.user, User) 295 | assert block.user.id == example_user["_id"] 296 | assert block.user.name == example_user["name"] 297 | 298 | 299 | @responses.activate 300 | @pytest.mark.parametrize("param,value", [("limit", 101)]) 301 | def test_get_user_block_list_raises_if_wrong_params_are_passed_in(param, value): 302 | client = TwitchClient("client id", "oauth token") 303 | kwargs = {param: value} 304 | with pytest.raises(TwitchAttributeException): 305 | client.users.get_user_block_list("1234", **kwargs) 306 | 307 | 308 | @responses.activate 309 | def test_block_user(): 310 | user_id = 1234 311 | blocked_user_id = 12345 312 | responses.add( 313 | responses.PUT, 314 | "{}users/{}/blocks/{}".format(BASE_URL, user_id, blocked_user_id), 315 | body=json.dumps(example_block), 316 | status=200, 317 | content_type="application/json", 318 | ) 319 | 320 | client = TwitchClient("client id", "oauth token") 321 | 322 | block = client.users.block_user(user_id, blocked_user_id) 323 | 324 | assert len(responses.calls) == 1 325 | assert isinstance(block, UserBlock) 326 | assert block.id == example_block["_id"] 327 | assert isinstance(block.user, User) 328 | assert block.user.id == example_user["_id"] 329 | assert block.user.name == example_user["name"] 330 | 331 | 332 | @responses.activate 333 | def test_unblock_user(): 334 | user_id = 1234 335 | blocked_user_id = 12345 336 | responses.add( 337 | responses.DELETE, 338 | "{}users/{}/blocks/{}".format(BASE_URL, user_id, blocked_user_id), 339 | body=json.dumps(example_block), 340 | status=200, 341 | content_type="application/json", 342 | ) 343 | 344 | client = TwitchClient("client id", "oauth token") 345 | 346 | client.users.unblock_user(user_id, blocked_user_id) 347 | 348 | assert len(responses.calls) == 1 349 | 350 | 351 | @responses.activate 352 | def test_translate_usernames_to_ids(): 353 | response = {"users": [example_user]} 354 | responses.add( 355 | responses.GET, 356 | "{}users".format(BASE_URL), 357 | body=json.dumps(response), 358 | status=200, 359 | content_type="application/json", 360 | ) 361 | 362 | client = TwitchClient("client id", "oauth token") 363 | 364 | users = client.users.translate_usernames_to_ids(["lirik"]) 365 | 366 | assert len(responses.calls) == 1 367 | assert len(users) == 1 368 | user = users[0] 369 | assert isinstance(user, User) 370 | assert user.id == example_user["_id"] 371 | assert user.name == example_user["name"] 372 | -------------------------------------------------------------------------------- /tests/api/test_communities.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | import responses 5 | 6 | from twitch.client import TwitchClient 7 | from twitch.constants import BASE_URL 8 | from twitch.exceptions import TwitchAttributeException 9 | from twitch.resources import Community, User 10 | 11 | example_community = { 12 | "_id": "e9f17055-810f-4736-ba40-fba4ac541caa", 13 | "name": "DallasTesterCommunity", 14 | } 15 | 16 | example_user = { 17 | "_id": "44322889", 18 | "name": "dallas", 19 | } 20 | 21 | 22 | @responses.activate 23 | def test_get_by_name(): 24 | responses.add( 25 | responses.GET, 26 | "{}communities".format(BASE_URL), 27 | body=json.dumps(example_community), 28 | status=200, 29 | content_type="application/json", 30 | ) 31 | 32 | client = TwitchClient("client id") 33 | 34 | community = client.communities.get_by_name("spongebob") 35 | 36 | assert len(responses.calls) == 1 37 | assert isinstance(community, Community) 38 | assert community.id == example_community["_id"] 39 | assert community.name == example_community["name"] 40 | 41 | 42 | @responses.activate 43 | def test_get_by_id(): 44 | community_id = "abcd" 45 | responses.add( 46 | responses.GET, 47 | "{}communities/{}".format(BASE_URL, community_id), 48 | body=json.dumps(example_community), 49 | status=200, 50 | content_type="application/json", 51 | ) 52 | 53 | client = TwitchClient("client id") 54 | 55 | community = client.communities.get_by_id(community_id) 56 | 57 | assert len(responses.calls) == 1 58 | assert isinstance(community, Community) 59 | assert community.id == example_community["_id"] 60 | assert community.name == example_community["name"] 61 | 62 | 63 | @responses.activate 64 | def test_update(): 65 | community_id = "abcd" 66 | responses.add( 67 | responses.PUT, 68 | "{}communities/{}".format(BASE_URL, community_id), 69 | body=json.dumps(example_community), 70 | status=204, 71 | content_type="application/json", 72 | ) 73 | 74 | client = TwitchClient("client id") 75 | 76 | client.communities.update(community_id) 77 | 78 | assert len(responses.calls) == 1 79 | 80 | 81 | @responses.activate 82 | def test_get_top(): 83 | response = {"_cursor": "MTA=", "_total": 100, "communities": [example_community]} 84 | 85 | responses.add( 86 | responses.GET, 87 | "{}communities/top".format(BASE_URL), 88 | body=json.dumps(response), 89 | status=200, 90 | content_type="application/json", 91 | ) 92 | 93 | client = TwitchClient("client id") 94 | 95 | communities = client.communities.get_top() 96 | 97 | assert len(responses.calls) == 1 98 | assert len(communities) == 1 99 | community = communities[0] 100 | assert isinstance(community, Community) 101 | assert community.id == example_community["_id"] 102 | assert community.name == example_community["name"] 103 | 104 | 105 | @responses.activate 106 | @pytest.mark.parametrize("param,value", [("limit", 101)]) 107 | def test_get_top_raises_if_wrong_params_are_passed_in(param, value): 108 | client = TwitchClient("client id") 109 | kwargs = {param: value} 110 | with pytest.raises(TwitchAttributeException): 111 | client.communities.get_top(**kwargs) 112 | 113 | 114 | @responses.activate 115 | def test_get_banned_users(): 116 | community_id = "abcd" 117 | response = {"_cursor": "", "banned_users": [example_user]} 118 | 119 | responses.add( 120 | responses.GET, 121 | "{}communities/{}/bans".format(BASE_URL, community_id), 122 | body=json.dumps(response), 123 | status=200, 124 | content_type="application/json", 125 | ) 126 | 127 | client = TwitchClient("client id", "oauth token") 128 | 129 | users = client.communities.get_banned_users(community_id) 130 | 131 | assert len(responses.calls) == 1 132 | assert len(users) == 1 133 | user = users[0] 134 | assert isinstance(user, User) 135 | assert user.id == example_user["_id"] 136 | assert user.name == example_user["name"] 137 | 138 | 139 | @responses.activate 140 | @pytest.mark.parametrize("param,value", [("limit", 101)]) 141 | def test_get_banned_users_raises_if_wrong_params_are_passed_in(param, value): 142 | client = TwitchClient("client id", "oauth token") 143 | kwargs = {param: value} 144 | with pytest.raises(TwitchAttributeException): 145 | client.communities.get_banned_users("1234", **kwargs) 146 | 147 | 148 | @responses.activate 149 | def test_ban_user(): 150 | community_id = "abcd" 151 | user_id = 1234 152 | responses.add( 153 | responses.PUT, 154 | "{}communities/{}/bans/{}".format(BASE_URL, community_id, user_id), 155 | status=204, 156 | content_type="application/json", 157 | ) 158 | 159 | client = TwitchClient("client id", "oauth token") 160 | 161 | client.communities.ban_user(community_id, user_id) 162 | 163 | assert len(responses.calls) == 1 164 | 165 | 166 | @responses.activate 167 | def test_unban_user(): 168 | community_id = "abcd" 169 | user_id = 1234 170 | responses.add( 171 | responses.DELETE, 172 | "{}communities/{}/bans/{}".format(BASE_URL, community_id, user_id), 173 | status=204, 174 | content_type="application/json", 175 | ) 176 | 177 | client = TwitchClient("client id", "oauth token") 178 | 179 | client.communities.unban_user(community_id, user_id) 180 | 181 | assert len(responses.calls) == 1 182 | 183 | 184 | @responses.activate 185 | def test_create_avatar_image(): 186 | community_id = "abcd" 187 | responses.add( 188 | responses.POST, 189 | "{}communities/{}/images/avatar".format(BASE_URL, community_id), 190 | status=204, 191 | content_type="application/json", 192 | ) 193 | 194 | client = TwitchClient("client id", "oauth token") 195 | 196 | client.communities.create_avatar_image(community_id, "imagecontent") 197 | 198 | assert len(responses.calls) == 1 199 | 200 | 201 | @responses.activate 202 | def test_delete_avatar_image(): 203 | community_id = "abcd" 204 | responses.add( 205 | responses.DELETE, 206 | "{}communities/{}/images/avatar".format(BASE_URL, community_id), 207 | status=204, 208 | content_type="application/json", 209 | ) 210 | 211 | client = TwitchClient("client id", "oauth token") 212 | 213 | client.communities.delete_avatar_image(community_id) 214 | 215 | assert len(responses.calls) == 1 216 | 217 | 218 | @responses.activate 219 | def test_create_cover_image(): 220 | community_id = "abcd" 221 | responses.add( 222 | responses.POST, 223 | "{}communities/{}/images/cover".format(BASE_URL, community_id), 224 | status=204, 225 | content_type="application/json", 226 | ) 227 | 228 | client = TwitchClient("client id", "oauth token") 229 | 230 | client.communities.create_cover_image(community_id, "imagecontent") 231 | 232 | assert len(responses.calls) == 1 233 | 234 | 235 | @responses.activate 236 | def test_delete_cover_image(): 237 | community_id = "abcd" 238 | responses.add( 239 | responses.DELETE, 240 | "{}communities/{}/images/cover".format(BASE_URL, community_id), 241 | status=204, 242 | content_type="application/json", 243 | ) 244 | 245 | client = TwitchClient("client id", "oauth token") 246 | 247 | client.communities.delete_cover_image(community_id) 248 | 249 | assert len(responses.calls) == 1 250 | 251 | 252 | @responses.activate 253 | def test_get_moderators(): 254 | community_id = "abcd" 255 | response = {"moderators": [example_user]} 256 | 257 | responses.add( 258 | responses.GET, 259 | "{}communities/{}/moderators".format(BASE_URL, community_id), 260 | body=json.dumps(response), 261 | status=200, 262 | content_type="application/json", 263 | ) 264 | 265 | client = TwitchClient("client id", "oauth token") 266 | 267 | moderators = client.communities.get_moderators(community_id) 268 | 269 | assert len(responses.calls) == 1 270 | assert len(moderators) == 1 271 | user = moderators[0] 272 | assert isinstance(user, User) 273 | assert user.id == example_user["_id"] 274 | assert user.name == example_user["name"] 275 | 276 | 277 | @responses.activate 278 | def test_add_moderator(): 279 | community_id = "abcd" 280 | user_id = 12345 281 | responses.add( 282 | responses.PUT, 283 | "{}communities/{}/moderators/{}".format(BASE_URL, community_id, user_id), 284 | status=204, 285 | content_type="application/json", 286 | ) 287 | 288 | client = TwitchClient("client id", "oauth token") 289 | 290 | client.communities.add_moderator(community_id, user_id) 291 | 292 | assert len(responses.calls) == 1 293 | 294 | 295 | @responses.activate 296 | def test_delete_moderator(): 297 | community_id = "abcd" 298 | user_id = 12345 299 | responses.add( 300 | responses.DELETE, 301 | "{}communities/{}/moderators/{}".format(BASE_URL, community_id, user_id), 302 | status=204, 303 | content_type="application/json", 304 | ) 305 | 306 | client = TwitchClient("client id", "oauth token") 307 | 308 | client.communities.delete_moderator(community_id, user_id) 309 | 310 | assert len(responses.calls) == 1 311 | 312 | 313 | @responses.activate 314 | def test_get_permissions(): 315 | community_id = "abcd" 316 | response = {"ban": True, "timeout": True, "edit": True} 317 | 318 | responses.add( 319 | responses.GET, 320 | "{}communities/{}/permissions".format(BASE_URL, community_id), 321 | body=json.dumps(response), 322 | status=200, 323 | content_type="application/json", 324 | ) 325 | 326 | client = TwitchClient("client id", "oauth token") 327 | 328 | permissions = client.communities.get_permissions(community_id) 329 | 330 | assert len(responses.calls) == 1 331 | assert isinstance(permissions, dict) 332 | assert permissions["ban"] is True 333 | 334 | 335 | @responses.activate 336 | def test_report_violation(): 337 | community_id = "abcd" 338 | responses.add( 339 | responses.POST, 340 | "{}communities/{}/report_channel".format(BASE_URL, community_id), 341 | status=204, 342 | content_type="application/json", 343 | ) 344 | 345 | client = TwitchClient("client id", "oauth token") 346 | 347 | client.communities.report_violation(community_id, 12345) 348 | 349 | assert len(responses.calls) == 1 350 | 351 | 352 | @responses.activate 353 | def test_get_timed_out_users(): 354 | community_id = "abcd" 355 | response = {"_cursor": "", "timed_out_users": [example_user]} 356 | 357 | responses.add( 358 | responses.GET, 359 | "{}communities/{}/timeouts".format(BASE_URL, community_id), 360 | body=json.dumps(response), 361 | status=200, 362 | content_type="application/json", 363 | ) 364 | 365 | client = TwitchClient("client id", "oauth token") 366 | 367 | users = client.communities.get_timed_out_users(community_id) 368 | 369 | assert len(responses.calls) == 1 370 | assert len(users) == 1 371 | user = users[0] 372 | assert isinstance(user, User) 373 | assert user.id == example_user["_id"] 374 | assert user.name == example_user["name"] 375 | 376 | 377 | @responses.activate 378 | @pytest.mark.parametrize("param,value", [("limit", 101)]) 379 | def test_get_timed_out_users_raises_if_wrong_params_are_passed_in(param, value): 380 | client = TwitchClient("client id", "oauth token") 381 | kwargs = {param: value} 382 | with pytest.raises(TwitchAttributeException): 383 | client.communities.get_timed_out_users("1234", **kwargs) 384 | 385 | 386 | @responses.activate 387 | def test_add_timed_out_user(): 388 | community_id = "abcd" 389 | user_id = 12345 390 | responses.add( 391 | responses.PUT, 392 | "{}communities/{}/timeouts/{}".format(BASE_URL, community_id, user_id), 393 | status=204, 394 | content_type="application/json", 395 | ) 396 | 397 | client = TwitchClient("client id", "oauth token") 398 | 399 | client.communities.add_timed_out_user(community_id, user_id, 5) 400 | 401 | assert len(responses.calls) == 1 402 | 403 | 404 | @responses.activate 405 | def test_delete_timed_out_user(): 406 | community_id = "abcd" 407 | user_id = 12345 408 | responses.add( 409 | responses.DELETE, 410 | "{}communities/{}/timeouts/{}".format(BASE_URL, community_id, user_id), 411 | status=204, 412 | content_type="application/json", 413 | ) 414 | 415 | client = TwitchClient("client id", "oauth token") 416 | 417 | client.communities.delete_timed_out_user(community_id, user_id) 418 | 419 | assert len(responses.calls) == 1 420 | --------------------------------------------------------------------------------