├── requirements.txt ├── tests └── test_client.py ├── .gitignore ├── xboxapi ├── __init__.py ├── gamer.py └── client.py ├── .travis.yml ├── LICENSE ├── setup.py └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.13.0 -------------------------------------------------------------------------------- /tests/test_client.py: -------------------------------------------------------------------------------- 1 | # Still need to write these... 2 | def test_client(): 3 | assert 1 == 1 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | example.py 4 | venv 5 | README.html 6 | .cache/ 7 | .vscode 8 | -------------------------------------------------------------------------------- /xboxapi/__init__.py: -------------------------------------------------------------------------------- 1 | #-*- coding: utf-8 -*- 2 | 3 | from .client import Client 4 | 5 | __version__ = '3.0.0' -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.6" 4 | install: 5 | - "pip install -r requirements.txt" 6 | - "pip install pytest" 7 | script: "pytest tests/" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import os 3 | import re 4 | 5 | import xboxapi 6 | 7 | from setuptools import setup 8 | 9 | bin = os.path.abspath(os.path.dirname(__file__)) 10 | 11 | def read(file): 12 | return codecs.open(os.path.join(bin, file), 'r').read() 13 | 14 | long_description = read('README.md') 15 | 16 | setup( 17 | name='xboxapi', 18 | version=xboxapi.__version__, 19 | url='https://github.com/xboxapi/Python-Wrapper', 20 | license='MIT License', 21 | author='xboxapi.com', 22 | install_requires=['requests'], 23 | description='XBOX One API', 24 | long_description=long_description, 25 | packages=['xboxapi'], 26 | package_data={'': ['README.md', 'LICENSE']}, 27 | include_package_data=True, 28 | platforms='any', 29 | classifiers=[ 30 | 'Programming Language :: Python', 31 | 'Development Status :: 3 - Alpha', 32 | 'Natural Language :: English', 33 | 'Environment :: Web Environment', 34 | 'Intended Audience :: Developers', 35 | 'License :: OSI Approved :: MIT License', 36 | 'Operating System :: OS Independent', 37 | 'Topic :: Software Development :: Libraries :: Python Modules', 38 | 'Topic :: Games/Entertainment', 39 | ], 40 | ) 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Description # 2 | 3 | This is a Python wrapper for the unofficial [Xbox API](https://xboxapi.com) 4 | 5 | [![Build Status](https://travis-ci.org/xboxapi/Python-Wrapper.svg?branch=master)](https://travis-ci.org/xboxapi/Python-Wrapper) 6 | 7 | # Installation # 8 | For now you will have to install manually, as I didn't upload the initial version to pypi (`pip`). 9 | 10 | 1. Clone this repo 11 | 2. Place the `xboxapi` directory in your project 12 | 13 | The only dependency is [requests](https://github.com/kennethreitz/requests) library. 14 | 15 | # Usage # 16 | 17 | This is a basic example of how to create a client and fetch a gamers profile information from their gamertag. 18 | 19 | ```python 20 | from xboxapi import Client 21 | 22 | client = Client(api_key=) 23 | gamer = client.gamer('voidpirate') 24 | 25 | profile = gamer.get('profile') 26 | ``` 27 | 28 | `Client` class constructor takes the following optional arguments except `api_key`. 29 | 30 | | Argument | Value | Short Description | 31 | |--- |--- |--- | 32 | | api_key | string | api token from [Xbox API](https://xboxapi.com) | 33 | | timeout | int | how long until the request times out (seconds) | 34 | | lang | string | country language code (e.g. for German (`de-DE`)) | 35 | 36 | 37 | `Client` class public methods. 38 | 39 | | Method | Value | Optional | Short Description | 40 | |--- |--- |--- |--- | 41 | | `gamer(gamertag=)` | string | `xuid=` | gamertag to lookup | 42 | | `calls_remaining()` | n/a | n/a | Return headers about api rate limits | 43 | 44 | A note about the gamer method. If you already know the gamers xuid you can use that instead to avoid an additional api call when using only a gamertag. 45 | 46 | `Gamer` class public methods, returned from gamer method in `Client`. 47 | 48 | | Method | Value | Optional | Short Description | 49 | |--- |--- |--- |--- | 50 | | `get(method=)` | string | `term=` | API calls. | 51 | | `send_message(message=)` | string | n/a | Send a message to gamer | 52 | | `send_activity(message=)` | string | n/a | Update your activity feed with a message | 53 | 54 | Pagination is supported in this client and all handled through `get` method. It works by detecting the response header for pagination, any subsequent calls to the same api endpoint will return paged data. If another api call is made to a different endpoint, the pagination token will be cleared and results will not be paged. -------------------------------------------------------------------------------- /xboxapi/gamer.py: -------------------------------------------------------------------------------- 1 | #-*- coding: utf-8 -*- 2 | 3 | 4 | class Gamer(object): 5 | ''' Xbox profile wrapper ''' 6 | 7 | def __init__(self, gamertag=None, client=None, xuid=None): 8 | self.client = client 9 | self.gamertag = gamertag 10 | self.xuid = xuid if xuid is not None else self.fetch_xuid() 11 | self.endpoints = [ 12 | 'messages', 13 | 'conversations', 14 | 'recent-players', 15 | 'activity-feed', 16 | 'latest-xbox360-games', 17 | 'latest-xboxone-games', 18 | 'latest-xboxone-apps', 19 | 'xboxone-gold-lounge', 20 | 'game-details', 21 | 'game-details-hex' 22 | ] 23 | self.endpoints_xuid = [ 24 | 'achievements', 25 | 'profile', 26 | 'presence', 27 | 'gamercard', 28 | 'activity', 29 | 'friends', 30 | 'followers', 31 | 'game-clips', 32 | 'game-clips/saved', 33 | 'game-stats', 34 | 'screenshots', 35 | 'xbox360games', 36 | 'xboxonegames', 37 | 'game-status' 38 | ] 39 | 40 | def get(self, method=None, term=None): 41 | ''' Retrieve data from supported endpoints ''' 42 | # Hack to avoid calling api again for xuid retrieval 43 | if method == 'xuid': 44 | return self.xuid 45 | 46 | url = self.parse_endpoints(method, term) 47 | if url is not False: 48 | return self.client.api_get(url).json() 49 | 50 | url = self.parse_endpoints_secondary(method, term) 51 | if url is not False: 52 | return self.client.api_get(url).json() 53 | 54 | return {} 55 | 56 | def parse_endpoints(self, method=None, term=None): 57 | ''' Constructs a valid endpoint url for api ''' 58 | if method is None: 59 | return False 60 | 61 | for endpoint in self.endpoints: 62 | if endpoint != method: 63 | continue 64 | url = endpoint 65 | if term is not None: 66 | url = url + '/' + term 67 | return url 68 | 69 | return False 70 | 71 | def parse_endpoints_secondary(self, method=None, term=None): 72 | ''' Parse secondary endpoints that require xuid in url ''' 73 | for endpoint in self.endpoints_xuid: 74 | if endpoint != method: 75 | continue 76 | url = str(self.xuid) + '/' + endpoint 77 | if term is not None: 78 | url = url + '/' + term 79 | return url 80 | 81 | return False 82 | 83 | def send_message(self, message=None, xuids=None): 84 | ''' Send a message given a list of gamer xuids ''' 85 | payload = {} 86 | if message is None: 87 | raise ValueError('A message is required!') 88 | 89 | if xuids is not None and not hasattr(xuids, 'append'): 90 | raise TypeError('List was not given!') 91 | 92 | if xuids is None: 93 | xuids = [self.xuid] 94 | 95 | payload['to'] = xuids 96 | payload['message'] = message 97 | return self.client.api_post('messages', payload) 98 | 99 | def post_activity(self, message=None): 100 | ''' Post directly to your activity feed ''' 101 | payload = {} 102 | if message is None: 103 | raise ValueError('A message is required!') 104 | payload['message'] = message 105 | return self.client.api_post('acitivity-feed', payload) 106 | 107 | def fetch_xuid(self): 108 | ''' Fetch gamer xuid from gamertag ''' 109 | return self.client.api_get('xuid/' + self.gamertag).json() 110 | -------------------------------------------------------------------------------- /xboxapi/client.py: -------------------------------------------------------------------------------- 1 | #-*- coding: utf-8 -*- 2 | 3 | import requests 4 | import logging 5 | import json 6 | import os 7 | 8 | # Local libraries 9 | from .gamer import Gamer 10 | 11 | import xboxapi 12 | 13 | logging.basicConfig() 14 | 15 | 16 | class Client(object): 17 | 18 | def __init__(self, api_key=None, timeout=None, lang=None): 19 | 20 | self.api_key = api_key 21 | self.timeout = timeout 22 | self.endpoint = 'https://xboxapi.com/api/' 23 | self.timeout = timeout if timeout is not None else 3 # Seconds 24 | self.lang = lang 25 | self.last_method_call = None 26 | self.continuation_token = None 27 | 28 | # Debug logging can be triggered from environment variable 29 | # XBOXAPI_DEBUG=1 30 | self.logger = logging.getLogger('xboxapi') 31 | log_level = logging.DEBUG if os.getenv('XBOXAPI_DEBUG') else logging.INFO 32 | self.logger.setLevel(log_level) 33 | 34 | if self.api_key is None: 35 | raise ValueError('Api key is missing') 36 | 37 | def gamer(self, gamertag=None, xuid=None): 38 | ''' return a gamer object ''' 39 | if gamertag is None: 40 | raise ValueError('No gamertag given!') 41 | 42 | return Gamer(gamertag=gamertag, client=self, xuid=xuid) 43 | 44 | def api_get(self, method): 45 | ''' GET wrapper on requests library ''' 46 | headers = { 47 | 'Authorization': 'Bearer ' + self.api_key, 48 | 'User-Agent': 'Python/XboxApi ' + xboxapi.__version__ 49 | } 50 | 51 | if self.lang is not None: 52 | headers['Accept-Language'] = self.lang 53 | 54 | url = self.endpoint + method 55 | # Check for continuation token and the method match the last call 56 | if method == self.last_method_call and self.continuation_token is not None: 57 | url = url + '?continuationToken=' + self.continuation_token 58 | 59 | self.logger.debug('%s %s', 'GET', url) 60 | self.logger.debug('Headers: %s', headers) 61 | 62 | res = requests.get(self.endpoint + method, 63 | headers=headers, timeout=self.timeout) 64 | self.xboxapi_response_error(res) 65 | self.logger.debug('Response: %s', res.json()) 66 | 67 | # Track method calls and peak for continuation token 68 | self.last_method_call = method 69 | self.continuation_token = None 70 | if 'X-Continuation-Token' in res.headers: 71 | self.continuation_token = res.headers['X-Continuation-Token'] 72 | 73 | return res 74 | 75 | def api_post(self, method, body): 76 | ''' POST wrapper on requests library ''' 77 | headers = { 78 | 'Authorization': 'Bearer ' + self.api_key, 79 | 'Content-Type': 'application/json' 80 | } 81 | 82 | url = '{}{}'.format(self.endpoint, method) 83 | 84 | self.logger.debug('%s %s', 'POST', url) 85 | self.logger.debug('Headers: %s', headers) 86 | self.logger.debug('Body: %s', body) 87 | 88 | res = requests.post(self.endpoint + method, headers=headers, data=json.dumps(body), 89 | timeout=self.timeout) 90 | self.xboxapi_response_error(res) 91 | 92 | self.logger.debug('Response: %s', res.json()) 93 | 94 | return res 95 | 96 | def calls_remaining(self): 97 | ''' Check on the limits from server ''' 98 | server_headers = self.api_get('accountxuid').headers 99 | limit_headers = {} 100 | limit_headers['X-RateLimit-Reset'] = server_headers['X-RateLimit-Reset'] 101 | limit_headers['X-RateLimit-Limit'] = server_headers['X-RateLimit-Limit'] 102 | limit_headers['X-RateLimit-Remaining'] = server_headers['X-RateLimit-Remaining'] 103 | return limit_headers 104 | 105 | def xboxapi_response_error(self, response): 106 | """ 107 | Check for an errors returned from the XboxAPI. Errors from the XboxAPI 108 | have the following format. 109 | 110 | Example: 111 | { 112 | "success": false, 113 | "error_code": 402, 114 | "error_message": "Paid subscriber feature only" 115 | } 116 | """ 117 | if not response.ok: 118 | self.logger.error('XboxAPI error: (%s) %s', response.status_code, response.reason) 119 | return 120 | --------------------------------------------------------------------------------