This post talks about how I refactored a Flask application and subsequently ran into a gotcha with Google App Engine's defaults, and how to resolve it.

Backstory: I previously wrote about creating a Slack app in Python. At the time, the app was pretty small, so all of my logic went into a main.py. However, that's not good development practice, and the file became hard to read at 500 lines long. I also wanted to add in things like config files, as the list of things to configure was growing longer. So I decided it was time to restructure the entire application and abide more by conventions.

I followed the Flask docs on structuring larger applications. I created separated different kinds of logic, like route handling and error handling, into different files. My end result looked something like this:

/myapp
    setup.py
    test.py
    /myapp
        __init__.py
        routes.py
        app_logic.py
        config.py
        /errors
            handlers.py
            exceptions.py

Unfortunately, when I actually deployed it to GAE, it blew up. Specifically, the GAE logs had the following error:

"Traceback (most recent call last):
  File "/env/lib/python3.7/site-packages/gunicorn/arbiter.py", line 583, in spawn_worker
    worker.init_process()
  File "/env/lib/python3.7/site-packages/gunicorn/workers/gthread.py", line 104, in init_process
    super(ThreadWorker, self).init_process()
  File "/env/lib/python3.7/site-packages/gunicorn/workers/base.py", line 129, in init_process
    self.load_wsgi()
  File "/env/lib/python3.7/site-packages/gunicorn/workers/base.py", line 138, in load_wsgi
    self.wsgi = self.app.wsgi()
  File "/env/lib/python3.7/site-packages/gunicorn/app/base.py", line 67, in wsgi
    self.callable = self.load()
  File "/env/lib/python3.7/site-packages/gunicorn/app/wsgiapp.py", line 52, in load
    return self.load_wsgiapp()
  File "/env/lib/python3.7/site-packages/gunicorn/app/wsgiapp.py", line 41, in load_wsgiapp
    return util.import_app(self.app_uri)
  File "/env/lib/python3.7/site-packages/gunicorn/util.py", line 350, in import_app
    __import__(module)
ModuleNotFoundError: No module named 'main'"

Turns out that according to Google's docs, GAE looks for a main.py in the application directory root containing a variable called app, which is the application. Since I switched to putting the app in myapp/__init__.py, GAE couldn't find this anymore.

The suggested solution is to add a custom entrypoint to your app.yaml such as entrypoint: gunicorn -b :$PORT myapp:app. However, that didn't work for me. So what I did was to simply conform to what GAE wanted, and added a dummy main.py which imports my application as app. The file only contains the following:

from myapp import app

And now my folder structure looks like this:

/myapp
    main.py
    test.py
    setup.py
    /myapp
        __init__.py
        routes.py
        app_logic.py
        config.py
        /errors
            handlers.py
            exceptions.py

Maybe not the most elegant solution, but it got my app working on GAE.