How to Create and Host a Cloud Python 3 Web App FREE (Part 3)
Introduction
In part 1 and part 2 you learned how to create a simple asynchronous Python 3 web app. It doesn’t do anything besides spit out some text. In this tutorial we’ll re-arrange the files slightly, and add some further cool asynchronous functionality.
Python 3 project file structure
Many complicated web applications will have a dozen or more routes, such as /tracks
, /artists
, /albums
, and so on. And each of these routes needs a handler to perform the appropriate functionality.
Without splitting up the code into multiple files, or modules, you’ll end up with messy, hard to maintain code.
A common file structure for an imaginary Python 3 web app project called Zeus could be
zeus/ ├── tests/ │ ├── test_zeus.py │ └── test_utilities.py ├── zeus/ | ├── handlers.py | ├── routes.py │ ├── utilities.py │ └── zeus.py ├── app.py ├── README.md ├── requirements.txt └── start.sh
Where you have the a folder called zeus/
containing all of the files.
Unit Tests
The tests/
folder contains the Unit Tests for your project. Unit tests aid you in maintaining the accurate functionality of your application, as you start to expand and improve upon it.
As a trivial example of a unit test, imagine your zeus/utilities.py
module had a function to calculate the total length of a music album, by taking in a list of tracks objects (each object has properties like track_name
, artist_name
, and track_length_seconds
).
To make sure that this function named calculate_album_length
worked as expected you could create a unit test that looked like this:
from zeus.utilities import calculate_album_length def test_calculate_album_length(): """"This function tests the calculate_album_length function from zeus/utilities.py """ # Create a list of dummy tracks tracks = [{ 'track_name': 'What Makes You Happy', 'artist_name': 'Edie Brickell', 'track_length': 242, }, { 'track_name': 'Superhero', 'artist_name': 'Edie Brickell', 'track_length': 274, }] calculated_length = calculated_album_length(tracks) assert calculated_length == 516
The above code first imports a function from the utilities module.
It then creates a Python list
of dummy tracks called tracks
(each track is respresented as a simple Python dict
).
The tracks
variable is then passed into the calculated_album_length
function from the .../zeus/utilities.py
file, and we ensure that what was calculated is equal to 516 (242 + 274).
The unit test above doesn’t care how calculate_album_length
actually calcuates the total length, it just cares that the final answer is correct.
It’s highly recommended for any even slightly significate applications to have accurate unit tests to make sure you don’t unintentionally break some functionality as you expand upon the functionality of the app, and especially as you optimize/improve existing code.
If you start your large project by thinking about, and creating the unit tests first you’ll usually end up writing better code. Writing & thiking about code and apps in this tests-first way is like thinking of what a consumer actually wants, and even what consumer’s experience will be like for any consumer product.
I find that, especially if you’re an in-experienced developer, by diving straight into implementation details immediately you end up wasting time creating code you don’t need, and the code you do write is more difficult to use.
Core Code
Under this main folder there’s another zeus
folder with the main guts of the application.
* utilities.py
could contain some general utility functions, such as the calculate_album_length
function as described above
* handlers.py
could contain the functions to handle the routes for the aiohttp web application.
* routes.py
could map the routes, like /login
and /tracks/
to the appropriate handlers
handlers.py
# /zeus/zeus/handlers.py from zeus.zeus import get_tracks async def index_handler(request): ... async def tracks_handler(request): # get the album name from the request album_name = request.match_info.get('album_name') tracks_text = await get_tracks(album_name) return web.Response(text=tracks_text) async def login_handler(request): ...
routes.py
# /zeus/zeus/handlers.py from zeus.handlers import index_handler, tracks_handler, login_handler def setup_routes(app): """take an aiohttp app object and setup the routes""" app.router.add_routes([ web.get('/', index_handler), web.get('/tracks', tracks_handler), web.post('/login', login_handler) ])
The zeus.py could have the main code, for example the imaginary get_tracks
function.
async def get_tracks(album_name): """make an HTTP request to some external service to get a list of tracks""" ...
Finally app.py
ties all of the above together into an aiohttp
web Application, similar to what you created in Part 1 of this tutorial.
# app.py from aiohttp import web from zeus.routes import setup_routes if __name__ == '__main__': app = web.Application() #app.router.add_get('/', handle_index) setup_routes(app) web.run_app(app)
The end result is an app that you can use to get a list of track names from an album
You now have a working web app that uses aiohttp
for two purposes
1. aiohttp.web
to run a web app service with two independant functional routes
2. aiohttp.ClientSession()
to make HTTP requests to an external server to get a resource.
Next steps
In Part 4 we’ll expand upon the app and take full advantage of the asynchronous nature of aiohttp
to show how powerful async is, as compared to non-async code.