Getting Started

Part 1, Chapter 3


In this chapter, we'll set up the base project structure.


To speed up development, we'll start with a pre-built Flask API.

The code for this comes from the RESTful API that was built in the Test-Driven Development with Python, Flask, and Docker course. Be sure to review the course for more details on how it was implemented.

Start by cloning down the project:

$ git clone https://gitlab.com/testdriven/flask-tdd-docker.git flask-react-auth
$ cd flask-react-auth

Main Commands

Build the images:

$ docker-compose build

Take a quick look at the project structure, while the images are building:

├── .coveragerc
├── .dockerignore
├── .gitignore
├── .gitlab-ci.yml
├── Dockerfile
├── Dockerfile.prod
├── README.md
├── docker-compose.yml
├── entrypoint.sh
├── manage.py
├── project
│   ├── __init__.py
│   ├── api
│   │   ├── __init__.py
│   │   ├── ping.py
│   │   └── users
│   │       ├── __init__.py
│   │       ├── admin.py
│   │       ├── models.py
│   │       ├── services.py
│   │       └── views.py
│   ├── config.py
│   ├── db
│   │   ├── Dockerfile
│   │   └── create.sql
│   └── tests
│       ├── __init__.py
│       ├── conftest.py
│       ├── pytest.ini
│       ├── test_admin.py
│       ├── test_config.py
│       ├── test_ping.py
│       ├── test_users.py
│       └── test_users_unit.py
├── release.sh
├── requirements.txt
└── setup.cfg

Important things to note:

  1. The "project" directory holds the Flask API, configuration, and tests.
  2. The docker-compose.yml file holds the configuration for building the images and for the Flask app (based on the Dockerfile in the project root) and a Postgres database.

Run the containers once the build is complete:

$ docker-compose up -d

Navigate to http://localhost:5001/ping in your browser.

You should see:

{
  "message": "pong!",
  "status": "success"
}

Create the database:

$ docker-compose exec users python manage.py recreate_db

Seed the database:

$ docker-compose exec users python manage.py seed_db

Navigate to http://localhost:5001/users.

You should see:

[
    {
        "id": 1,
        "username": "michael",
        "email": "[email protected]",
        "created_date": "2019-12-19T05:26:43.293514"
    },
    {
        "id": 2,
        "username": "michaelherman",
        "email": "[email protected]",
        "created_date": "2019-12-19T05:26:43.293514"
    }
]

Review then run the tests with coverage:

$ docker-compose exec users pytest "project/tests" -p no:warnings --cov="project"

You should see:

======================================== test session starts ========================================
platform linux -- Python 3.8.0, pytest-5.3.1, py-1.8.0, pluggy-0.13.1
rootdir: /usr/src/app/project/tests, inifile: pytest.ini
plugins: forked-1.1.3, cov-2.8.1, xdist-1.30.0
collected 32 items

project/tests/test_admin.py ..                                                                [  6%]
project/tests/test_config.py ...                                                              [ 15%]
project/tests/test_ping.py .                                                                  [ 18%]
project/tests/test_users.py .............                                                     [ 59%]
project/tests/test_users_unit.py .............                                                [100%]

----------- coverage: platform linux, python 3.8.0-final-0 -----------
Name                            Stmts   Miss Branch BrPart  Cover
-----------------------------------------------------------------
project/__init__.py                19      1      2      0    95%
project/api/__init__.py             6      0      0      0   100%
project/api/ping.py                 6      0      0      0   100%
project/api/users/__init__.py       0      0      0      0   100%
project/api/users/admin.py          7      0      0      0   100%
project/api/users/models.py        17      0      2      1    95%
project/api/users/services.py      24      0      0      0   100%
project/api/users/views.py         59      0      8      0   100%
project/config.py                  12      0      0      0   100%
-----------------------------------------------------------------
TOTAL                             150      1     12      1    99%


======================================== 32 passed in 1.68s =========================================

Lint with Flake8:

$ docker-compose exec users flake8 project

Run Black and isort with check options:

$ docker-compose exec users black project --check
$ docker-compose exec users /bin/sh -c "isort project/*/*.py --check-only"

Need to make changes based on Black and isort recommendations?

$ docker-compose exec users black project
$ docker-compose exec users /bin/sh -c "isort project/*/*.py"

Other Commands

To stop the containers:

$ docker-compose stop

To bring down the containers:

$ docker-compose down

Want to force a build?

$ docker-compose build --no-cache

Remove images:

$ docker rmi $(docker images -q)

Postgres

Want to access the database via psql?

$ docker-compose exec users-db psql -U postgres

Then, you can connect to the database and run SQL queries. For example:

# \c users_dev
# select * from users;

Project Structure

Make sure you have a solid grasp of the project structure along with the above Docker commands. Again, review the Test-Driven Development with Python, Flask, and Docker course for more details.

Let's refactor the project structure before adding React.

Bring down the existing containers:

$ docker-compose down

