Skip to content

Commit 88b3614

Browse files
committed
Refactored to use Flask app factory pattern
1 parent 2cc2570 commit 88b3614

File tree

10 files changed

+127
-64
lines changed

10 files changed

+127
-64
lines changed

.devcontainer/docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ services:
1111
- ..:/app
1212
command: sleep infinity
1313
environment:
14-
FLASK_APP: service:app
14+
FLASK_APP: wsgi:app
1515
FLASK_DEBUG: "True"
1616
CLOUDANT_HOST: couchdb
1717
CLOUDANT_PORT: 5984

.github/workflows/build.yml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,19 +38,22 @@ jobs:
3838

3939
- name: Install dependencies
4040
run: |
41-
python -m pip install --upgrade pip wheel
42-
pip install -r requirements.txt
41+
python -m pip install -U pip poetry
42+
poetry config virtualenvs.create false
43+
poetry install
4344
4445
- name: Create the test database
4546
run: |
4647
apt-get update
4748
apt-get install -y curl
4849
curl -X PUT http://admin:pass@couchdb:5984/test
4950
50-
- name: Run unit tests with green
51-
run: green
51+
- name: Run unit tests with pytest
52+
run: pytest --pspec --cov=service --cov-fail-under=95 --disable-warnings
5253
env:
54+
FLASK_APP: "wsgi:app"
5355
BINDING_CLOUDANT: '{"username":"admin","password":"pass","host":"couchdb","port":5984,"url":"http://admin:pass@couchdb:5984"}'
5456

5557
- name: Upload code coverage
5658
uses: codecov/codecov-action@v3.1.4
59+

Dockerfile

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
##################################################
2+
# Create production image
3+
##################################################
4+
FROM python:3.11-slim
5+
6+
# Establish a working folder
7+
WORKDIR /app
8+
9+
# Establish dependencies
10+
COPY pyproject.toml poetry.lock ./
11+
RUN python -m pip install poetry && \
12+
poetry config virtualenvs.create false && \
13+
poetry install --without dev
14+
15+
# Copy source files last because they change the most
16+
COPY wsgi.py .
17+
COPY service ./service
18+
19+
# Switch to a non-root user and set file ownership
20+
RUN useradd --uid 1001 flask && \
21+
chown -R flask:flask /app
22+
USER flask
23+
24+
# Expose any ports the app is expecting in the environment
25+
ENV FLASK_APP=wsgi:app
26+
ENV PORT 8080
27+
EXPOSE $PORT
28+
29+
ENV GUNICORN_BIND 0.0.0.0:$PORT
30+
ENTRYPOINT ["gunicorn"]
31+
CMD ["--log-level=info", "wsgi:app"]

Procfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
web: gunicorn --bind 0.0.0.0:$PORT --log-level=info service:app
1+
web: gunicorn --bind 0.0.0.0:$PORT --log-level=info wsgi:app

service/__init__.py

Lines changed: 63 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -30,60 +30,73 @@
3030
# The Flask app must be created
3131
# BEFORE you import modules that depend on it !!!
3232

33-
# Create the Flask app
34-
app = Flask(__name__)
35-
app.config.from_object(config)
36-
37-
# Turn off strict slashes because it violates best practices
38-
app.url_map.strict_slashes = False
39-
4033
# Document the type of authorization required
4134
authorizations = {
4235
"apikey": {
43-
"type": "apiKey",
44-
"in": "header",
36+
"type": "apiKey",
37+
"in": "header",
4538
"name": "X-Api-Key"
4639
}
4740
}
4841

