Tutorial: Building a Minecraft Microservice with Python 3

Post to Twitter

Probably many of you have heard of Microservices. They are essentially a small single-purpose, API accessible (typically REST) based application. You can build your microservices in a variety of programming languages or just one depending on your needs. Today I want to guide you through a Python 3 microservice that focuses on one thing and is self-contained in the sense of not requiring any external dependencies that need to be installed via PIP.

Note: It is my 2015 New Years resolution to write any future articles regarding Python using Python 3.

I’m going to create a new Python 3 project. I use tools pyenv to easily switch between Python versions. If you interested on how I setup my development environment please see my article on that.

Note: I keep all my Python projects inside a folder in my home folder called: PythonProjects

$ cd ~/PythonProjects
$ python3 -m venv mineservice_project
$ cd mineservice_project
$ source bin/activate
$ mkdir mineservice && cd mineservice
# yes, we are making another folder called mineservice inside the folder mineservice
$ mkdir mineservice
$ touch mineservice/__init__.py

The above shell commands will create a Python 3 environment for our project called mineservice_project and “turn on” the Python 3 interpreter for it. If we were to install any external packages they would be isolated to this environment and would not pollute the global python packages.

So the next question is: What do we build that could qualify as a microservice? I’ve been playing Minecraft for a while with my son and am still learning the “whos” and “whats” of the game so with that in mind we will write a simple microservice that returns the Minecraft mobs. Of course this is a pretty pointless application since one can just hit the wiki, but its easy to grasp and learn from so take it for what it is: something to learn from.

To get this running quickly and with no external PIP packages required I’m going to copy the structure I used in a previous article. Also, what I’m going add to this project is an entry point for a console script. What this will do is allow people to easily install this application and simply call the console script and voila – instantly its a running service. You could even put something like this onto PyPi and users can PIP install and then simply call the console script. However, we are getting ahead ourselves so let’s start with adding the files we need in the project first.

Grab the bottle.py file and store it inside the project folder named as mineservice/bottle.py.

Next, create a file called mineservice/mtwsgi.py and save it inside the mineservice folder with the bottle.py file. Add the following content to mtwsgi.py (I’ve modified it from here):

"""
WSGI-compliant HTTP server.  Dispatches requests to a pool of threads.
"""

from wsgiref.simple_server import WSGIServer, WSGIRequestHandler
import multiprocessing.pool

__all__ = ['ThreadPoolWSGIServer', 'make_server']


class ThreadPoolWSGIServer(WSGIServer):
    """
    WSGI-compliant HTTP server. Dispatches requests to a pool of threads.
    """

    def __init__(self, thread_count=None, *args, **kwargs):
        """
        If thread_count == None, we'll use multiprocessing.cpu_count() threads.
        """
        WSGIServer.__init__(self, *args, **kwargs)
        self.thread_count = thread_count
        self.pool = multiprocessing.pool.ThreadPool(self.thread_count)

    # Inspired by SocketServer.ThreadingMixIn.
    def process_request_thread(self, request, client_address):
        try:
            self.finish_request(request, client_address)
            self.shutdown_request(request)
        except:
            self.handle_error(request, client_address)
            self.shutdown_request(request)

    def process_request(self, request, client_address):
        self.pool.apply_async(
            self.process_request_thread, args=(request, client_address))


def make_server(
        host, port, app, thread_count=None, handler_class=WSGIRequestHandler):
    """
    Create a new WSGI server listening on `host` and `port` for `app`
    """
    httpd = ThreadPoolWSGIServer(thread_count, (host, port), handler_class)
    httpd.set_app(app)
    return httpd

Now its time to add our mineservice/app.py file to the project. This will be where our simple Minecraft Microservice will live. The contents of the file to start off with will look like this:

"""
Multithreading Bottle server adapter
"""
from mineservice import bottle
from mineservice import mtwsgi


class MTServer(bottle.ServerAdapter):
    def run(self, handler):
        thread_count = self.options.pop('thread_count', None)
        server = mtwsgi.make_server(
            self.host, self.port, handler, thread_count, **self.options)
        server.serve_forever()


if __name__ == '__main__':
    app = bottle.Bottle()

    @app.route('/')
    def version():
        app_ver = {
            "version": "1.0.0"
        }
        return app_ver

    app.run(server=MTServer, host='0.0.0.0', port=8080, thread_count=3)

Run the application now and hit the endpoint: http://0.0.0.0:8080/

You should see the following JSON response:

{"version": "1.0.0"}

Note: Bottle will handle setting the content-type header to application/json for us.

Stop the server and let’s edit the mineservice/app.py file some more by adding a little more functionality.

Contents of mineservice/app.py:

"""
Multithreading Bottle server adapter
"""
from mineservice import bottle
from mineservice import mtwsgi


class MTServer(bottle.ServerAdapter):
    def run(self, handler):
        thread_count = self.options.pop('thread_count', None)
        server = mtwsgi.make_server(
            self.host, self.port, handler, thread_count, **self.options)
        server.serve_forever()


if __name__ == '__main__':
    app = bottle.Bottle()

    mobs = {
        "passive": ["Chicken", "Cow", "Horse", "Ocelot", "Pig", "Sheep",
                    "Bat", "Mushroom", "Squid", "Villager"],
        "neutral": ["Cave Spider", "Enderman", "Spider", "Wolf",
                    "Zombie Pigman"],
        "hostile": ["Blaze", "Creep", "Endermite", "Ghast", "Magma Cube",
                    "Silverfish", "Skeleton", "Slime", "Spider Jockey",
                    "Witch", "Whither Skeleton", "Zombie",
                    "Zombie Villager", "Chicken Jockey", "Killer Bunny",
                    "Guardian", "Elder Guardian"],
        "utility": ["Snow Golem", "Iron Golem"],
        "boss": ["Whither", "Ender Dragon"]
    }

    @app.route('/')
    def version():
        app_ver = {
            "version": "1.0.0"
        }
        return app_ver

    @app.route('/mobs')
    def all_mobs():
        return mobs

    app.run(server=MTServer, host='0.0.0.0', port=8080, thread_count=3)