Add a new folder called "services" to the project root. Within that folder, create a new folder called "users". Then move the following files and folders to the "users" folder:

  • .coverage
  • .coveragerc
  • .dockerignore
  • Dockerfile
  • Dockerfile.prod
  • entrypoint.sh
  • manage.py
  • "project"
  • requirements.txt
  • setup.cfg

The structure should now look like:

├── .gitignore
├── .gitlab-ci.yml
├── README.md
├── docker-compose.yml
├── release.sh
└── services
    └── users
        ├── .coverage
        ├── .coveragerc
        ├── .dockerignore
        ├── Dockerfile
        ├── Dockerfile.prod
        ├── entrypoint.sh
        ├── manage.py
        ├── project
        │   ├── __init__.py
        │   ├── api
        │   │   ├── __init__.py
        │   │   ├── ping.py
        │   │   └── users
        │   │       ├── __init__.py
        │   │       ├── admin.py
        │   │       ├── models.py
        │   │       ├── services.py
        │   │       └── views.py
        │   ├── config.py
        │   ├── db
        │   │   ├── Dockerfile
        │   │   └── create.sql
        │   └── tests
        │       ├── __init__.py
        │       ├── conftest.py
        │       ├── pytest.ini
        │       ├── test_admin.py
        │       ├── test_config.py
        │       ├── test_ping.py
        │       ├── test_users.py
        │       └── test_users_unit.py
        ├── requirements.txt
        └── setup.cfg

Make the following changes to docker-compose.yml:

version: '3.7'

services:

  users:
    build:
      context: ./services/users  # updated
      dockerfile: Dockerfile
    entrypoint: ['/usr/src/app/entrypoint.sh']
    volumes:
      - './services/users:/usr/src/app'  # updated
    ports:
      - 5001:5000
    environment:
      - FLASK_ENV=development
      - APP_SETTINGS=project.config.DevelopmentConfig
      - DATABASE_URL=postgresql://postgres:[email protected]:5432/users_dev
      - DATABASE_TEST_URL=postgresql://postgres:[email protected]:5432/users_test
    depends_on:
      - users-db

  users-db:
    build:
      context: ./services/users/project/db  # updated
      dockerfile: Dockerfile
    expose:
      - 5432
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres

Build and spin up the new containers:

$ docker-compose up -d --build

Ensure http://localhost:5001/ping still works.

Create and seed the database:

$ docker-compose exec users python manage.py recreate_db
$ docker-compose exec users python manage.py seed_db

Ensure http://localhost:5001/users works as well.

Run the tests, flake8, Black, and isort:

$ docker-compose exec users pytest "project/tests" -p no:warnings --cov="project"
$ docker-compose exec users flake8 project
$ docker-compose exec users black project --check
$ docker-compose exec users /bin/sh -c "isort project/*/*.py --check-only"
$ docker-compose exec users black project
$ docker-compose exec users /bin/sh -c "isort project/*/*.py"

Update the README:

# Authentication with Flask, React, and Docker

HTTPie

Finally, test all the routes with HTTPie.

GET all users:

$ http GET http://localhost:5001/users

HTTP/1.0 200 OK
Content-Length: 316
Content-Type: application/json
Date: Thu, 19 Dec 2019 05:39:22 GMT
Server: Werkzeug/0.16.0 Python/3.8.0

[
    {
        "created_date": "2019-12-19T05:38:11.588741",
        "email": "[email protected]",
        "id": 1,
        "username": "michael"
    },
    {
        "created_date": "2019-12-19T05:38:11.588741",
        "email": "[email protected]",
        "id": 2,
        "username": "michaelherman"
    }
]

GET single user:

$ http GET http://localhost:5001/users/1

HTTP/1.0 200 OK
Content-Length: 128
Content-Type: application/json
Date: Thu, 19 Dec 2019 05:39:46 GMT
Server: Werkzeug/0.16.0 Python/3.8.0

{
    "created_date": "2019-12-19T05:38:11.588741",
    "email": "[email protected]",
    "id": 1,
    "username": "michael"
}

POST:

$ http --json POST http://localhost:5001/users username=someone email=[email protected]

HTTP/1.0 201 CREATED
Content-Length: 54
Content-Type: application/json
Date: Thu, 19 Dec 2019 05:40:01 GMT
Server: Werkzeug/0.16.0 Python/3.8.0

{
    "message": "[email protected] was added!"
}

PUT:

$ http --json PUT http://localhost:5001/users/3 username=foo email=[email protected]

HTTP/1.0 200 OK
Content-Length: 36
Content-Type: application/json
Date: Thu, 19 Dec 2019 05:40:16 GMT
Server: Werkzeug/0.16.0 Python/3.8.0

{
    "message": "3 was updated!"
}

DELETE:

$ http DELETE http://localhost:5001/users/3

HTTP/1.0 200 OK
Content-Length: 46
Content-Type: application/json
Date: Thu, 19 Dec 2019 05:40:30 GMT
Server: Werkzeug/0.16.0 Python/3.8.0

{
    "message": "[email protected] was removed!"
}



Mark as Completed