├── .gitignore ├── .travis.yml ├── LICENSE ├── README.rst ├── mojang_api ├── __init__.py ├── _common │ ├── __init__.py │ ├── endpoint.py │ ├── player.py │ └── response.py ├── servers │ ├── __init__.py │ ├── api.py │ ├── authserver.py │ ├── sessionserver.py │ └── status.py ├── user │ ├── __init__.py │ └── player.py └── utils │ ├── __init__.py │ └── uuid.py ├── setup.cfg ├── setup.py └── tests ├── conftest.py ├── test_imports.py └── test_uuids.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/python 3 | 4 | ### Python ### 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # Environments 86 | .env 87 | .venv 88 | env/ 89 | venv/ 90 | ENV/ 91 | env.bak/ 92 | venv.bak/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # mypy 105 | .mypy_cache/ 106 | 107 | # End of https://www.gitignore.io/api/python 108 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | stages: 4 | - test 5 | - upload coverage 6 | - name: deploy 7 | if: tag IS present 8 | jobs: 9 | include: 10 | - stage: test 11 | python: 3.6 12 | install: pip install . 13 | script: python setup.py test 14 | - stage: test 15 | python: 3.5 16 | install: pip install . 17 | script: python setup.py test 18 | - stage: upload coverage 19 | python: 3.6 20 | install: pip install coveralls 21 | script: true 22 | after_success: coveralls 23 | - stage: deploy 24 | python: 3.6 25 | install: true 26 | script: true 27 | deploy: 28 | provider: pypi 29 | user: $PYPI_USERNAME 30 | password: $PYPI_PASSWORD 31 | distributions: sdist bdist_wheel 32 | skip_cleanup: true 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Synchronous 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 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | mojang-api 2 | ========== 3 | 4 | |build-status| |code-coverage| |version| |python-versions| |implementation| |license| 5 | 6 | A full Python wrapper of Mojang's `API`_ and `Authentication API`_. 7 | 8 | .. |build-status| image:: https://img.shields.io/travis/SynchronousX/mojang-api.svg 9 | :alt: Build Status 10 | :scale: 100% 11 | :target: https://travis-ci.org/SynchronousX/mojang-api 12 | 13 | .. |code-coverage| image:: https://img.shields.io/coveralls/github/SynchronousX/mojang-api.svg 14 | :alt: Code Coverage 15 | :scale: 100% 16 | :target: https://coveralls.io/github/SynchronousX/mojang-api 17 | 18 | .. |version| image:: https://img.shields.io/pypi/v/mojang-api.svg 19 | :alt: Version 20 | :scale: 100% 21 | :target: https://pypi.python.org/pypi/mojang-api 22 | 23 | .. |python-versions| image:: https://img.shields.io/pypi/pyversions/mojang-api.svg 24 | :alt: Python Versions 25 | :scale: 100% 26 | :target: https://pypi.python.org/pypi/mojang-api 27 | 28 | .. |implementation| image:: https://img.shields.io/pypi/implementation/mojang-api.svg 29 | :alt: Implementation 30 | :scale: 100% 31 | :target: https://pypi.python.org/pypi/mojang-api 32 | 33 | .. |license| image:: https://img.shields.io/github/license/SynchronousX/mojang-api.svg 34 | :alt: License 35 | :scale: 100% 36 | :target: LICENSE 37 | 38 | .. _API: http://wiki.vg/Mojang_API 39 | .. _Authentication API: http://wiki.vg/Authentication 40 | 41 | Installation 42 | ------------ 43 | ``$ pip install mojang-api`` 44 | -------------------------------------------------------------------------------- /mojang_api/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from .servers.api import (change_skin, get_statistics, get_username_history, 4 | get_uuid, get_uuids, reset_skin, upload_skin) 5 | from .servers.authserver import (authenticate_user, invalidate_access_token, 6 | refresh_access_token, signout_user, 7 | validate_access_token) 8 | from .servers.sessionserver import get_blocked_servers, get_user_profile 9 | from .servers.status import get_status 10 | from .utils.uuid import generate_client_token, is_valid_uuid 11 | -------------------------------------------------------------------------------- /mojang_api/_common/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | -------------------------------------------------------------------------------- /mojang_api/_common/endpoint.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from enum import Enum 4 | 5 | 6 | class BaseURL: 7 | def __init__(self, base_url=''): 8 | self._base_url = base_url 9 | 10 | def __get__(self, instance, owner): 11 | return self._base_url 12 | 13 | 14 | class Endpoint(Enum): 15 | BASE_URL = BaseURL() 16 | 17 | def __init__(self, endpoint_uri): 18 | self._endpoint_uri = endpoint_uri 19 | self._url = self.BASE_URL + self.endpoint_uri 20 | 21 | @property 22 | def endpoint_uri(self): 23 | return self._endpoint_uri 24 | 25 | @property 26 | def url(self): 27 | return self._url 28 | -------------------------------------------------------------------------------- /mojang_api/_common/player.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from functools import wraps 4 | 5 | from ..user.player import Player 6 | from ..utils.uuid import is_valid_uuid 7 | 8 | 9 | def accept_player(*arg_pos_range): 10 | arg_pos_slice = slice(*arg_pos_range) 11 | arg_pos_range = [0 if arg_pos_slice.start is None else arg_pos_slice.start, 12 | arg_pos_slice.stop, 1 if arg_pos_slice.step is None else arg_pos_slice.step] 13 | 14 | def decorator(func): 15 | @wraps(func) 16 | def with_player_acceptance(*args, **kwargs): 17 | args = list(args) 18 | if arg_pos_range[1] is None: 19 | arg_pos_range[1] = len(args) 20 | 21 | for arg_pos in range(*arg_pos_range): 22 | player = args[arg_pos] 23 | if isinstance(player, Player): 24 | pass 25 | elif isinstance(player, str): 26 | if is_valid_uuid(player): 27 | args[arg_pos] = Player(uuid=player) 28 | else: 29 | args[arg_pos] = Player(username=player) 30 | else: 31 | raise TypeError( 32 | 'player must be a valid Player, username, or UUID') 33 | 34 | return func(*args, **kwargs) 35 | 36 | return with_player_acceptance 37 | 38 | return decorator 39 | -------------------------------------------------------------------------------- /mojang_api/_common/response.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from box import Box, BoxList 4 | 5 | 6 | class APIResponseDict(Box): 7 | pass 8 | 9 | 10 | class APIResponseList(BoxList): 11 | pass 12 | 13 | 14 | class APIResponseEmpty: 15 | def __new__(cls, *args, **kwargs): 16 | return super(cls.__class__, cls).__new__(cls) 17 | 18 | def __init__(self, *args, **kwargs): 19 | pass 20 | 21 | 22 | class APIResponse: 23 | _response = None 24 | 25 | def __new__(cls, response, *args, **kwargs): 26 | data = None 27 | try: 28 | data = response.json() 29 | except ValueError: 30 | instance_class = APIResponseEmpty 31 | else: 32 | if isinstance(data, dict): 33 | instance_class = APIResponseDict 34 | elif isinstance(data, list): 35 | instance_class = APIResponseList 36 | else: 37 | raise TypeError( 38 | 'response\'s JSON data must be of type \'dict\' or \'list\'') 39 | 40 | kwargs['camel_killer_box'] = True 41 | name = cls.__name__ 42 | cls = globals()[name] 43 | bases = (instance_class,) + (cls,) 44 | dct = dict(instance_class.__dict__) 45 | preserved_attrs = [ 46 | '_response', 47 | 'response', 48 | '__init__' 49 | ] 50 | 51 | for attr in preserved_attrs: 52 | dct[attr] = cls.__dict__[attr] 53 | 54 | new_class = type(name, bases, dct) 55 | instance = new_class.__new__( 56 | new_class, response, data, *args, **kwargs) 57 | new_class.__init__(instance, response, data, *args, **kwargs) 58 | return instance 59 | 60 | def __init__(self, response, *args, **kwargs): 61 | self._response = response 62 | super(self.__class__, self).__init__(*args, **kwargs) 63 | 64 | @property 65 | def response(self): 66 | return self._response 67 | -------------------------------------------------------------------------------- /mojang_api/servers/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | -------------------------------------------------------------------------------- /mojang_api/servers/api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from requests import delete, get, post, put 4 | 5 | from . import sessionserver 6 | from .._common.endpoint import BaseURL, Endpoint 7 | from .._common.player import accept_player 8 | from .._common.response import APIResponse 9 | 10 | 11 | class APIEndpoint(Endpoint): 12 | BASE_URL = BaseURL('https://api.mojang.com') 13 | USERNAME_TO_UUID_AT_TIME = '/users/profiles/minecraft/{username}' 14 | UUID_TO_USERNAME_HISTORY = '/user/profiles/{uuid}/names' 15 | USERNAMES_TO_UUIDS = '/profiles/minecraft' 16 | CHANGE_SKIN = '/user/profile/{uuid}/skin' 17 | UPLOAD_SKIN = '/user/profile/{uuid}/skin' 18 | RESET_SKIN = '/user/profile/{uuid}/skin' 19 | STATISTICS = '/orders/statistics' 20 | 21 | 22 | @accept_player(1) 23 | def get_uuid(player, timestamp=None): 24 | if not player.username and player.uuid: 25 | player.username = sessionserver.get_user_profile(player).name 26 | 27 | params = { 28 | 'at': timestamp 29 | } 30 | response = get(APIEndpoint.USERNAME_TO_UUID_AT_TIME.url.format( 31 | username=player.username), params=params) 32 | return APIResponse(response) 33 | 34 | 35 | @accept_player(1) 36 | def get_username_history(player): 37 | if not player.uuid and player.username: 38 | player.uuid = get_uuid(player).id 39 | 40 | response = get( 41 | APIEndpoint.UUID_TO_USERNAME_HISTORY.url.format(uuid=player.uuid)) 42 | return APIResponse(response) 43 | 44 | 45 | @accept_player(None) 46 | def get_uuids(*players): 47 | usernames = [] 48 | for player in players: 49 | if not player.username and player.uuid: 50 | player.username = sessionserver.get_user_profile(player).name 51 | 52 | usernames.append(player.username) 53 | 54 | response = post(APIEndpoint.USERNAMES_TO_UUIDS.url, json=usernames) 55 | return APIResponse(response) 56 | 57 | 58 | @accept_player(1) 59 | def change_skin(player, access_token, skin_url, slim_model=False): 60 | if not player.uuid and player.username: 61 | player.uuid = get_uuid(player).id 62 | 63 | headers = { 64 | 'Authorization': 'Bearer ' + access_token 65 | } 66 | payload = { 67 | 'model': 'slim' if slim_model else '', 68 | 'url': skin_url 69 | } 70 | response = post(APIEndpoint.CHANGE_SKIN.url.format( 71 | uuid=player.uuid), headers=headers, data=payload) 72 | return APIResponse(response) 73 | 74 | 75 | @accept_player(1) 76 | def upload_skin(player, access_token, path_to_skin, slim_model=False): 77 | if not player.uuid and player.username: 78 | player.uuid = get_uuid(player).id 79 | 80 | headers = { 81 | 'Authorization': 'Bearer ' + access_token 82 | } 83 | files = { 84 | 'model': 'slim' if slim_model else '', 85 | 'file': open(path_to_skin, 'rb') 86 | } 87 | response = put(APIEndpoint.UPLOAD_SKIN.url.format( 88 | uuid=player.uuid), headers=headers, files=files) 89 | return APIResponse(response) 90 | 91 | 92 | @accept_player(1) 93 | def reset_skin(player, access_token): 94 | if not player.uuid and player.username: 95 | player.uuid = get_uuid(player).id 96 | 97 | headers = { 98 | 'Authorization': 'Bearer ' + access_token 99 | } 100 | response = delete(APIEndpoint.RESET_SKIN.url.format( 101 | uuid=player.uuid), headers=headers) 102 | return APIResponse(response) 103 | 104 | 105 | def get_statistics(item_sold_minecraft=False, prepaid_card_redeemed_minecraft=False, item_sold_cobalt=False, item_sold_scrolls=False): 106 | sales_mapping = { 107 | 'item_sold_minecraft': item_sold_minecraft, 108 | 'prepaid_card_redeemed_minecraft': prepaid_card_redeemed_minecraft, 109 | 'item_sold_cobalt': item_sold_cobalt, 110 | 'item_sold_scrolls': item_sold_scrolls 111 | } 112 | payload = { 113 | 'metricKeys': [k for (k, v) in sales_mapping.items() if v] 114 | } 115 | response = post(APIEndpoint.STATISTICS.url, json=payload) 116 | return APIResponse(response) 117 | -------------------------------------------------------------------------------- /mojang_api/servers/authserver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from requests import post 4 | 5 | from .._common.endpoint import BaseURL, Endpoint 6 | from .._common.response import APIResponse 7 | from ..utils.uuid import generate_client_token 8 | 9 | 10 | class AuthserverEndpoint(Endpoint): 11 | BASE_URL = BaseURL('https://authserver.mojang.com') 12 | AUTHENTICATE = '/authenticate' 13 | REFRESH = '/refresh' 14 | VALIDATE = '/validate' 15 | SIGNOUT = '/signout' 16 | INVALIDATE = '/invalidate' 17 | 18 | 19 | def authenticate_user(username, password, client_token=generate_client_token(), request_user=False): 20 | payload = { 21 | 'agent': { 22 | 'name': 'Minecraft', 23 | 'version': 1 24 | }, 25 | 'username': username, 26 | 'password': password, 27 | 'clientToken': client_token 28 | } 29 | if request_user: 30 | payload['requestUser'] = True 31 | 32 | response = post(AuthserverEndpoint.AUTHENTICATE.url, json=payload) 33 | return APIResponse(response) 34 | 35 | 36 | def refresh_access_token(access_token, client_token, request_user=False): 37 | payload = { 38 | 'accessToken': access_token, 39 | 'clientToken': client_token 40 | } 41 | if request_user: 42 | payload['requestUser'] = True 43 | 44 | response = post(AuthserverEndpoint.REFRESH.url, json=payload) 45 | return APIResponse(response) 46 | 47 | 48 | def validate_access_token(access_token, client_token=None): 49 | payload = { 50 | 'accessToken': access_token 51 | } 52 | if client_token != None: 53 | payload['clientToken'] = client_token 54 | 55 | response = post(AuthserverEndpoint.VALIDATE.url, json=payload) 56 | return APIResponse(response) 57 | 58 | 59 | def signout_user(username, password): 60 | payload = { 61 | 'username': username, 62 | 'password': password 63 | } 64 | response = post(AuthserverEndpoint.SIGNOUT.url, json=payload) 65 | return APIResponse(response) 66 | 67 | 68 | def invalidate_access_token(access_token, client_token): 69 | payload = { 70 | 'accessToken': access_token, 71 | 'clientToken': client_token 72 | } 73 | response = post(AuthserverEndpoint.INVALIDATE.url, json=payload) 74 | return APIResponse(response) 75 | -------------------------------------------------------------------------------- /mojang_api/servers/sessionserver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from requests import get 4 | 5 | from . import api 6 | from .._common.endpoint import BaseURL, Endpoint 7 | from .._common.player import accept_player 8 | from .._common.response import APIResponse 9 | 10 | 11 | class SessionserverEndpoint(Endpoint): 12 | BASE_URL = BaseURL('https://sessionserver.mojang.com') 13 | UUID_TO_PROFILE = '/session/minecraft/profile/{uuid}' 14 | BLOCKED_SERVERS = '/blockedservers' 15 | 16 | 17 | @accept_player(1) 18 | def get_user_profile(player): 19 | if not player.uuid and player.username: 20 | player.uuid = api.get_uuid(player).id 21 | 22 | response = get( 23 | SessionserverEndpoint.UUID_TO_PROFILE.url.format(uuid=player.uuid)) 24 | return APIResponse(response) 25 | 26 | 27 | def get_blocked_servers(): 28 | response = get(SessionserverEndpoint.BLOCKED_SERVERS.url) 29 | return APIResponse(response) 30 | -------------------------------------------------------------------------------- /mojang_api/servers/status.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from requests import get 4 | 5 | from .._common.endpoint import BaseURL, Endpoint 6 | from .._common.response import APIResponse 7 | 8 | 9 | class StatusEndpoint(Endpoint): 10 | BASE_URL = BaseURL('https://status.mojang.com') 11 | CHECK = '/check' 12 | 13 | 14 | def get_status(): 15 | response = get(StatusEndpoint.CHECK.url) 16 | return APIResponse(response) 17 | -------------------------------------------------------------------------------- /mojang_api/user/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | -------------------------------------------------------------------------------- /mojang_api/user/player.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | 4 | class Player: 5 | def __init__(self, username='', uuid=''): 6 | self._validate(username, uuid) 7 | self._username = username 8 | self._uuid = uuid 9 | 10 | def _validate(self, username=None, uuid=None): 11 | if username is None: 12 | username = self.username 13 | 14 | if uuid is None: 15 | uuid = self.uuid 16 | 17 | if not (username or uuid): 18 | raise AttributeError('Player must contain a username or UUID') 19 | 20 | @property 21 | def username(self): 22 | return self._username 23 | 24 | @username.setter 25 | def username(self, username): 26 | if username: 27 | self._username = username 28 | else: 29 | del self.username 30 | 31 | @username.deleter 32 | def username(self): 33 | self._validate(username='') 34 | self._username = '' 35 | 36 | @property 37 | def uuid(self): 38 | return self._uuid 39 | 40 | @uuid.setter 41 | def uuid(self, uuid): 42 | if uuid: 43 | self._uuid = uuid 44 | else: 45 | del self.uuid 46 | 47 | @uuid.deleter 48 | def uuid(self): 49 | self._validate(uuid='') 50 | self._uuid = '' 51 | -------------------------------------------------------------------------------- /mojang_api/utils/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | -------------------------------------------------------------------------------- /mojang_api/utils/uuid.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from uuid import UUID, uuid4 4 | 5 | 6 | def generate_client_token(): 7 | return uuid4().hex 8 | 9 | 10 | def is_valid_uuid(uuid_string): 11 | try: 12 | UUID(uuid_string) 13 | except ValueError: 14 | return False 15 | 16 | return True 17 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test=pytest 3 | 4 | [tool:pytest] 5 | addopts = --cov-report= --cov=mojang_api --verbose 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from codecs import open 4 | from os import path 5 | 6 | from setuptools import find_packages, setup 7 | 8 | here = path.abspath(path.dirname(__file__)) 9 | with open(path.join(here, 'README.rst'), encoding='utf-8') as readme: 10 | long_description = readme.read() 11 | 12 | setup( 13 | name='mojang-api', 14 | version='2.1.0', 15 | description='A full Python wrapper of Mojang\'s API and Authentication API.', 16 | long_description=long_description, 17 | url='https://github.com/SynchronousX/mojang-api', 18 | author='Synchronous', 19 | license='MIT', 20 | classifiers=[ 21 | 'Development Status :: 4 - Beta', 22 | 'Intended Audience :: Developers', 23 | 'Topic :: Software Development :: Libraries :: Python Modules', 24 | 'License :: OSI Approved :: MIT License', 25 | 'Programming Language :: Python :: 3', 26 | 'Programming Language :: Python :: 3.5', 27 | 'Programming Language :: Python :: 3.6' 28 | ], 29 | keywords='mojang minecraft mc api authentication auth', 30 | packages=find_packages(), 31 | setup_requires='pytest-runner', 32 | tests_require=[ 33 | 'pytest', 34 | 'pytest-cov' 35 | ], 36 | install_requires=[ 37 | 'python-box', 38 | 'requests' 39 | ], 40 | python_requires='>=3.5.*' 41 | ) 42 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | 4 | def pytest_namespace(): 5 | return { 6 | 'users': [ 7 | ('Notch', '069a79f444e94726a5befca90e38aaf5'), 8 | ('jeb_', '853c80ef3c3749fdaa49938b674adae6'), 9 | ('Synchronous', '15fffb7e57c64b70bbc3a42dddaf0f81') 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /tests/test_imports.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from importlib import import_module 4 | 5 | PACKAGE = 'mojang_api' 6 | IMPORTS = { 7 | 'authenticate_user': ['', 'servers.authserver'], 8 | 'change_skin': ['', 'servers.api'], 9 | 'generate_client_token': ['', 'utils.uuid'], 10 | 'get_blocked_servers': ['', 'servers.sessionserver'], 11 | 'get_statistics': ['', 'servers.api'], 12 | 'get_status': ['', 'servers.status'], 13 | 'get_user_profile': ['', 'servers.sessionserver'], 14 | 'get_username_history': ['', 'servers.api'], 15 | 'get_uuid': ['', 'servers.api'], 16 | 'get_uuids': ['', 'servers.api'], 17 | 'invalidate_access_token': ['', 'servers.authserver'], 18 | 'is_valid_uuid': ['', 'utils.uuid'], 19 | 'refresh_access_token': ['', 'servers.authserver'], 20 | 'reset_skin': ['', 'servers.api'], 21 | 'signout_user': ['', 'servers.authserver'], 22 | 'upload_skin': ['', 'servers.api'], 23 | 'validate_access_token': ['', 'servers.authserver'] 24 | } 25 | 26 | 27 | def parse_imports(package, imports): 28 | imported = {import_name: [] for import_name in imports} 29 | for (import_name, module_paths) in imports.items(): 30 | for module_path in module_paths: 31 | split_module_path = module_path.split('.') 32 | package_path = '.'.join([PACKAGE] + split_module_path[:-1]) 33 | module_name = split_module_path[-1] 34 | package = import_module(package_path) 35 | 36 | if module_name: 37 | module = getattr(package, module_name) 38 | import_ = getattr(module, import_name) 39 | else: 40 | import_ = getattr(package, import_name) 41 | 42 | imported[import_name].append(import_) 43 | 44 | return imported 45 | 46 | 47 | def are_elements_identical(iterable): 48 | iterator = iter(iterable) 49 | try: 50 | first = next(iterator) 51 | except StopIteration: 52 | return True 53 | 54 | return all(first is rest for rest in iterator) 55 | 56 | 57 | def test_imports(): 58 | imported = parse_imports(PACKAGE, IMPORTS) 59 | for imports in imported.values(): 60 | assert are_elements_identical(imports) 61 | -------------------------------------------------------------------------------- /tests/test_uuids.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from mojang_api import get_uuid 4 | 5 | from pytest import users 6 | 7 | 8 | def test_uuids(): 9 | empirical_users = [(uuid.name, uuid.id) 10 | for uuid in [get_uuid(user[0]) for user in users]] 11 | assert empirical_users == users 12 | --------------------------------------------------------------------------------