init
This commit is contained in:
commit
c06dea8d7e
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@ -0,0 +1,6 @@
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
*.git
|
||||
*myenv
|
||||
38
.github/workflows/deploy.yml
vendored
Normal file
38
.github/workflows/deploy.yml
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
name: Deploy to Heroku
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.9 # Match your app's Python version
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
|
||||
- name: Test Application
|
||||
run: |
|
||||
pytest # Assuming you have tests configured
|
||||
|
||||
- name: Deploy to Heroku
|
||||
env:
|
||||
HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }}
|
||||
HEROKU_APP_NAME: ${{ secrets.HEROKU_APP_NAME }}
|
||||
run: |
|
||||
echo "Deploying to Heroku..."
|
||||
heroku container:login
|
||||
heroku container:push web --app $HEROKU_APP_NAME
|
||||
heroku container:release web --app $HEROKU_APP_NAME
|
||||
24
Dockerfile
Normal file
24
Dockerfile
Normal file
@ -0,0 +1,24 @@
|
||||
# Use the official Python image as a base
|
||||
FROM python:3.9-slim
|
||||
|
||||
# Set environment variables to prevent Python from writing .pyc files and buffer outputs
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
# Set the working directory inside the container
|
||||
WORKDIR /app
|
||||
|
||||
# Copy the requirements file to the container
|
||||
COPY requirements.txt /app/requirements.txt
|
||||
|
||||
# Install the Python dependencies
|
||||
RUN pip install -r /app/requirements.txt
|
||||
|
||||
# Copy the application code to the container
|
||||
COPY . /app
|
||||
|
||||
# Expose the port the app runs on
|
||||
EXPOSE 8000
|
||||
|
||||
# Ensure the application runs on Heroku's dynamic port
|
||||
CMD ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port $PORT"]
|
||||
151
IRIS.csv
Normal file
151
IRIS.csv
Normal file
@ -0,0 +1,151 @@
|
||||
sepal_length,sepal_width,petal_length,petal_width,species
|
||||
5.1,3.5,1.4,0.2,Iris-setosa
|
||||
4.9,3,1.4,0.2,Iris-setosa
|
||||
4.7,3.2,1.3,0.2,Iris-setosa
|
||||
4.6,3.1,1.5,0.2,Iris-setosa
|
||||
5,3.6,1.4,0.2,Iris-setosa
|
||||
5.4,3.9,1.7,0.4,Iris-setosa
|
||||
4.6,3.4,1.4,0.3,Iris-setosa
|
||||
5,3.4,1.5,0.2,Iris-setosa
|
||||
4.4,2.9,1.4,0.2,Iris-setosa
|
||||
4.9,3.1,1.5,0.1,Iris-setosa
|
||||
5.4,3.7,1.5,0.2,Iris-setosa
|
||||
4.8,3.4,1.6,0.2,Iris-setosa
|
||||
4.8,3,1.4,0.1,Iris-setosa
|
||||
4.3,3,1.1,0.1,Iris-setosa
|
||||
5.8,4,1.2,0.2,Iris-setosa
|
||||
5.7,4.4,1.5,0.4,Iris-setosa
|
||||
5.4,3.9,1.3,0.4,Iris-setosa
|
||||
5.1,3.5,1.4,0.3,Iris-setosa
|
||||
5.7,3.8,1.7,0.3,Iris-setosa
|
||||
5.1,3.8,1.5,0.3,Iris-setosa
|
||||
5.4,3.4,1.7,0.2,Iris-setosa
|
||||
5.1,3.7,1.5,0.4,Iris-setosa
|
||||
4.6,3.6,1,0.2,Iris-setosa
|
||||
5.1,3.3,1.7,0.5,Iris-setosa
|
||||
4.8,3.4,1.9,0.2,Iris-setosa
|
||||
5,3,1.6,0.2,Iris-setosa
|
||||
5,3.4,1.6,0.4,Iris-setosa
|
||||
5.2,3.5,1.5,0.2,Iris-setosa
|
||||
5.2,3.4,1.4,0.2,Iris-setosa
|
||||
4.7,3.2,1.6,0.2,Iris-setosa
|
||||
4.8,3.1,1.6,0.2,Iris-setosa
|
||||
5.4,3.4,1.5,0.4,Iris-setosa
|
||||
5.2,4.1,1.5,0.1,Iris-setosa
|
||||
5.5,4.2,1.4,0.2,Iris-setosa
|
||||
4.9,3.1,1.5,0.1,Iris-setosa
|
||||
5,3.2,1.2,0.2,Iris-setosa
|
||||
5.5,3.5,1.3,0.2,Iris-setosa
|
||||
4.9,3.1,1.5,0.1,Iris-setosa
|
||||
4.4,3,1.3,0.2,Iris-setosa
|
||||
5.1,3.4,1.5,0.2,Iris-setosa
|
||||
5,3.5,1.3,0.3,Iris-setosa
|
||||
4.5,2.3,1.3,0.3,Iris-setosa
|
||||
4.4,3.2,1.3,0.2,Iris-setosa
|
||||
5,3.5,1.6,0.6,Iris-setosa
|
||||
5.1,3.8,1.9,0.4,Iris-setosa
|
||||
4.8,3,1.4,0.3,Iris-setosa
|
||||
5.1,3.8,1.6,0.2,Iris-setosa
|
||||
4.6,3.2,1.4,0.2,Iris-setosa
|
||||
5.3,3.7,1.5,0.2,Iris-setosa
|
||||
5,3.3,1.4,0.2,Iris-setosa
|
||||
7,3.2,4.7,1.4,Iris-versicolor
|
||||
6.4,3.2,4.5,1.5,Iris-versicolor
|
||||
6.9,3.1,4.9,1.5,Iris-versicolor
|
||||
5.5,2.3,4,1.3,Iris-versicolor
|
||||
6.5,2.8,4.6,1.5,Iris-versicolor
|
||||
5.7,2.8,4.5,1.3,Iris-versicolor
|
||||
6.3,3.3,4.7,1.6,Iris-versicolor
|
||||
4.9,2.4,3.3,1,Iris-versicolor
|
||||
6.6,2.9,4.6,1.3,Iris-versicolor
|
||||
5.2,2.7,3.9,1.4,Iris-versicolor
|
||||
5,2,3.5,1,Iris-versicolor
|
||||
5.9,3,4.2,1.5,Iris-versicolor
|
||||
6,2.2,4,1,Iris-versicolor
|
||||
6.1,2.9,4.7,1.4,Iris-versicolor
|
||||
5.6,2.9,3.6,1.3,Iris-versicolor
|
||||
6.7,3.1,4.4,1.4,Iris-versicolor
|
||||
5.6,3,4.5,1.5,Iris-versicolor
|
||||
5.8,2.7,4.1,1,Iris-versicolor
|
||||
6.2,2.2,4.5,1.5,Iris-versicolor
|
||||
5.6,2.5,3.9,1.1,Iris-versicolor
|
||||
5.9,3.2,4.8,1.8,Iris-versicolor
|
||||
6.1,2.8,4,1.3,Iris-versicolor
|
||||
6.3,2.5,4.9,1.5,Iris-versicolor
|
||||
6.1,2.8,4.7,1.2,Iris-versicolor
|
||||
6.4,2.9,4.3,1.3,Iris-versicolor
|
||||
6.6,3,4.4,1.4,Iris-versicolor
|
||||
6.8,2.8,4.8,1.4,Iris-versicolor
|
||||
6.7,3,5,1.7,Iris-versicolor
|
||||
6,2.9,4.5,1.5,Iris-versicolor
|
||||
5.7,2.6,3.5,1,Iris-versicolor
|
||||
5.5,2.4,3.8,1.1,Iris-versicolor
|
||||
5.5,2.4,3.7,1,Iris-versicolor
|
||||
5.8,2.7,3.9,1.2,Iris-versicolor
|
||||
6,2.7,5.1,1.6,Iris-versicolor
|
||||
5.4,3,4.5,1.5,Iris-versicolor
|
||||
6,3.4,4.5,1.6,Iris-versicolor
|
||||
6.7,3.1,4.7,1.5,Iris-versicolor
|
||||
6.3,2.3,4.4,1.3,Iris-versicolor
|
||||
5.6,3,4.1,1.3,Iris-versicolor
|
||||
5.5,2.5,4,1.3,Iris-versicolor
|
||||
5.5,2.6,4.4,1.2,Iris-versicolor
|
||||
6.1,3,4.6,1.4,Iris-versicolor
|
||||
5.8,2.6,4,1.2,Iris-versicolor
|
||||
5,2.3,3.3,1,Iris-versicolor
|
||||
5.6,2.7,4.2,1.3,Iris-versicolor
|
||||
5.7,3,4.2,1.2,Iris-versicolor
|
||||
5.7,2.9,4.2,1.3,Iris-versicolor
|
||||
6.2,2.9,4.3,1.3,Iris-versicolor
|
||||
5.1,2.5,3,1.1,Iris-versicolor
|
||||
5.7,2.8,4.1,1.3,Iris-versicolor
|
||||
6.3,3.3,6,2.5,Iris-virginica
|
||||
5.8,2.7,5.1,1.9,Iris-virginica
|
||||
7.1,3,5.9,2.1,Iris-virginica
|
||||
6.3,2.9,5.6,1.8,Iris-virginica
|
||||
6.5,3,5.8,2.2,Iris-virginica
|
||||
7.6,3,6.6,2.1,Iris-virginica
|
||||
4.9,2.5,4.5,1.7,Iris-virginica
|
||||
7.3,2.9,6.3,1.8,Iris-virginica
|
||||
6.7,2.5,5.8,1.8,Iris-virginica
|
||||
7.2,3.6,6.1,2.5,Iris-virginica
|
||||
6.5,3.2,5.1,2,Iris-virginica
|
||||
6.4,2.7,5.3,1.9,Iris-virginica
|
||||
6.8,3,5.5,2.1,Iris-virginica
|
||||
5.7,2.5,5,2,Iris-virginica
|
||||
5.8,2.8,5.1,2.4,Iris-virginica
|
||||
6.4,3.2,5.3,2.3,Iris-virginica
|
||||
6.5,3,5.5,1.8,Iris-virginica
|
||||
7.7,3.8,6.7,2.2,Iris-virginica
|
||||
7.7,2.6,6.9,2.3,Iris-virginica
|
||||
6,2.2,5,1.5,Iris-virginica
|
||||
6.9,3.2,5.7,2.3,Iris-virginica
|
||||
5.6,2.8,4.9,2,Iris-virginica
|
||||
7.7,2.8,6.7,2,Iris-virginica
|
||||
6.3,2.7,4.9,1.8,Iris-virginica
|
||||
6.7,3.3,5.7,2.1,Iris-virginica
|
||||
7.2,3.2,6,1.8,Iris-virginica
|
||||
6.2,2.8,4.8,1.8,Iris-virginica
|
||||
6.1,3,4.9,1.8,Iris-virginica
|
||||
6.4,2.8,5.6,2.1,Iris-virginica
|
||||
7.2,3,5.8,1.6,Iris-virginica
|
||||
7.4,2.8,6.1,1.9,Iris-virginica
|
||||
7.9,3.8,6.4,2,Iris-virginica
|
||||
6.4,2.8,5.6,2.2,Iris-virginica
|
||||
6.3,2.8,5.1,1.5,Iris-virginica
|
||||
6.1,2.6,5.6,1.4,Iris-virginica
|
||||
7.7,3,6.1,2.3,Iris-virginica
|
||||
6.3,3.4,5.6,2.4,Iris-virginica
|
||||
6.4,3.1,5.5,1.8,Iris-virginica
|
||||
6,3,4.8,1.8,Iris-virginica
|
||||
6.9,3.1,5.4,2.1,Iris-virginica
|
||||
6.7,3.1,5.6,2.4,Iris-virginica
|
||||
6.9,3.1,5.1,2.3,Iris-virginica
|
||||
5.8,2.7,5.1,1.9,Iris-virginica
|
||||
6.8,3.2,5.9,2.3,Iris-virginica
|
||||
6.7,3.3,5.7,2.5,Iris-virginica
|
||||
6.7,3,5.2,2.3,Iris-virginica
|
||||
6.3,2.5,5,1.9,Iris-virginica
|
||||
6.5,3,5.2,2,Iris-virginica
|
||||
6.2,3.4,5.4,2.3,Iris-virginica
|
||||
5.9,3,5.1,1.8,Iris-virginica
|
||||
|
414
Untitled.ipynb
Normal file
414
Untitled.ipynb
Normal file
File diff suppressed because one or more lines are too long
85
main.py
Normal file
85
main.py
Normal file
@ -0,0 +1,85 @@
|
||||
from fastapi import FastAPI, Form, Request
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
import logging
|
||||
import joblib
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import os
|
||||
import uvicorn
|
||||
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
handlers=[logging.StreamHandler()]
|
||||
)
|
||||
|
||||
logger = logging.getLogger("FastAPI Iris Predictor")
|
||||
|
||||
try:
|
||||
model = joblib.load("model.pkl")
|
||||
logger.info("Model loaded successfully.")
|
||||
except Exception as e:
|
||||
logger.error("Failed to load the model: %s", e)
|
||||
raise RuntimeError("Model loading failed.") from e
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# Mount static folder for serving static files like CSS, HTML
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
|
||||
|
||||
species_mapping = {0: "Iris Setosa", 1: "Iris Versicolor", 2: "Iris Virginica"}
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def home():
|
||||
try:
|
||||
# Serve the home page
|
||||
with open("static/index.html", "r") as file:
|
||||
logger.info("Home page served.")
|
||||
return file.read()
|
||||
except Exception as e:
|
||||
logger.error("Error serving home page: %s", e)
|
||||
return HTMLResponse(content="Error loading the home page.", status_code=500)
|
||||
|
||||
@app.get("/predict", response_class=HTMLResponse)
|
||||
async def predict():
|
||||
# Serve the prediction page
|
||||
try:
|
||||
with open("static/predict.html", "r") as file:
|
||||
logger.info("Prediction page served.")
|
||||
return file.read()
|
||||
except Exception as e:
|
||||
logger.error("Error serving prediction page: %s", e)
|
||||
return HTMLResponse(content="Error loading the prediction page.", status_code=500)
|
||||
|
||||
@app.post("/predict")
|
||||
async def predict_species(
|
||||
sepal_length: float = Form(...),
|
||||
sepal_width: float = Form(...),
|
||||
petal_length: float = Form(...),
|
||||
petal_width: float = Form(...)
|
||||
):
|
||||
feature_names = ["sepal_length", "sepal_width", "petal_length", "petal_width"]
|
||||
try:
|
||||
input_df = pd.DataFrame([[sepal_length, sepal_width, petal_length, petal_width]], columns=feature_names)
|
||||
logger.info("Input data received: %s", input_df)
|
||||
|
||||
# Predict using the model
|
||||
prediction = model.predict(input_df)[0]
|
||||
|
||||
# Get the species name
|
||||
species = species_mapping.get(prediction, "Unknown")
|
||||
logger.info("Prediction made: %s", species)
|
||||
|
||||
return { "prediction": species}
|
||||
except Exception as e:
|
||||
logger.error("Error making prediction: %s", e)
|
||||
return JSONResponse(content={"error": "Failed to make a prediction"}, status_code=500)
|
||||
|
||||
if __name__ == "__main__":
|
||||
port = int(os.getenv("PORT", 8000)) # Use Heroku's PORT
|
||||
uvicorn.run(app, host="0.0.0.0", port=port)
|
||||
|
||||
10
requirements.txt
Normal file
10
requirements.txt
Normal file
@ -0,0 +1,10 @@
|
||||
fastapi
|
||||
joblib
|
||||
numpy
|
||||
pandas
|
||||
pydantic
|
||||
pytest
|
||||
httpx
|
||||
uvicorn
|
||||
scikit-learn
|
||||
python-multipart
|
||||
18
static/index.html
Normal file
18
static/index.html
Normal file
@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Iris Prediction Home</title>
|
||||
<link rel="stylesheet" href="/static/styles.css">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Welcome to Iris Species Predictor</h1>
|
||||
<p>This application predicts the species of an Iris flower based on its features.</p>
|
||||
<a href="/predict" class="button">Go to Prediction</a>
|
||||
<p>© Meta Brains. All rights reserved.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
57
static/predict.html
Normal file
57
static/predict.html
Normal file
@ -0,0 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Iris Prediction</title>
|
||||
<link rel="stylesheet" href="/static/styles.css">
|
||||
<script>
|
||||
// JavaScript function to handle the form submission
|
||||
async function submitForm(event) {
|
||||
event.preventDefault(); // Prevent the default form submission
|
||||
|
||||
// Get the form data
|
||||
const formData = new FormData(event.target);
|
||||
|
||||
// Prepare the data to send to the FastAPI endpoint
|
||||
const data = new URLSearchParams();
|
||||
for (const [key, value] of formData.entries()) {
|
||||
data.append(key, value);
|
||||
}
|
||||
|
||||
// Send the data to FastAPI via POST request
|
||||
const response = await fetch("/predict", {
|
||||
method: "POST",
|
||||
body: data
|
||||
});
|
||||
|
||||
// Get the JSON response and update the result on the page
|
||||
const result = await response.json();
|
||||
document.getElementById("prediction").innerText = "Predicted Species: " + result.prediction;
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Predict Iris Species</h1>
|
||||
<form onsubmit="submitForm(event)">
|
||||
<label for="sepal_length">Sepal Length (cm):</label>
|
||||
<input type="number" step="0.1" name="sepal_length" id="sepal_length" required>
|
||||
|
||||
<label for="sepal_width">Sepal Width (cm):</label>
|
||||
<input type="number" step="0.1" name="sepal_width" id="sepal_width" required>
|
||||
|
||||
<label for="petal_length">Petal Length (cm):</label>
|
||||
<input type="number" step="0.1" name="petal_length" id="petal_length" required>
|
||||
|
||||
<label for="petal_width">Petal Width (cm):</label>
|
||||
<input type="number" step="0.1" name="petal_width" id="petal_width" required>
|
||||
|
||||
<button type="submit" class="button">Predict</button>
|
||||
</form>
|
||||
|
||||
<!-- Display prediction result here -->
|
||||
<div id="prediction"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
65
static/styles.css
Normal file
65
static/styles.css
Normal file
@ -0,0 +1,65 @@
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background: linear-gradient(135deg, #87CEEB, #FFDEE9);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
text-align: center;
|
||||
margin: 50px auto;
|
||||
max-width: 600px;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
form label {
|
||||
margin-top: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
form input {
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
width: 80%;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
margin: 20px 0;
|
||||
text-decoration: none;
|
||||
border-radius: 5px;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: #45a049;
|
||||
}
|
||||
|
||||
.result {
|
||||
margin-top: 20px;
|
||||
font-size: 1.2em;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
27
test_integration.py
Normal file
27
test_integration.py
Normal file
@ -0,0 +1,27 @@
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from main import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
def test_home_endpoint():
|
||||
"""Test if the home page loads successfully."""
|
||||
response = client.get("/")
|
||||
assert response.status_code == 200, "Failed to load the home page."
|
||||
|
||||
def test_predict_page_endpoint():
|
||||
"""Test if the prediction page loads successfully."""
|
||||
response = client.get("/predict")
|
||||
assert response.status_code == 200, "Failed to load the prediction page."
|
||||
|
||||
def test_predict_species_endpoint():
|
||||
"""Test if the predict endpoint works."""
|
||||
payload = {
|
||||
"sepal_length": 5.1,
|
||||
"sepal_width": 3.5,
|
||||
"petal_length": 1.4,
|
||||
"petal_width": 0.2
|
||||
}
|
||||
response = client.post("/predict", data=payload)
|
||||
assert response.status_code == 200, "Failed to predict species."
|
||||
assert "prediction" in response.json(), "Response missing prediction field."
|
||||
15
test_unit.py
Normal file
15
test_unit.py
Normal file
@ -0,0 +1,15 @@
|
||||
import pytest
|
||||
import pandas as pd
|
||||
from main import model, species_mapping
|
||||
|
||||
|
||||
def test_model_prediction():
|
||||
# Arrange: Create a test input DataFrame
|
||||
input_df = pd.DataFrame([[5.1, 3.5, 1.4, 0.2]], columns=["sepal_length", "sepal_width", "petal_length", "petal_width"])
|
||||
|
||||
# Act: Perform prediction
|
||||
prediction = model.predict(input_df)[0]
|
||||
species = species_mapping.get(prediction, "Unknown")
|
||||
|
||||
# Assert: Check prediction result
|
||||
assert species == "Iris Setosa", "The prediction did not return the expected result."
|
||||
Loading…
x
Reference in New Issue
Block a user