49-
######################################################################
50-
# Configure Swagger before initializing it
51-
######################################################################
52-
api = Api(
53-
app,
54-
version="1.0.0",
55-
title="Pet Demo REST API Service",
56-
description="This is a sample server Pet store server.",
57-
default="pets",
58-
default_label="Pet shop operations",
59-
doc="/apidocs", # default also could use doc='/apidocs/'
60-
authorizations=authorizations,
61-
prefix="/api",
62-
)
63-
64-
65-
# Import the routes After the Flask app is created
66-
# pylint: disable=wrong-import-position, wrong-import-order, cyclic-import
67-
from service import routes, models # noqa: F401, E402
68-
from service.common import error_handlers
69-
70-
# Set up logging for production
71-
log_handlers.init_logging(app, "gunicorn.error")
72-
73-
app.logger.info(70 * "*")
74-
app.logger.info(" P E T S E R V I C E R U N N I N G ".center(70, "*"))
75-
app.logger.info(70 * "*")
76-
77-
app.logger.info("Service initialized!")
78-
79-
# If an API Key was not provided, autogenerate one
80-
if not app.config["API_KEY"]:
81-
app.config["API_KEY"] = routes.generate_apikey()
82-
app.logger.info("Missing API Key! Autogenerated: %s", app.config["API_KEY"])
83-
84-
try:
85-
models.Pet.init_db(app.config["CLOUDANT_DBNAME"])
86-
except Exception as error: # pylint: disable=broad-except
87-
app.logger.critical("%s: Cannot continue", error)
88-
# gunicorn requires exit code 4 to stop spawning workers when they die
89-
sys.exit(4)
42+
# Will be initialize when app is created
43+
api = None # pylint: disable=invalid-name
44+
45+
46+
############################################################
47+
# Initialize the Flask instance
48+
############################################################
49+
def create_app():
50+
"""Initialize the core application."""
51+
52+
# Create the Flask app
53+
app = Flask(__name__)
54+
app.config.from_object(config)
55+
56+
# Turn off strict slashes because it violates best practices
57+
app.url_map.strict_slashes = False
58+
59+
######################################################################
60+
# Configure Swagger before initializing it
61+
######################################################################
62+
global api
63+
api = Api(
64+
app,
65+
version="1.0.0",
66+
title="Pet Demo REST API Service",
67+
description="This is a sample server Pet store server.",
68+
default="pets",
69+
default_label="Pet shop operations",
70+
doc="/apidocs", # default also could use doc='/apidocs/'
71+
authorizations=authorizations,
72+
prefix="/api",
73+
)
74+
75+
with app.app_context():
76+
# Import the routes After the Flask app is created
77+
# pylint: disable=import-outside-toplevel
78+
from service import routes, models # noqa: F401, E402
79+
from service.common import error_handlers # pylint: disable=unused-import
80+
81+
try:
82+
models.Pet.init_db(app.config["CLOUDANT_DBNAME"])
83+
except Exception as error: # pylint: disable=broad-except
84+
app.logger.critical("%s: Cannot continue", error)
85+
# gunicorn requires exit code 4 to stop spawning workers when they die
86+
sys.exit(4)
87+
88+
# Set up logging for production
89+
log_handlers.init_logging(app, "gunicorn.error")
90+
91+
app.logger.info(70 * "*")
92+
app.logger.info(" P E T S E R V I C E R U N N I N G ".center(70, "*"))
93+
app.logger.info(70 * "*")
94+
95+
# If an API Key was not provided, autogenerate one
96+
if not app.config["API_KEY"]:
97+
app.config["API_KEY"] = routes.generate_apikey()
98+
app.logger.info("Missing API Key! Autogenerated: %s", app.config["API_KEY"])
99+
100+
app.logger.info("Service initialized!")
101+
102+
return app

service/common/error_handlers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
Handles all of the HTTP Error Codes returning JSON messages
2121
"""
2222

23-
from service import app, api
23+
from flask import current_app as app # Import Flask application
24+
from service import api
2425
from service.models import DataValidationError, DatabaseConnectionError
2526
from . import status
2627

service/models.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,9 @@
5252
CLOUDANT_PASSWORD = os.getenv("CLOUDANT_PASSWORD", "pass")
5353

5454
# global variables for retry (must be int)
55-
RETRY_COUNT = int(os.getenv("RETRY_COUNT", 10))
56-
RETRY_DELAY = int(os.getenv("RETRY_DELAY", 1))
57-
RETRY_BACKOFF = int(os.getenv("RETRY_BACKOFF", 2))
55+
RETRY_COUNT = int(os.getenv("RETRY_COUNT", "10"))
56+
RETRY_DELAY = int(os.getenv("RETRY_DELAY", "1"))
57+
RETRY_BACKOFF = int(os.getenv("RETRY_BACKOFF", "2"))
5858

5959

6060
class DatabaseConnectionError(Exception):

service/routes.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,11 @@
3131
import secrets
3232
from functools import wraps
3333
from flask import request
34+
from flask import current_app as app # Import Flask application
3435
from flask_restx import Resource, fields, reqparse, inputs
3536
from service.models import Pet, Gender
3637
from service.common import status # HTTP Status Codes
37-
from . import app, api
38+
from . import api
3839

3940

4041
######################################################################

tests/test_routes.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,12 @@
2525

2626
import logging
2727
from unittest import TestCase
28-
from unittest.mock import patch
28+
# from unittest.mock import patch
2929
from urllib.parse import quote_plus
30-
from service import app, routes
30+
from wsgi import app
31+
from service import routes
3132
from service.common import status
32-
from service.models import DatabaseConnectionError
33+
# from service.models import DatabaseConnectionError
3334
from tests.factories import PetFactory
3435

3536
# Disable all but critical errors during normal test run

wsgi.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""
2+
Web Server Gateway Interface (WSGI) entry point
3+
"""
4+
5+
import os
6+
from service import create_app
7+
8+
PORT = int(os.getenv("PORT", "8000"))
9+
10+
app = create_app()
11+
12+
if __name__ == "__main__":
13+
app.run(host="0.0.0.0", port=PORT)

0 commit comments

Comments
 (0)