Skip to content

Commit

Permalink
commit--
Browse files Browse the repository at this point in the history
  • Loading branch information
Luc-Bertin committed Dec 14, 2020
0 parents commit 1966b29
Show file tree
Hide file tree
Showing 18 changed files with 639 additions and 0 deletions.
14 changes: 14 additions & 0 deletions .gitignore
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
55 changes: 55 additions & 0 deletions READMe.md
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.
191 changes: 191 additions & 0 deletions app.py
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()
38 changes: 38 additions & 0 deletions config.py
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 added model_saved
Binary file not shown.
27 changes: 27 additions & 0 deletions requirements.txt
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
11 changes: 11 additions & 0 deletions templates/404.html
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 %}
27 changes: 27 additions & 0 deletions templates/base.html
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">&times;</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 %}
10 changes: 10 additions & 0 deletions templates/index.html
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 %}
Loading

0 comments on commit 1966b29

Please sign in to comment.