Commit
·
fe70d38
1
Parent(s):
1291dfb
add docker file app/ and req file
Browse files- Dockerfile +27 -0
- app/.DS_Store +0 -0
- app/__init__.py +0 -0
- app/main.py +80 -0
- app/model.sav +0 -0
- app/static/css/bootstrap.min.css +0 -0
- app/static/css/jumbotron-narrow.css +88 -0
- app/static/img/setosa.jpg +0 -0
- app/static/img/versicolor.jpg +0 -0
- app/static/img/virginica.jpg +0 -0
- app/templates/index.html +91 -0
- app/templates/prediction.html +46 -0
- requirements.txt +1 -1
Dockerfile
ADDED
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# 2 stages build
|
2 |
+
FROM python:3.12-slim AS python-base
|
3 |
+
WORKDIR /app
|
4 |
+
# We need to add the path of our virtual env to our $PATH environment variable :
|
5 |
+
ENV PATH=".venv/bin:$PATH"
|
6 |
+
|
7 |
+
# Our builder image to install the libraries
|
8 |
+
FROM python-base AS python-builder
|
9 |
+
RUN apt-get update && \
|
10 |
+
apt-get upgrade -y && \
|
11 |
+
apt-get install -y gcc \
|
12 |
+
&& apt-get clean
|
13 |
+
|
14 |
+
COPY ./requirements.txt ./
|
15 |
+
RUN python -m venv .venv
|
16 |
+
# Install requirements
|
17 |
+
RUN pip install -r requirements.txt
|
18 |
+
|
19 |
+
|
20 |
+
# Our final image
|
21 |
+
FROM python-base
|
22 |
+
COPY --from=python-builder /app/.venv ./.venv
|
23 |
+
COPY ./app /app
|
24 |
+
|
25 |
+
EXPOSE 5000
|
26 |
+
|
27 |
+
ENTRYPOINT gunicorn --bind 0.0.0.0:5000 main:app
|
app/.DS_Store
ADDED
Binary file (6.15 kB). View file
|
|
app/__init__.py
ADDED
File without changes
|
app/main.py
ADDED
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import pickle
|
3 |
+
import requests
|
4 |
+
from dotenv import load_dotenv
|
5 |
+
from flask import Flask, render_template, request
|
6 |
+
from pydantic import BaseModel, Field, PositiveFloat
|
7 |
+
|
8 |
+
app = Flask(__name__)
|
9 |
+
MODEL = pickle.load(open("model.sav", "rb"))
|
10 |
+
|
11 |
+
PRICE_BASE = 10**5
|
12 |
+
|
13 |
+
load_dotenv(override=False)
|
14 |
+
|
15 |
+
API_HOST = os.environ.get("API_HOST", "localhost")
|
16 |
+
|
17 |
+
|
18 |
+
class FormQuery(BaseModel):
|
19 |
+
med_inc: PositiveFloat = Field(..., validation_alias="MedInc")
|
20 |
+
house_age: PositiveFloat = Field(..., validation_alias="HouseAge")
|
21 |
+
ave_rooms: PositiveFloat = Field(..., validation_alias="AveRooms")
|
22 |
+
ave_bedrms: PositiveFloat = Field(..., validation_alias="AveBedrms")
|
23 |
+
population: PositiveFloat = Field(..., validation_alias="Population")
|
24 |
+
ave_occup: PositiveFloat = Field(..., validation_alias="AveOccup")
|
25 |
+
latitude: float = Field(..., validation_alias="Latitude")
|
26 |
+
longitude: float = Field(..., validation_alias="Longitude")
|
27 |
+
|
28 |
+
|
29 |
+
@app.route("/", methods=["GET"])
|
30 |
+
def california_index():
|
31 |
+
return render_template("index.html")
|
32 |
+
|
33 |
+
|
34 |
+
@app.route("/predict/", methods=["POST"])
|
35 |
+
def local_model_result():
|
36 |
+
form_query = FormQuery(**request.form.to_dict(flat=True))
|
37 |
+
|
38 |
+
reg = MODEL.predict(
|
39 |
+
[
|
40 |
+
[
|
41 |
+
form_query.med_inc,
|
42 |
+
form_query.house_age,
|
43 |
+
form_query.ave_rooms,
|
44 |
+
form_query.ave_bedrms,
|
45 |
+
form_query.population,
|
46 |
+
form_query.ave_occup,
|
47 |
+
form_query.latitude,
|
48 |
+
form_query.longitude,
|
49 |
+
]
|
50 |
+
]
|
51 |
+
)[0]
|
52 |
+
return render_template("prediction.html", price=reg * PRICE_BASE)
|
53 |
+
|
54 |
+
|
55 |
+
@app.route("/predict_from_api/", methods=["POST"])
|
56 |
+
def api_result():
|
57 |
+
model_list = requests.get(f"http://{API_HOST}:8000/model/list/").json()
|
58 |
+
if len(model_list) == 0:
|
59 |
+
raise Exception("No model could be retrieved from the model registry")
|
60 |
+
|
61 |
+
best_model = sorted(model_list, key=lambda d: d["rse"])[0]
|
62 |
+
app.logger.debug(f"Best model retrieved : {best_model}")
|
63 |
+
|
64 |
+
api_response = requests.post(
|
65 |
+
f"http://{API_HOST}:8000/model/predict/",
|
66 |
+
json={
|
67 |
+
**{"train_id": best_model["train_id"]},
|
68 |
+
**FormQuery(**request.form.to_dict(flat=True)).model_dump(),
|
69 |
+
},
|
70 |
+
)
|
71 |
+
|
72 |
+
response = api_response.json()
|
73 |
+
app.logger.debug(response)
|
74 |
+
|
75 |
+
return render_template("prediction.html", price=response["reg"] * PRICE_BASE)
|
76 |
+
|
77 |
+
|
78 |
+
if __name__ == "__main__":
|
79 |
+
app.debug = True
|
80 |
+
app.run(host="0.0.0.0", port=5000, debug=True)
|
app/model.sav
ADDED
Binary file (482 Bytes). View file
|
|
app/static/css/bootstrap.min.css
ADDED
The diff for this file is too large to render.
See raw diff
|
|
app/static/css/jumbotron-narrow.css
ADDED
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/* Space out content a bit */
|
2 |
+
body {
|
3 |
+
padding-top: 20px;
|
4 |
+
padding-bottom: 20px;
|
5 |
+
}
|
6 |
+
|
7 |
+
a, a:hover, a:visited, a:link, a:active{
|
8 |
+
text-decoration: none;
|
9 |
+
}
|
10 |
+
|
11 |
+
/* Everything but the jumbotron gets side spacing for mobile first views */
|
12 |
+
.header,
|
13 |
+
.marketing,
|
14 |
+
.footer {
|
15 |
+
padding-right: 15px;
|
16 |
+
padding-left: 15px;
|
17 |
+
}
|
18 |
+
|
19 |
+
/* Custom page header */
|
20 |
+
.header {
|
21 |
+
padding-bottom: 20px;
|
22 |
+
border-bottom: 1px solid #e5e5e5;
|
23 |
+
}
|
24 |
+
/* Make the masthead heading the same height as the navigation */
|
25 |
+
.header h3 {
|
26 |
+
margin-top: 0;
|
27 |
+
margin-bottom: 0;
|
28 |
+
line-height: 40px;
|
29 |
+
}
|
30 |
+
|
31 |
+
/* Custom page footer */
|
32 |
+
.footer {
|
33 |
+
padding-top: 19px;
|
34 |
+
color: #777;
|
35 |
+
border-top: 1px solid #e5e5e5;
|
36 |
+
}
|
37 |
+
|
38 |
+
/* Customize container */
|
39 |
+
@media (min-width: 768px) {
|
40 |
+
.container {
|
41 |
+
max-width: 730px;
|
42 |
+
}
|
43 |
+
}
|
44 |
+
.container-narrow > hr {
|
45 |
+
margin: 30px 0;
|
46 |
+
}
|
47 |
+
|
48 |
+
/* Main marketing message and sign up button */
|
49 |
+
.jumbotron {
|
50 |
+
text-align: center;
|
51 |
+
border-bottom: 1px solid #e5e5e5;
|
52 |
+
}
|
53 |
+
.jumbotron .btn {
|
54 |
+
padding: 14px 24px;
|
55 |
+
font-size: 21px;
|
56 |
+
}
|
57 |
+
|
58 |
+
/* Supporting marketing content */
|
59 |
+
.marketing {
|
60 |
+
margin: 40px 0;
|
61 |
+
}
|
62 |
+
.marketing p + h4 {
|
63 |
+
margin-top: 28px;
|
64 |
+
}
|
65 |
+
|
66 |
+
/* Responsive: Portrait tablets and up */
|
67 |
+
@media screen and (min-width: 768px) {
|
68 |
+
/* Remove the padding we set earlier */
|
69 |
+
.header,
|
70 |
+
.marketing,
|
71 |
+
.footer {
|
72 |
+
padding-right: 0;
|
73 |
+
padding-left: 0;
|
74 |
+
}
|
75 |
+
/* Space out the masthead */
|
76 |
+
.header {
|
77 |
+
margin-bottom: 30px;
|
78 |
+
}
|
79 |
+
/* Remove the bottom border on the jumbotron for visual effect */
|
80 |
+
.jumbotron {
|
81 |
+
border-bottom: 0;
|
82 |
+
}
|
83 |
+
}
|
84 |
+
|
85 |
+
#selector {
|
86 |
+
width: 600px;
|
87 |
+
height: 200px;
|
88 |
+
}
|
app/static/img/setosa.jpg
ADDED
![]() |
app/static/img/versicolor.jpg
ADDED
![]() |
app/static/img/virginica.jpg
ADDED
![]() |
app/templates/index.html
ADDED
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="utf-8">
|
5 |
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
7 |
+
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
|
8 |
+
<meta name="description" content="">
|
9 |
+
<meta name="author" content="">
|
10 |
+
<link rel="icon" href="../../favicon.ico">
|
11 |
+
|
12 |
+
<title>VIVADATA |California ML</title>
|
13 |
+
|
14 |
+
<!-- Bootstrap core CSS -->
|
15 |
+
<link href="../static/css/bootstrap.min.css" rel="stylesheet">
|
16 |
+
|
17 |
+
<!-- Custom styles for this template -->
|
18 |
+
<link href="../static/css/jumbotron-narrow.css" rel="stylesheet">
|
19 |
+
|
20 |
+
</head>
|
21 |
+
|
22 |
+
<body>
|
23 |
+
|
24 |
+
<div class="container">
|
25 |
+
<div class="header clearfix">
|
26 |
+
<h3 class="text-muted">VIVADATA - Flask Demo</h3>
|
27 |
+
</div>
|
28 |
+
|
29 |
+
<div class="jumbotron">
|
30 |
+
<h1>Predicting of California Housing 🏘️</h1>
|
31 |
+
<p class="lead">Fill the needed data to predict the median value!</p>
|
32 |
+
<!-- <p><a class="btn btn-lg btn-success" href="home" role="button">Get started</a></p> -->
|
33 |
+
</div class="form-row">
|
34 |
+
<form action="" id="prediction-form" method="post">
|
35 |
+
<div class="form-row">
|
36 |
+
<div class="form-group col-md-6">
|
37 |
+
<label for="MedInc">Median income in block</label>
|
38 |
+
<input type="number" step="0.01" class="form-control" id="MedInc" name="MedInc" placeholder="Enter Value">
|
39 |
+
</div>
|
40 |
+
<div class="form-group col-md-6">
|
41 |
+
<label for="HouseAge">Median house age in block</label>
|
42 |
+
<input type="number" step="0.01" class="form-control" id="HouseAge" name="HouseAge" placeholder="Enter Value">
|
43 |
+
</div>
|
44 |
+
</div>
|
45 |
+
<div class="form-row">
|
46 |
+
<div class="form-group col-md-6">
|
47 |
+
<label for="AveRooms">Average number of rooms</label>
|
48 |
+
<input type="number" step="0.01" class="form-control" id="AveRooms" name="AveRooms" placeholder="Enter Value">
|
49 |
+
</div>
|
50 |
+
<div class="form-group col-md-6">
|
51 |
+
<label for="AveBedrms">Average number of bedrooms</label>
|
52 |
+
<input type="number" step="0.01" class="form-control" id="AveBedrms" name="AveBedrms" placeholder="Enter Value">
|
53 |
+
</div>
|
54 |
+
</div>
|
55 |
+
<div class="form-row">
|
56 |
+
<div class="form-group col-md-6">
|
57 |
+
<label for="Population">Block population</label>
|
58 |
+
<input type="number" step="0.01" class="form-control" id="Population" name="Population" placeholder="Enter Value">
|
59 |
+
</div>
|
60 |
+
<div class="form-group col-md-6">
|
61 |
+
<label for="AveOccup">Average house occupancy</label>
|
62 |
+
<input type="number" step="0.01" class="form-control" id="AveOccup" name="AveOccup" placeholder="Enter Value">
|
63 |
+
</div>
|
64 |
+
</div>
|
65 |
+
<div class="form-row">
|
66 |
+
<div class="form-group col-md-6">
|
67 |
+
<label for="Latitude">House block latitude</label>
|
68 |
+
<input type="number" step="0.01" class="form-control" id="Latitude" name="Latitude" placeholder="Enter Value">
|
69 |
+
</div>
|
70 |
+
<div class="form-group col-md-6">
|
71 |
+
<label for="Longitude">House block longitude</label>
|
72 |
+
<input type="number" step="0.01" class="form-control" id="Longitude" name="Longitude" placeholder="Enter Value">
|
73 |
+
</div>
|
74 |
+
</div>
|
75 |
+
<button type="submit" class="btn btn-primary" onclick="setAction('/predict/')">Predict via local model</button>
|
76 |
+
<button type="submit" class="btn btn-primary" onclick="setAction('/predict_from_api/')">Predict via API</button>
|
77 |
+
</form>
|
78 |
+
<br>
|
79 |
+
<footer class="footer">
|
80 |
+
<p>© Vivadata 2019</p>
|
81 |
+
</footer>
|
82 |
+
|
83 |
+
</div> <!-- /container -->
|
84 |
+
</body>
|
85 |
+
</html>
|
86 |
+
|
87 |
+
<script>
|
88 |
+
function setAction(action) {
|
89 |
+
document.getElementById('prediction-form').action = action;
|
90 |
+
}
|
91 |
+
</script>
|
app/templates/prediction.html
ADDED
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="utf-8">
|
5 |
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
7 |
+
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
|
8 |
+
<meta name="description" content="">
|
9 |
+
<meta name="author" content="">
|
10 |
+
<link rel="icon" href="../../favicon.ico">
|
11 |
+
|
12 |
+
<title>VIVADATA | Prediction</title>
|
13 |
+
|
14 |
+
<!-- Bootstrap core CSS -->
|
15 |
+
<link href="../static/css/bootstrap.min.css" rel="stylesheet">
|
16 |
+
|
17 |
+
<!-- Custom styles for this template -->
|
18 |
+
<link href="../static/css/jumbotron-narrow.css" rel="stylesheet">
|
19 |
+
|
20 |
+
</head>
|
21 |
+
|
22 |
+
<body>
|
23 |
+
|
24 |
+
<div class="container">
|
25 |
+
<div class="header clearfix">
|
26 |
+
<h3 class="text-muted">VIVADATA - Flask Demo</h3>
|
27 |
+
</div>
|
28 |
+
|
29 |
+
<div class="jumbotron">
|
30 |
+
<h1>Median house value: {{price}} 💵</h1>
|
31 |
+
</div>
|
32 |
+
|
33 |
+
<br>
|
34 |
+
<div class="text-center">
|
35 |
+
<a href="/" class="btn btn-primary">Back</a>
|
36 |
+
</div>
|
37 |
+
|
38 |
+
<br>
|
39 |
+
<footer class="footer">
|
40 |
+
<p>© Vivadata 2019</p>
|
41 |
+
</footer>
|
42 |
+
|
43 |
+
</div> <!-- /container -->
|
44 |
+
</body>
|
45 |
+
</html>
|
46 |
+
|
requirements.txt
CHANGED
@@ -30,4 +30,4 @@ threadpoolctl==3.2.0
|
|
30 |
typing_extensions==4.8.0
|
31 |
urllib3==2.0.7
|
32 |
uvicorn==0.23.2
|
33 |
-
Werkzeug==3.0.1
|
|
|
30 |
typing_extensions==4.8.0
|
31 |
urllib3==2.0.7
|
32 |
uvicorn==0.23.2
|
33 |
+
Werkzeug==3.0.1
|