if __name__ == '__main__':
    main()

Run the application now and hit the endpoint to get all the mobs: http://0.0.0.0:8080/

You should see the following JSON response (your order may differ):

{
    "boss": [
        "Whither",
        "Ender Dragon"
    ],
    "neutral": [
        "Cave Spider",
        "Enderman",
        "Spider",
        "Wolf",
        "Zombie Pigman"
    ],
    "passive": [
        "Chicken",
        "Cow",
        "Horse",
        "Ocelot",
        "Pig",
        "Sheep",
        "Bat",
        "Mushroom",
        "Squid",
        "Villager"
    ],
    "hostile": [
        "Blaze",
        "Creep",
        "Endermite",
        "Ghast",
        "Magma Cube",
        "Silverfish",
        "Skeleton",
        "Slime",
        "Spider Jockey",
        "Witch",
        "Whither Skeleton",
        "Zombie",
        "Zombie Villager",
        "Chicken Jockey",
        "Killer Bunny",
        "Guardian",
        "Elder Guardian"
    ],
    "utility": [
        "Snow Golem",
        "Iron Golem"
    ]
}

What if we want to just return the passive mobs? Turns out that this is pretty simple:

@app.route('/mobs/passive')
def passive_mobs():
    return {"passive": mobs['passive']}

However it would get tedious to do that same code above for all the mod types. We can instead add a single route for fetching a single mob type like so:

@app.route('/mobs/<mob_type>')
def get_mob(mob_type):
    return {mob_type: mobs[mob_type]}

So now the mineservice/app.py code should look like the following:

"""
Multithreading Bottle server adapter
"""
from mineservice import bottle
from mineservice import mtwsgi


class MTServer(bottle.ServerAdapter):
    def run(self, handler):
        thread_count = self.options.pop('thread_count', None)
        server = mtwsgi.make_server(
            self.host, self.port, handler, thread_count, **self.options)
        server.serve_forever()


def main():
    app = bottle.Bottle()

    mobs = {
        "passive": ["Chicken", "Cow", "Horse", "Ocelot", "Pig", "Sheep",
                    "Bat", "Mushroom", "Squid", "Villager"],
        "neutral": ["Cave Spider", "Enderman", "Spider", "Wolf",
                    "Zombie Pigman"],
        "hostile": ["Blaze", "Creep", "Endermite", "Ghast", "Magma Cube",
                    "Silverfish", "Skeleton", "Slime", "Spider Jockey",
                    "Witch", "Whither Skeleton", "Zombie",
                    "Zombie Villager", "Chicken Jockey", "Killer Bunny",
                    "Guardian", "Elder Guardian"],
        "utility": ["Snow Golem", "Iron Golem"],
        "boss": ["Whither", "Ender Dragon"]
    }

    @app.route('/')
    def version():
        app_ver = {
            "version": "1.0.0"
        }
        return app_ver

    @app.route('/mobs')
    def all_mobs():
        return mobs

    @app.route('/mobs/<mob_type>')
    def get_mob(mob_type):
        return {mob_type: mobs[mob_type]}

    app.run(server=MTServer, host='0.0.0.0', port=8080, thread_count=3)

if __name__ == '__main__':
    main()

Run that code and try a few different route: http://0.0.0.0:8080/mobs/hostile or http://0.0.0.0:8080/mobs/passive

That works pretty good, but what if someone enters a mob that isn’t in our list like aggressive? Unfortunately they will get an HTTP 500 error. Let’s go ahead and fix that. Modify the get_mob function to look like this:

@app.route('/mobs/<mob_type>')
def get_mob(mob_type):
    try:
        return {mob_type: mobs[mob_type]}
    except KeyError:
        return {"Error": "{} is not a supported mob type".format(mob_type)}

Now when they use an invalid mob type they will see an error (also in JSON format):

{"Error": "aggressive is not a supported mob type"}

The final step of our service will be to add a setup.py file so we can define an entry point and install this application to run it as a console script. Add the setup.py file to the root mineservice folder of the project along with the following code:

from setuptools import setup
 
setup(
    name='mineservice',
    version='1.0.0',
    packages=['mineservice'],
    entry_points={
        'console_scripts': [
            'mineservice = mineservice.app:main'
        ]
    }
)

Save the file and run the following command to install it (normally you won’t be installing into your project workspace but for testing this is fine):

$ python setup.py install

Verify the package was installed:

$ pip freeze

The result should be:

mineservice==1.0.0

Run the service:

$ mineservice

The web server should start and you can try the supported routes in your browser like this one: http://0.0.0.0:8080/mobs/neutral

JSON Results:

{"neutral": ["Cave Spider", "Enderman", "Spider", "Wolf", "Zombie Pigman"]}

To uninstall mineservice:

$ pip uninstall mineservice

So there you have it, the groundwork for a simple Python Microservice. In a future article I plan to show you how to expand on this idea even more and make a Python Microservice that doesn’t even require the host operating system even have Python installed thus creating a very self-contained microservice.

Update Sept. 16: 2015: – See the self-contained Python article app here: http://www.giantflyingsaucer.com/blog/?p=5680

Post to Twitter

This entry was posted in Open Source, Python. Bookmark the permalink.