forked from AFR2512/theFLASKAPP2
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 1966b29
Showing
18 changed files
with
639 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
# ignore all Python compiled file | ||
*.pyc | ||
|
||
# ignore the virtual env (the user will create it and install packages from requirements.txt) | ||
myenv/ | ||
|
||
# ignore all __pycache__ folders | ||
__pycache__ | ||
|
||
# from Mac | ||
.DS_Store | ||
|
||
# from pyenv | ||
.python-version |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
# Last session project. | ||
## Duration: 3h | ||
|
||
# Brief description | ||
|
||
This is an overly simplistic application developped using the Flask micro web-framework. | ||
Of course you can do better in terms of code refactoring (especially the app.py file), but that's not the main focus here. | ||
It uses an already trained (logistic regression) model to predict if the user would have had survived to the Titanic, given the data he submits to the server. | ||
It also caches the predictions and inputs in a Postgres database so that a user may only submit its details once (can't retry a prediction with the same username). | ||
|
||
<u>What we ask you to do is:</u> | ||
1. **containerize this app**, bundling it along with its dependencies using a **Dockerfile** | ||
2. **orchestrate it** using a **docker-compose.yml file**, with a **Postgres server** using the corresponding **Docker image** from the Docker Hub registry. | ||
|
||
# Prerequities | ||
|
||
1. You **must** have a GitHub account. | ||
2. **Fork** this repository. | ||
3. Git **clone** the **forked** repository into a local repository: `git clone your_forked_repo_https_url` | ||
4. PLay with the app, read the code | ||
5. Create a **Dockerfile + docker-compose.yml** file | ||
6. Check if your containerized app works\* | ||
7. Git push\* | ||
8. Post your name, lastname (as appears on the DVO/attendance list) and Github link here: https://forms.gle/qhdQQ2mcgPenCgdb7 | ||
|
||
# Must do ! | ||
- Dockerfile and docker-compose.yml file should be in **root project folder**. | ||
- the service name for the flask application in the docker-compose file **must** be named ***flaskapp***. | ||
- I must be able to run your project only by doing `docker-compose up --build`, no more! | ||
- In the dockerfile, you must copy the whole content of the flaskapp folder in a dedicated folder in the container. | ||
- The Python code **MUST NOT** be modified, **NOR** moved under **NO** circumstances, **only 1 Dockerfile and 1 docker-compose file should be created** | ||
|
||
Anything that does not respect the aforementioned conditions will result in an automatic score of 0 for this exam. This is CRUCIAL so that I can perform the tests on your project. | ||
|
||
\* How to push code, | ||
Do your modifications, and at the end of the 3h session, from your **root project folder**, do from your terminal: | ||
```sh | ||
git add . | ||
git commit -m "first commit" | ||
git push origin master | ||
``` | ||
|
||
To install git on your [computer](https://git-scm.com/book/fr/v2/Démarrage-rapide-Installation-de-Git) | ||
in [english](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git). | ||
|
||
## Tips | ||
|
||
1. Include this block in your Dockerfile | ||
``` | ||
RUN wget https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh -P /scripts | ||
RUN chmod +x /scripts/wait-for-it.sh | ||
ENTRYPOINT ["/scripts/wait-for-it.sh", "db:5432", "--"] | ||
``` | ||
2. Running the app can be done simply `python app.py runserver --host=0.0.0.0 --threaded`. This should then also be the **Dockerfile default startup command**. | ||
3. Look at the whole project (code, config file, etc.) to check the dependencies to install, name of the service to give, and env variables to be set. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
## | ||
## flask imports | ||
## | ||
from flask import Flask, current_app | ||
from flask import (make_response, | ||
redirect, | ||
abort, | ||
session, | ||
render_template, | ||
url_for, | ||
flash, | ||
request) | ||
## | ||
## Flask extensions imports | ||
## | ||
from flask_script import Manager | ||
from flask_bootstrap import Bootstrap | ||
from flask_sqlalchemy import SQLAlchemy | ||
from flask_migrate import Migrate, MigrateCommand | ||
from flask_wtf import FlaskForm | ||
from wtforms import (StringField, | ||
SubmitField, IntegerField, FloatField, SelectField) | ||
from wtforms.validators import (DataRequired, | ||
InputRequired, Length, NumberRange) | ||
## | ||
## configuration file imports | ||
## | ||
import config | ||
|
||
|
||
## | ||
## creation of the Flask application instance | ||
## | ||
## passing its configuration, and initializing the extensions along with the flask app. | ||
## The web server passes all requests it receives from clients to this object for handling, using a protocol called Web Server Gateway Interface (WSGI) | ||
app = Flask(__name__, template_folder="templates") | ||
app.config.from_object('config') | ||
|
||
manager = Manager(app) # To give provide CLI commands | ||
bootstrap = Bootstrap(app) # Twitter Bootstrap extension | ||
db = SQLAlchemy(app) # SQLAlchemy extension, manipulating the database object db | ||
migrate = Migrate(app, db) # database migrations | ||
manager.add_command('db', MigrateCommand) # to add it as command line using Flask Script | ||
|
||
|
||
## | ||
## Forms | ||
## | ||
|
||
class EnterYourInfosForm(FlaskForm): | ||
""" | ||
This field is the base for most of the more complicated fields, and represents an <input type="text">. | ||
""" | ||
name = StringField("Name", validators=[DataRequired()]) | ||
|
||
""" | ||
Represents an <input type="number"> | ||
""" | ||
age = IntegerField("Age", | ||
validators=[DataRequired(), NumberRange(min=0, max=123)]) | ||
""" | ||
A text field, except all input is coerced to an float. Erroneous input is ignored and will not be accepted as a value | ||
""" | ||
ticket_price = FloatField("Ticket Price", | ||
validators=[DataRequired(), NumberRange(min=0)]) | ||
""" | ||
Select fields take a choices parameter which is a list of (value, label) pairs. It can also be a list of only values, in which case the value is used as the label. The value can be any type, but because form data is sent to the browser as strings, you will need to provide a coerce function that converts a string back to the expected type. | ||
""" | ||
sexe = SelectField("Sexe", | ||
choices=[(0, "Homme"),(1,"Femme")], | ||
validators=[InputRequired()], | ||
coerce=int) | ||
""" | ||
Represents an <input type="submit">. This allows checking if a given submit button has been pressed. | ||
""" | ||
submit = SubmitField("Submit") | ||
|
||
#def clear(self): | ||
#self.name.data = '' | ||
#self.age.raw_data = [''] | ||
#self. | ||
|
||
|
||
## | ||
## models | ||
## | ||
|
||
class User(db.Model): | ||
""" | ||
This class describes an user. | ||
SQLAlchemy extension for flask provides an ORM (Object-relational mappers). | ||
An ORM is a tool that provides convertion from high-level (object-oriented) objects into low-level database instructions, so you don't have to directly deal with tables, documents, or query languages. | ||
+ for SQLAlchemy the abstraction is even higher as you can choose different database engines that relies on the same Python object (hence, without having to rewrite or adapt code)! | ||
""" | ||
__tablename__ = "users" # renaming the table and not default given name "user" not respecting the conventions | ||
|
||
id_ = db.Column(db.Integer, primary_key=True) | ||
name = db.Column(db.String(64), unique=True) | ||
age = db.Column(db.Integer, nullable=False) | ||
ticket_price = db.Column(db.Float, nullable=False) | ||
sexe = db.Column(db.Boolean, nullable=False) | ||
survived = db.Column(db.Boolean, nullable=True) | ||
#date_submitted = db.Column(db.DateTime, nullable=False) | ||
|
||
def __repr__(self): | ||
return "User {}, with name: {}".format(self.id_, self.name) | ||
|
||
## | ||
## errors handlers | ||
## def: is a function that returns a response when a type of error is raised. | ||
## here only one is implemented for the 404 Not Found | ||
## The good old “chap, you made a mistake typing that URL” message. | ||
## | ||
|
||
@app.errorhandler(404) | ||
def page_not_found(e): | ||
return render_template('404.html', nompath=e), 404 | ||
|
||
## | ||
## views | ||
## | ||
|
||
@app.route('/') | ||
def index(): | ||
return render_template("index.html") | ||
|
||
@app.route('/predict', methods=['GET', 'POST']) | ||
def predict(): | ||
form = EnterYourInfosForm() | ||
if request.method=="POST" and form.validate_on_submit(): | ||
# check if a user with the same name already exist in the db | ||
user = User.query.filter_by(name=form.name.data).first() | ||
if user is not None: | ||
flash('It seems you already played, this was your prediction: {}'.format( | ||
user.survived)) | ||
else: | ||
import joblib | ||
loaded_model = joblib.load("model_saved") | ||
# predict for the user if he/she would have had survived | ||
prediction = bool(loaded_model.predict( | ||
[[form.age.data, form.ticket_price.data, form.sexe.data]] | ||
)) | ||
|
||
# create a new "user" of the service | ||
user = User(name=form.name.data, | ||
age=form.age.data, | ||
ticket_price=form.ticket_price.data, | ||
sexe=form.sexe.data, | ||
survived=prediction) | ||
# add the user to the db | ||
db.session.add(user) | ||
db.session.commit() | ||
session["survived"] = user.survived | ||
return redirect(url_for('show_result')) | ||
return render_template('predict.html', form=form) | ||
|
||
|
||
@app.route('/results') | ||
def show_result(): | ||
return render_template('results.html', success=session.get('survived', 'first')) | ||
|
||
@app.route('/users') | ||
def show_all_users(): | ||
users = User.query.all() | ||
return render_template('users.html', users=users) | ||
|
||
@app.route('/<path:nompath>') | ||
def error_test(nompath): | ||
# raises an HTTPException of status code 404 | ||
# nompath is the payload | ||
# retrieved from e in the page_not_found(e) error_handler | ||
abort(404, nompath) | ||
|
||
|
||
@manager.command | ||
def test(): | ||
""" Run the integration tests """ | ||
import unittest | ||
tests = unittest.TestLoader().discover('tests') | ||
unittest.TextTestRunner(verbosity=2).run(tests) | ||
|
||
if __name__ == "__main__": | ||
# app.run(debug=True), | ||
# changed to manager | ||
# using the extension | ||
# so you can pass options | ||
# directly from the command-line | ||
with app.app_context(): | ||
# create all the tables | ||
db.create_all() | ||
manager.run() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
""" | ||
This module defines the environment variables that will be used to configure the flask application | ||
basedir: absolute path to the directory including this module and the app module | ||
SECRET_KEY: the secret key is accessed through an environment variable instead of being embedded in the code.This variable is used as a general-purpose encryption key by Flask and several third-party extensions. Hence choose a very hard string to remember to replace the default 'hard to guess string' | ||
SQLALCHEMY_DATABASE_URI: database is specified as a URI. | ||
i.e. | ||
dbengine://username:password@hostname/database | ||
- dbengine is the database engine to use among mysql, postgresql or sqlite. | ||
- hostname refers to the server that hosts the MySQL service | ||
- username and password are used for authentification | ||
- database refers to the name of the database | ||
SQLite databases do not have a server, but are simple files, so hostname, username, and password are omitted and database is the filename of a disk file: e.g. | ||
'sqlite:///' + os.path.join(basedir, 'data.sqlite')' | ||
""" | ||
|
||
import os | ||
|
||
|
||
basedir = os.path.abspath(os.path.dirname(__file__)) | ||
|
||
SECRET_KEY = os.getenv("SECRET_KEY") or "hard to guess string" | ||
|
||
# To use a postgres db hosted on a postgres server, set the following env vars and USE_POSTGRES to any non-falsy values (e.g. 1) | ||
password = os.getenv("POSTGRES_PASSWORD") | ||
username = os.getenv("POSTGRES_USER") | ||
database = os.getenv("POSTGRES_DB") | ||
hostname = "db" | ||
if os.getenv("USE_POSTGRES"): | ||
# use a psotgres db using above env variables | ||
SQLALCHEMY_DATABASE_URI = f"postgresql://{username}:{password}@{hostname}/{database}" | ||
else: | ||
# simply use a SQLlite database (<=> flat file) | ||
SQLALCHEMY_DATABASE_URI = os.getenv("SQLALCHEMY_DATABASE_URI") or \ | ||
'sqlite:///' + os.path.join(basedir, 'data.sqlite') |
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
alembic==1.4.2 | ||
click==7.1.2 | ||
dominate==2.5.2 | ||
Flask==1.1.2 | ||
Flask-Bootstrap==3.3.7.1 | ||
Flask-Migrate==2.5.3 | ||
Flask-Script==2.0.6 | ||
Flask-SQLAlchemy==2.4.4 | ||
Flask-WTF==0.14.3 | ||
itsdangerous==1.1.0 | ||
Jinja2==2.11.2 | ||
joblib==0.17.0 | ||
Mako==1.1.3 | ||
MarkupSafe==1.1.1 | ||
numpy==1.19.4 | ||
psycopg2-binary==2.8.6 | ||
python-dateutil==2.8.1 | ||
python-editor==1.0.4 | ||
scikit-learn==0.23.2 | ||
scipy==1.5.4 | ||
six==1.15.0 | ||
sklearn==0.0 | ||
SQLAlchemy==1.3.19 | ||
threadpoolctl==2.1.0 | ||
visitor==0.1.3 | ||
Werkzeug==1.0.1 | ||
WTForms==2.3.3 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{% extends 'base.html' %} | ||
|
||
{% block title %}My Flask App - Page Not Found{% endblock %} | ||
|
||
{% block page_content %} | ||
<div class="page-header"> | ||
<div class="page-header"> | ||
<h1>{{ nompath }} is not found !</h1> | ||
</div> | ||
</div> | ||
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
{# The html comment tag <!-- ... --> does not work in a jinja template but this does #} | ||
|
||
{# extends declares that this template inherits from bootstrap/base template where blocks'content can be overriden #} | ||
{% extends "bootstrap/base.html" %} | ||
|
||
{# example of block to (re)define, check Bootstrap template #} | ||
{% block title %}My Flask App{% endblock %} | ||
|
||
{% block navbar %} | ||
{% include 'navigation.html' %} | ||
{% endblock %} | ||
|
||
{% block content %} | ||
<div class="container"> | ||
{# for the messages that get flashed #} | ||
{% for message in get_flashed_messages() %} | ||
<div class="alert alert-warning"> | ||
<button type="button" class="close" data-dismiss="alert">×</button> | ||
{{ message }} | ||
</div> | ||
{% endfor %} | ||
|
||
{# Here i define a brand new block and a child template inheriting from this template will later fill it by redeclaring it #} | ||
{% block page_content %} | ||
{% endblock %} | ||
</div> | ||
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
{% extends "base.html" %} | ||
|
||
{% block page_content %} | ||
|
||
<div class="page-header"> | ||
<h1>Welcome to Titanic Prediction App !</h1> | ||
<p>This is a web app where you can assess whether you would have survived based on your age, the ticket price you would have invested, and whether you are a female or a male</p> | ||
</div> | ||
<a href="/predict" class="btn btn-info" role="button">Play</a> | ||
{% endblock %} |
Oops, something went wrong.