From 7bcbbc7409e68f4975f4533aa4fd38c66bcc65bf Mon Sep 17 00:00:00 2001 From: Nathan Adams Date: Fri, 26 Sep 2014 01:05:47 +0200 Subject: [PATCH] Added a MinecraftServer.lookup method, converts a string to host/port (including SRV lookup) This closes #12 --- .travis.yml | 4 ++-- README.md | 16 +++++++++----- mcstatus/server.py | 24 +++++++++++++++++++++ mcstatus/tests/test_server.py | 40 ++++++++++++++++++++++++++++++++++- requirements.txt | 2 +- requirements/base.txt | 2 ++ requirements/python2.txt | 3 +++ requirements/python3.txt | 3 +++ setup.py | 18 ++++++++++++++-- 9 files changed, 101 insertions(+), 11 deletions(-) create mode 100644 requirements/base.txt create mode 100644 requirements/python2.txt create mode 100644 requirements/python3.txt diff --git a/.travis.yml b/.travis.yml index 25d73a7..8690f99 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,6 @@ python: - 3.3 - 2.7 install: - - pip install -r requirements.txt --use-mirrors - - pip install mock --use-mirrors + - if [[ $TRAVIS_PYTHON_VERSION == 2* ]]; then pip install -r requirements/python2.txt --use-mirrors; fi + - if [[ $TRAVIS_PYTHON_VERSION == 3* ]]; then pip install -r requirements/python3.txt --use-mirrors; fi script: nosetests \ No newline at end of file diff --git a/README.md b/README.md index c3fff58..a09f928 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ mcstatus ======== `mcstatus` provides an easy way to query Minecraft servers for any information they can expose. -It provides two modes of access, `query` and `ping`, the differences of which are listed below in usage. +It provides three modes of access (`query`, `status` and `ping`), the differences of which are listed below in usage. Usage ----- @@ -10,15 +10,21 @@ Usage ```python from mcstatus import MinecraftServer -server = MinecraftServer("localhost", 25565) +# If you know the host and port, you may skip this and use MinecraftServer("example.org", 1234) +server = MinecraftServer.lookup("example.org:1234") + +# 'status' is supported by all Minecraft servers that are version 1.7 or higher. +status = server.status() +print("The server has {0} players and replied in {1} ms".format(status.players.online, status.latency)) # 'ping' is supported by all Minecraft servers that are version 1.7 or higher. -status, ping = server.ping_server() -print("The server has {0} players".format(status.players.online)) +# It is included in a 'status' call, but is exposed separate if you do not require the additional info. +latency = server.ping() +print("The server replied in {0} ms".format(latency)) # 'query' has to be enabled in a servers' server.properties file. # It may give more information than a ping, such as a full player list or mod information. -query = server.query_server() +query = server.query() print("The server has the following players online: {0}".format(", ".join(query.players.names))) ``` diff --git a/mcstatus/server.py b/mcstatus/server.py index 5e70c90..d0e1a1d 100644 --- a/mcstatus/server.py +++ b/mcstatus/server.py @@ -1,6 +1,7 @@ from mcstatus.pinger import ServerPinger from mcstatus.protocol.connection import TCPSocketConnection, UDPSocketConnection from mcstatus.querier import ServerQuerier +import dns.resolver class MinecraftServer: @@ -8,6 +9,29 @@ class MinecraftServer: self.host = host self.port = port + @staticmethod + def lookup(address): + host = address + port = None + if ":" in address: + parts = address.split(":") + if len(parts) > 2: + raise ValueError("Invalid address '%s'" % address) + host = parts[0] + port = int(parts[1]) + if port is None: + port = 25565 + try: + answers = dns.resolver.query("_minecraft._tcp." + host, "SRV") + if len(answers): + answer = answers[0] + host = str(answer.target).rstrip(".") + port = int(answer.port) + except Exception: + pass + + return MinecraftServer(host, port) + def ping(self, retries=3, **kwargs): attempt = 0 connection = TCPSocketConnection((self.host, self.port)) diff --git a/mcstatus/tests/test_server.py b/mcstatus/tests/test_server.py index e613b52..f6d02a6 100644 --- a/mcstatus/tests/test_server.py +++ b/mcstatus/tests/test_server.py @@ -81,4 +81,42 @@ class TestMinecraftServer(TestCase): with patch("mcstatus.server.ServerQuerier") as querier: querier.side_effect = [Exception, Exception, Exception] self.assertRaises(Exception, self.server.query) - self.assertEqual(querier.call_count, 3) \ No newline at end of file + self.assertEqual(querier.call_count, 3) + + def test_by_address_no_srv(self): + with patch("dns.resolver.query") as query: + query.return_value = [] + self.server = MinecraftServer.lookup("example.org") + query.assert_called_once_with("_minecraft._tcp.example.org", "SRV") + self.assertEqual(self.server.host, "example.org") + self.assertEqual(self.server.port, 25565) + + def test_by_address_invalid_srv(self): + with patch("dns.resolver.query") as query: + query.side_effect = [Exception] + self.server = MinecraftServer.lookup("example.org") + query.assert_called_once_with("_minecraft._tcp.example.org", "SRV") + self.assertEqual(self.server.host, "example.org") + self.assertEqual(self.server.port, 25565) + + def test_by_address_with_srv(self): + with patch("dns.resolver.query") as query: + answer = Mock() + answer.target = "different.example.org." + answer.port = 12345 + query.return_value = [answer] + self.server = MinecraftServer.lookup("example.org") + query.assert_called_once_with("_minecraft._tcp.example.org", "SRV") + self.assertEqual(self.server.host, "different.example.org") + self.assertEqual(self.server.port, 12345) + + def test_by_address_with_port(self): + self.server = MinecraftServer.lookup("example.org:12345") + self.assertEqual(self.server.host, "example.org") + self.assertEqual(self.server.port, 12345) + + def test_by_address_with_multiple_ports(self): + self.assertRaises(ValueError, MinecraftServer.lookup, "example.org:12345:6789") + + def test_by_address_with_invalid_port(self): + self.assertRaises(ValueError, MinecraftServer.lookup, "example.org:port") \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 97578ba..1ea7283 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -six==1.7.3 \ No newline at end of file +-r requirements/base.txt \ No newline at end of file diff --git a/requirements/base.txt b/requirements/base.txt new file mode 100644 index 0000000..e77b00b --- /dev/null +++ b/requirements/base.txt @@ -0,0 +1,2 @@ +six==1.7.3 +mock==1.0.1 \ No newline at end of file diff --git a/requirements/python2.txt b/requirements/python2.txt new file mode 100644 index 0000000..7da539c --- /dev/null +++ b/requirements/python2.txt @@ -0,0 +1,3 @@ +-r base.txt + +dnspython==1.12.0 \ No newline at end of file diff --git a/requirements/python3.txt b/requirements/python3.txt new file mode 100644 index 0000000..c9bb173 --- /dev/null +++ b/requirements/python3.txt @@ -0,0 +1,3 @@ +-r base.txt + +dnspython3==1.12.0 \ No newline at end of file diff --git a/setup.py b/setup.py index 3623524..d8c8eb2 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,18 @@ from distutils.core import setup +from six import PY2 + +install_requires = [ + 'six' +] + +if PY2: + install_requires.append('dnspython') +else: + install_requires.append('dnspython3') + +tests_require = [ + 'mock' +] setup( name='mcstatus', @@ -8,8 +22,8 @@ setup( url='https://pypi.python.org/pypi/mcstatus', packages=['mcstatus', 'mcstatus.protocol'], description='A library to query Minecraft Servers for their status and capabilities.', - install_requires=['six'], - tests_require=['mock'], + install_requires=install_requires, + tests_require=tests_require, classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers',