Simple REST API that, given a Pokemon name, returns its Shakesperean description for TrueLayer.
The application exposes a single endpoint /pokemon/<name>
which can be queried
via GET
method passing the pokemon name and it will return a JSON in the form of
{
"name": "<pokemon_name>",
"description": "<Shakesperean description>"
}
In case of error of any type it returns a 404 - Not found
with the explanation of
what happened.
Dependencies:
- python >= 3.7
- sqlite (optional)
The project is written in python, it's advisable to create a virtual environment
to test it or use the Dockerfile
to run it inside a container.
Install dependencies
$ pip install -r requirements.txt
Set the configuration by env variable
$ export APP_CONFIG=pokespeare.config.DevelopmentConfig
Run application
$ python start.py
Docker build & run
The host can be set only for pokespeare.config.ProductionConfig
as for the development one it's standard on 127.0.0.1
$ docker build -t pokespeare . && docker run --rm -e "HOST=0.0.0.0" -p5000:5000 pokespeare
Run tests
$ python setup.py test
Configuration
There two configurations currently that can be set via ENV:
pokespeare.config.DevelopmentConfig
pokespeare.config.ProductionConfig
Practically every configuration key can be set via ENV variables, currently support:
Dev and prod.
CACHE_NAME
The name of the cachesqlite
DB or the namespace inredis
CACHE_BACKEND
The backend of the cache layer. Can be eithermemory
,sqlite
, orredis
CACHE_EXPIRATION
The eviction time of each key in the cache. Default to 3600 secondsPOKEMON_API_URL
The URL of thepokeapi.co/v2
serviceTRANSLATOR_API_URL
The URL of thefuntranslations.com
serviceTRANSLATOR_API_KEY
The optional API key forfuntranslations.com
WSGI_SERVER
TheWSGI
server to adopt, can be eitherFlask
(better for dev) orgunicorn
Production only
HOST
Address to listen on (only withWSGI_SERVER=gunicorn
)PORT
Port to listen on (only withWSGI_SERVER=gunicorn
)
- Exploration of the domain, analysis of the external services documentations (return codes, format, limitations etc.)
- Rough estimation of the usage, traffic volume and possible future extensions
- Exploration and tradeoffs over the frameworks and libraries to adopt
- Development, mainly following a mixed TDD approach and prototyping a simple solution
- Improvements and refines
For the development of the application I decided to use python as it's the language I feel more comfortable with at the moment and the one which I know most frameworks and best practices overall.
The task is pretty simple but I tried to implement it with an eye for maintenability and extensibility of the code, for this reason the final result can seem a bit of an overkill in some parts but I also wrote some hints on the code to express my opinions on what could have been a better choice to take.
-
Flask: battle-tested and the simplest solution for the domain, it's lightweight and being the problem essentially a stateless computation, I thought there was no need for fancy features extensions like
Flask-Restplus
or speedsters performance frameworks likefastapi
(automatic swagger documentations comes handy) orasync/await
based frameworks. Flask is more than capable of handling the volume of requests. If the traffic volume is expected to grow rapidly in the short-term (unlikely, given the hard-cap of the shakesperean-translator of 12500 calls/day with the ultra plan) it's trivial to increase nodes and put a load-balancer in front of the application, spreading it into multiple processes on a cluster. -
Requests: Again, the most widespread library for HTTP calls, it does one thing and it does it well. Also there's no possibility of making the
pokeappi.co/v2
call and thefuntranslations.com
call concurrently as their sequentials:funtranslations.com
needs the result of thepokeappi.co/v2
call to give a result. -
Requests-cache: A super simple caching layer working out-of-the box paired with requests module to cache requests as also advised by
pokeappi.co/v2
documentation. Supports redis, mongodb, sqlite and also simple memory dict-based cache. I have to admit I'm not a huge fan of these under-the-hood monkey-patching implementations or having a global objects like the request one in flask but it's a common pattern in python as long as it's well-tested, and given the size of the project it's a good, fast solution that fulfill all the needs. For something more complex I'd probably switch to different solutions or design a caching layer from scratch with more control over the logics. -
Marshmallow: JSON marshalling can be achieved in various ways and python offers built-ins that allow to handle them easily even without the needs of external libraries. Though I decided to use a marshalling library as it make simpler validations and typechecking without writing too much code, and it comes handy for future extensions of the codebase as it encapsulate all the serializations/deserializations logics.
I also made some assumptions:
- Expecting a low to medium traffic volume, given the limitations on the external services the API relies on, anyway it's easy to scale-out the application being it a stateless computation demanded to external APIs which ultimately define the limits of the app.
- An error at any stage results in a 404 - Not Found response
Assuming a noticeable increase of features and traffic for the future I'd probably switch Flask with uvicorn ASGI based FastApi and Requests with aiohttp or some other async solutions to increase performance by several orders of magnitude.
A solution to tackle the problem of the scarce number of calls available toward
funtranslations.com
beside the cache, could be to store results at each new
call in a persistent DB avoiding calls each time the cache is flushed.
For the caching layer I've opted for simple in-memory dictionary for
development and sqlite
for a persisted solution just to give the idea, it's
easy anyway to add redis for example as it's natively supported by the
requests-cache module adopted.
I decided to implement a wrapper around the requests
library as I think it
gives more flexibility in terms of testing and extensibility for the future, to
be honest I've to say I'm still not completely convinced of the usage of
abstractclasses
in python for classes other than containers as I think it
clashes a bit with the duck-typing nature of the language but still I decided
to create a basic interface to enforce some kind of "type-safety".
I didn't reach 100% test coverage as some parts I assume are already covered by the external library they relies on as I don't make any modifications beside calling their public methods (ex: pokespeare.http module).
I've tested a rough draft in Rust as well, very basic as I think I don't have enough experience with the language. Anyway the libraries I explored/adopted were rocket as webframework with an eye for worp but being more async oriented I ditched it. serde for JSON marshalling and reqwest to make requests to external APIs.
Beside the good old borrow checker, I think the main thing I found myself unaccostumed it's the error handling and missing fields, but mainly because of habit with exceptions system or plain old return codes C/Go like; but I think it become very intuitive pretty fast and very powerful and neat as well.
I really like cargo and the toolchain offered, way better than the python one, and, like in python, the battery included unit testing module; definetely interesting.