A simple but comprehensive REST API for tic-tac-toe.
This is a solution to a coding assignment sent by Anaconda. The problem statement can be viewed here.
This application is based on the following:
- Flask: a lightweight web application framework for Python.
- Connexion: an OpenAPI first framework on top of Flask.
- Flask-Injector: adding dependency injection for Flask.
- Redis: a key-value data store.
- Poetry: a tool to manage Python dependencies.
Make sure you have docker-compose
installed, then just run:
docker-compose up # You can pass the option `-d` for detached mode
This will start 2 services:
- flask: Flask app, by default running on 0.0.0.0:5000. See
Dockerfile
for full config. - redis: Redis storage backend with enabled persistence.
You can visit http://0.0.0.0:5000/api/ui/ to see the OpenAPI UI and try the
API in a browser. There is a description for every operation and further down in Schemas
you can
find an explanation for the fields accepted/returned by the API.
There are 2 demo shell scripts that can let your try the app:
./new_game.sh
export DEMO_GAME_ID="<COPY_GAME_ID"
./play_game.sh
To run the automated tests you need to install the dev dependencies.
Poetry is a relatively new tool to package Python projects and manage their dependencies. It's probably not worth it for such a small app but it was cool to try. You can skip to next section if you want to use a virtual env.
If you are still reading, install Poetry, then run:
poetry run pytest -q --flake8 --isort --cov=.
For convenience, a requirements.txt
file has been generated using Poetry.
Run these commands:
python3.6 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
pytest -q --flake8 --isort --cov=.
These tests ran successfully on Ubuntu 18.04.3 LTS
.
What could we do if we had twice the time?
- Tests: currently only having unit tests, we should add a fixture representing a test client then make API calls. This fixture can have a mocked Storage or FakeStrictRedis injected into the app.
- UI/CLI: add a wrapper that lets users interact with the API to list available games, start a new game and play rounds.
- Undo: something cool we could add is an "undo" endpoint which can simply remove the latest move and allow a player to make another move.
I took the freedom to deviate from this requirement:
POST /api/games/<id>: Update the Game with the given ID, replacing its data with the newly POSTed data.
First, I think if we want to update an existing game to a new state we should use
PUT
instead. POST
is known to be non-idempotent as it usually creates new
resources, therefore repeating the same POST
request should not keep our
server in the exact same state, which is not respected by this requirement.
Second, PUT
usually requires sending all the resource data which makes it
idempotent, unfortunately for our case this is a waste because we know that
the only thing that a player can add while playing a game is one single move.
So for these reasons, I made the choice to change this to:
POST /api/games/<id>/moves
This design allows us to easily build a snapshot of the board by combining all moves but additionally it offers a historical view on it, which we could use for example to show a replay of finished games or add an undo functionality.