.. _aiohttp-demos-polls-getting-started:
Getting started
---------------
aiohttp server is built around :class:`aiohttp.web.Application` instance.
It is used for registering *startup*/*cleanup* signals, connecting routes etc.
The following code creates an application::
# main.py
from aiohttp import web
app = web.Application()
web.run_app(app)
Save it under ``aiohttpdemo_polls/main.py`` and start the server using:
.. code-block:: shell
$ python3 main.py
======== Running on http://0.0.0.0:8080 ========
(Press CTRL+C to quit)
Next, open the displayed link in a browser. It returns a ``404: Not Found``
error. To show something more meaningful than an error, let's create a route
and a view.
.. _aiohttp-demos-polls-views:
Views
-----
Let's start with the first views. Create the file ``aiohttpdemo_polls/views.py``
and add the following to it::
# views.py
from aiohttp import web
async def index(request):
return web.Response(text='Hello Aiohttp!')
This ``index`` view is the simplest view possible in Aiohttp.
Now, we should create a route for this ``index`` view. Put the following into
``aiohttpdemo_polls/routes.py``. It is a good practice to separate views,
routes, models etc. You'll have more of each file type, and it is nice to group
them into different places::
# routes.py
from views import index
def setup_routes(app):
app.router.add_get('/', index)
We should add a call to the ``setup_routes`` function somewhere. The best place
to do this is in ``main.py``::
# main.py
from aiohttp import web
from routes import setup_routes
app = web.Application()
setup_routes(app)
web.run_app(app)
Start server again using ``python3 main.py``. This time when we open the browser
we see::
Hello Aiohttp!
**Success!** Now, your working directory should look like this:
.. code-block:: none
.
├── ..
└── polls
└── aiohttpdemo_polls
├── main.py
├── routes.py
└── views.py
.. _aiohttp-demos-polls-configuration-files:
Configuration files
-------------------
.. note::
aiohttp is configuration agnostic. It means the library does not
require any specific configuration approach, and it does not have built-in
support for any config schema.
Please note these facts:
1. 99% of servers have configuration files.
2. Most products (except Python-based solutions like Django and
Flask) do not store configs with source code.
For example Nginx has its own configuration files stored by default
under ``/etc/nginx`` folder.
MongoDB stores its config as ``/etc/mongodb.conf``.
3. Config file validation is a good idea. Strong checks may prevent
unnecessary errors during product deployment.
Thus, we **suggest** to use the following approach:
1. Push configs as ``yaml`` files (``json`` or ``ini`` is also
good but ``yaml`` is preferred).
2. Load ``yaml`` config from a list of predefined locations,
e.g. ``./config/app_cfg.yaml``, ``/etc/app_cfg.yaml``.
3. Keep the ability to override a config file by a command line
parameter, e.g. ``./run_app --config=/opt/config/app_cfg.yaml``.
4. Apply strict validation checks to loaded dict. `trafaret
`_, `colander
`_
or `JSON schema
`_ are good
candidates for such job.
One way to store your config is in folder at the same level as `aiohttpdemo_polls`.
Create a ``config`` folder and config file at desired location. E.g.:
.. code-block:: none
.
├── ..
└── polls <-- [BASE_DIR]
│
├── aiohttpdemo_polls
│ ├── main.py
│ ├── routes.py
│ └── views.py
│
└── config
└── polls.yaml <-- [config file]
Create a ``config/polls.yaml`` file with meaningful option names:
.. code-block:: yaml
# polls.yaml
postgres:
database: aiohttpdemo_polls
user: aiohttpdemo_user
password: aiohttpdemo_pass
host: localhost
port: 5432
minsize: 1
maxsize: 5
Install ``pyyaml`` package::
$ pip install pyyaml
Let's also create a separate ``settings.py`` file. It helps to leave ``main.py``
clean and short::
# settings.py
import pathlib
import yaml
BASE_DIR = pathlib.Path(__file__).parent.parent
config_path = BASE_DIR / 'config' / 'polls.yaml'
def get_config(path):
with open(path) as f:
config = yaml.load(f)
return config
config = get_config(config_path)
Next, load the config into the application::
# main.py
from aiohttp import web
from settings import config
from routes import setup_routes
app = web.Application()
setup_routes(app)
app['config'] = config
web.run_app(app)
Now, try to run your app again. Make sure you are running it from ``BASE_DIR``::
$ python aiohttpdemo_polls/main.py
======== Running on http://0.0.0.0:8080 ========
(Press CTRL+C to quit)
For the moment nothing should have changed in application's behavior. Since we
see no errors, we succeeded in learning how to configure our application.
.. _aiohttp-demos-polls-database:
Database
--------
Server
^^^^^^
Here, we assume that you have running database and a user with write access.
Refer to :ref:`aiohttp-demos-polls-preparations-database` for details.
Schema
^^^^^^
We will use SQLAlchemy to describe database schema for two related models,
``question`` and ``choice``::
+---------------+ +---------------+
| question | | choice |
+===============+ +===============+
| id | <---+ | id |
+---------------+ | +---------------+
| question_text | | | choice_text |
+---------------+ | +---------------+
| pub_date | | | votes |
+---------------+ | +---------------+
+-------- | question_id |
+---------------+
Create ``db.py`` file with database schemas::
# db.py
from sqlalchemy import (
MetaData, Table, Column, ForeignKey,
Integer, String, Date
)
meta = MetaData()
question = Table(
'question', meta,
Column('id', Integer, primary_key=True),
Column('question_text', String(200), nullable=False),
Column('pub_date', Date, nullable=False)
)
choice = Table(
'choice', meta,
Column('id', Integer, primary_key=True),
Column('choice_text', String(200), nullable=False),
Column('votes', Integer, server_default="0", nullable=False),
Column('question_id',
Integer,
ForeignKey('question.id', ondelete='CASCADE'))
)
.. note::
It is possible to configure tables in a declarative style like so:
.. code-block:: python
class Question(Base):
__tablename__ = 'question'
id = Column(Integer, primary_key=True)
question_text = Column(String(200), nullable=False)
pub_date = Column(Date, nullable=False)
But it doesn't give much benefits later on. SQLAlchemy ORM doesn't work in
asynchronous style and as a result ``aiopg.sa`` doesn't support related ORM
expressions such as ``Question.query.filter_by(question_text='Why').first()``
or ``session.query(TableName).all()``.
You still can make ``select`` queries after some code modifications:
.. code-block:: python
from sqlalchemy.sql import select
result = await conn.execute(select([Question]))
instead of
.. code-block:: python
result = await conn.execute(question.select())
But it is not as easy to deal with as update/delete queries.
Now we need to create tables in database as it was described with sqlalchemy.
Helper script can do that for you. Create a new file, ``init_db.py``::
# init_db.py
from sqlalchemy import create_engine, MetaData
from settings import config
from models import question, choice
DSN = "postgresql://{user}:{password}@{host}:{port}/{database}"
def create_tables(engine):
meta = MetaData()
meta.create_all(bind=engine, tables=[question, choice])
def sample_data(engine):
conn = engine.connect()
conn.execute(question.insert(), [
{'question_text': 'What\'s new?',
'pub_date': '2015-12-15 17:17:49.629+02'}
])
conn.execute(choice.insert(), [
{'choice_text': 'Not much', 'votes': 0, 'question_id': 1},
{'choice_text': 'The sky', 'votes': 0, 'question_id': 1},
{'choice_text': 'Just hacking again', 'votes': 0, 'question_id': 1},
])
conn.close()
if __name__ == '__main__':
db_url = DSN.format(**config['postgres'])
engine = create_engine(db_url)
create_tables(engine)
sample_data(engine)
.. note::
A more advanced version of this script is mentioned in :ref:`aiohttp-demos-polls-preparations-database` notes.
Install the ``aiopg[sa]`` package to interact with the database and run the script::
$ pip install aiopg[sa]
$ python init_db.py
.. note::
At this point we are not using any async features of the package. For this
reason, you could have installed ``psycopg2`` package. Though since we are
using sqlalchemy, we also could switch the type of database server.
Now there should be one record for *question* with related *choice* options
stored in corresponding tables in the database.
.. _aiohttp-demos-polls-creating-connection-engine:
Creating connection engine
^^^^^^^^^^^^^^^^^^^^^^^^^^
For making DB queries we need an engine instance. Assuming ``conf`` is
a :class:`dict` with the configuration info for a Postgres connection, this
could be done by the following coroutine:
.. literalinclude:: ../demos/polls/aiohttpdemo_polls/db.py
:pyobject: init_pg
The best place for connecting to the DB is using the
:attr:`~aiohtp.web.Application.on_startup` signal::
app.on_startup.append(init_pg)
.. _aiohttp-demos-polls-graceful-shutdown:
Graceful shutdown
^^^^^^^^^^^^^^^^^
It is a good practice to close all resources on program exit.
Let's close the DB connection with the :attr:`~aiohtp.web.Application.on_cleanup`
signal::
app.on_cleanup.append(close_pg)
.. literalinclude:: ../demos/polls/aiohttpdemo_polls/db.py
:pyobject: close_pg
.. _aiohttp-demos-polls-templates:
Templates
---------
Let's add more views:
.. literalinclude:: ../demos/polls/aiohttpdemo_polls/views.py
:pyobject: poll
Templates are a very convenient way for web page writing. If we return a
dict with page content, the ``aiohttp_jinja2.template`` decorator
processes the dict using the jinja2 template renderer.
For setting up the template engine, we install the ``aiohttp_jinja2``
library first:
.. code-block:: shell
$ pip install aiohttp_jinja2
After installing, we setup the library::
import aiohttp_jinja2
import jinja2
aiohttp_jinja2.setup(
app, loader=jinja2.PackageLoader('aiohttpdemo_polls', 'templates'))
In the tutorial we place template files under
``polls/aiohttpdemo_polls/templates`` folder.
.. _aiohttp-demos-polls-static-files:
Static files
------------
Any web site has static files such as: images, JavaScript sources, CSS files
The best way to handle static files in production is by setting up a reverse
proxy like NGINX or using CDN services.
During development, handling static files using the aiohttp server is very
convenient.
Fortunately, this can be done easily by a single call:
.. literalinclude:: ../demos/polls/aiohttpdemo_polls/routes.py
:pyobject: setup_static_routes
where ``project_root`` is the path to the root folder.
.. _aiohttp-demos-polls-middlewares:
Middlewares
-----------
Middlewares are stacked around every web-handler. They are called
before the handler for a pre-processing request. After getting
a response back, they are used for post-processing the given response.
A common use of middlewares is to implement custom error pages. Example from
:ref:`aiohttp-web-middlewares` documentation will render 404 errors using a
JSON response, as might be appropriate for a REST service.
Here we'll create a little bit more complex middleware custom display pages
for *404 Not Found* and *500 Internal Error*.
Every middleware should accept two parameters, a *request* and a *handler*,
and return the *response*. Middleware itself is a *coroutine* that can modify
either request or response:
Now, create a new ``middlewares.py`` file:
.. literalinclude:: ../demos/polls/aiohttpdemo_polls/middlewares.py
As you can see, we do nothing *before* the web handler. We choose Jinja2
template renderer based on ``response.status`` *after* the request was handled.
In case of exceptions, we do something similar, based on ``ex.status``.
Without the ``create_error_middleware`` function, the same task would take us
many more ``if`` statements.
We have registered middleware in ``app`` by adding it to ``app.middlewares``.
Now, add a ``setup_middlewares`` step to the main file:
.. code-block:: python
:emphasize-lines: 6, 10
# main.py
from aiohttp import web
from settings import config
from routes import setup_routes
from middlewares import setup_middlewares
app = web.Application()
setup_routes(app)
setup_middlewares(app)
app['config'] = config
web.run_app(app)
Run the app again. To test, try an invalid url.