├── app ├── __init__.py ├── california_housing_model.joblib ├── __pycache__ │ ├── server.cpython-311.pyc │ └── __init__.cpython-311.pyc ├── model.py └── server.py ├── .gitignore ├── requirements.txt ├── runserver.py ├── .github └── workflows │ ├── main.yml │ └── GHCR.yml ├── Dockerfile ├── static ├── predict.js └── style.css ├── templates └── index.html └── README.md /app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | 2 | numpy 3 | pandas 4 | scikit-learn 5 | flask 6 | joblib 7 | -------------------------------------------------------------------------------- /app/california_housing_model.joblib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EzioDEVio/MLOps/HEAD/app/california_housing_model.joblib -------------------------------------------------------------------------------- /app/__pycache__/server.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EzioDEVio/MLOps/HEAD/app/__pycache__/server.cpython-311.pyc -------------------------------------------------------------------------------- /app/__pycache__/__init__.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EzioDEVio/MLOps/HEAD/app/__pycache__/__init__.cpython-311.pyc -------------------------------------------------------------------------------- /runserver.py: -------------------------------------------------------------------------------- 1 | 2 | from waitress import serve 3 | from app.server import app 4 | 5 | if __name__ == '__main__': 6 | serve(app, host='0.0.0.0', port=5000) 7 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | 2 | name: MLOps with Python application.. 3 | 4 | on: 5 | #push: 6 | branches: [ main ] 7 | #pull_request: 8 | branches: [ main ] 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: windows-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python 3.8 18 | uses: actions/setup-python@v1 19 | with: 20 | python-version: 3.8 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install -r requirements.txt 25 | - name: Run tests 26 | run: | 27 | python -m unittest discover 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Python runtime as a parent image 2 | FROM python:3.11-slim 3 | 4 | # Set the working directory to /app 5 | WORKDIR /app 6 | 7 | # Copy the current directory contents into the container at /app 8 | COPY . . 9 | 10 | # Install any needed packages specified in requirements.txt 11 | RUN pip install --no-cache-dir -r requirements.txt gunicorn 12 | 13 | # Make port 5000 available to the world outside this container 14 | EXPOSE 5000 15 | 16 | # Define environment variable 17 | ENV NAME World 18 | 19 | # Run app.py when the container launches 20 | CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "app.server:app"] 21 | 22 | # Create a non-root user and switch to it 23 | RUN adduser --disabled-password --gecos "" appuser 24 | USER appuser 25 | 26 | # Health check 27 | HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ 28 | CMD curl -f http://localhost:5000/ || exit 1 29 | -------------------------------------------------------------------------------- /app/model.py: -------------------------------------------------------------------------------- 1 | 2 | import pandas as pd 3 | from sklearn.datasets import fetch_california_housing 4 | from sklearn.model_selection import train_test_split 5 | from sklearn.linear_model import LinearRegression 6 | from sklearn.metrics import mean_squared_error 7 | from joblib import dump 8 | 9 | def train_model(): 10 | # Load data 11 | data = fetch_california_housing() 12 | df = pd.DataFrame(data.data, columns=data.feature_names) 13 | df['TARGET'] = data.target 14 | 15 | # Split data 16 | X = df.drop('TARGET', axis=1) 17 | y = df['TARGET'] 18 | X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) 19 | 20 | # Train model 21 | model = LinearRegression() 22 | model.fit(X_train, y_train) 23 | 24 | # Save the model 25 | dump(model, 'app/california_housing_model.joblib') 26 | 27 | # Predict & Evaluate 28 | predictions = model.predict(X_test) 29 | mse = mean_squared_error(y_test, predictions) 30 | print(f"Model MSE: {mse}") 31 | 32 | if __name__ == "__main__": 33 | train_model() 34 | -------------------------------------------------------------------------------- /static/predict.js: -------------------------------------------------------------------------------- 1 | 2 | document.addEventListener("DOMContentLoaded", function() { 3 | var form = document.getElementById("predictionForm"); 4 | form.addEventListener("submit", function(event) { 5 | event.preventDefault(); // Prevent the default form submission 6 | var formData = new FormData(form); 7 | var requestData = {}; 8 | formData.forEach(function(value, key) { 9 | requestData[key] = value; 10 | }); 11 | 12 | fetch('/predict', { 13 | method: 'POST', 14 | headers: { 15 | 'Content-Type': 'application/json', 16 | }, 17 | body: JSON.stringify(requestData), 18 | }) 19 | .then(response => response.json()) 20 | .then(data => { 21 | document.getElementById('predictionResult').textContent = 'Estimated Price: $' + data.prediction[0].toFixed(2); 22 | }) 23 | .catch((error) => { 24 | console.error('Error:', error); 25 | document.getElementById('predictionResult').textContent = 'Error predicting price.'; 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /app/server.py: -------------------------------------------------------------------------------- 1 | 2 | from flask import Flask, request, jsonify, render_template 3 | import numpy as np 4 | import pandas as pd # Ensure pandas is imported 5 | from joblib import load 6 | import os 7 | 8 | # Define the root directory path 9 | ROOT_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 10 | 11 | # Initialize the Flask application 12 | app = Flask(__name__, root_path=ROOT_PATH) 13 | model = load(os.path.join(ROOT_PATH, 'app', 'california_housing_model.joblib')) 14 | 15 | @app.route('/') 16 | def index(): 17 | # Print the path to the templates directory for debugging 18 | print(f"Templates directory: {app.template_folder}") 19 | return render_template('index.html') 20 | 21 | @app.route('/predict', methods=['POST']) 22 | def predict(): 23 | try: 24 | data = request.get_json() # Get data as JSON 25 | features = [float(data[key]) for key in sorted(data)] # Convert and sort the data 26 | feature_names = ['MedInc', 'HouseAge', 'AveRooms', 'AveBedrms', 'Population', 'AveOccup', 'Latitude', 'Longitude'] # Ensure these match your training data 27 | df = pd.DataFrame([features], columns=feature_names) 28 | prediction = model.predict(df) 29 | return jsonify({'prediction': prediction.tolist()}) 30 | except Exception as e: 31 | print("Error:", str(e)) # Printing the error might give more insights in debugging 32 | return jsonify({'error': str(e)}), 400 33 | 34 | if __name__ == '__main__': 35 | app.run(debug=True) 36 | -------------------------------------------------------------------------------- /static/style.css: -------------------------------------------------------------------------------- 1 | 2 | body, html { 3 | height: 100%; 4 | margin: 0; 5 | font-family: Arial, sans-serif; 6 | background-color: #eaeaea; 7 | } 8 | 9 | .wrapper { 10 | min-height: 100%; 11 | position: relative; 12 | padding-bottom: 50px; /* Footer height */ 13 | } 14 | 15 | header { 16 | background-color: #004b97; 17 | color: #fff; 18 | padding: 10px 0; 19 | text-align: center; 20 | } 21 | 22 | header h1 { 23 | margin: 0; 24 | } 25 | 26 | .form-container { 27 | width: 60%; 28 | margin: 30px auto; 29 | background: #fff; 30 | padding: 20px; 31 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); 32 | border-radius: 8px; 33 | } 34 | 35 | .input-group { 36 | margin-bottom: 10px; 37 | } 38 | 39 | .input-group label { 40 | display: block; 41 | margin-bottom: 5px; 42 | } 43 | 44 | .input-group input { 45 | width: 100%; 46 | padding: 8px; 47 | margin-bottom: 10px; 48 | border: 1px solid #ccc; 49 | border-radius: 4px; 50 | } 51 | 52 | .submit-btn button { 53 | width: 100%; 54 | padding: 10px; 55 | border: none; 56 | background-color: #5cb85c; 57 | color: white; 58 | font-size: 16px; 59 | cursor: pointer; 60 | border-radius: 4px; 61 | transition: background-color 0.3s; 62 | } 63 | 64 | .submit-btn button:hover { 65 | background-color: #4cae4c; 66 | } 67 | 68 | footer { 69 | position: absolute; 70 | width: 100%; 71 | height: 50px; /* Footer height */ 72 | background-color: #333; 73 | color: #fff; 74 | text-align: center; 75 | bottom: 0; 76 | left: 0; 77 | } 78 | 79 | footer p { 80 | margin: 0; 81 | padding: 15px; 82 | } 83 | 84 | /* Add this to your existing style.css content */ 85 | .prediction-result { 86 | margin-top: 20px; 87 | padding: 10px; 88 | background-color: #f8f9fa; 89 | border: 1px solid #ddd; 90 | text-align: center; 91 | font-size: 1.2em; 92 | color: #333; 93 | } 94 | -------------------------------------------------------------------------------- /.github/workflows/GHCR.yml: -------------------------------------------------------------------------------- 1 | name: Build and Push Docker image to Github Container Registry 2 | 3 | on: 4 | #push: 5 | branches: 6 | - main 7 | 8 | concurrency: 9 | group: ghcr-build-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | build-and-push: 14 | runs-on: ubuntu-latest 15 | outputs: 16 | image_tag: ${{ steps.set_output.outputs.image_tag }} # This will set the output for the job 17 | steps: 18 | - name: Check out the code 19 | uses: actions/checkout@v2 20 | 21 | - name: Log in to GitHub Container Registry 22 | uses: docker/login-action@v1 23 | with: 24 | registry: ghcr.io 25 | username: ${{ secrets.DOCKER_USERNAME }} 26 | password: ${{ secrets.DOCKER_PASSWORD }} 27 | 28 | - name: Set lowercase repository owner and set image tag 29 | id: set_output 30 | run: | 31 | REPO_OWNER=$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}') 32 | IMAGE_TAG="v1.2.1" 33 | echo "REPO_OWNER=$REPO_OWNER" >> $GITHUB_ENV 34 | echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV 35 | echo "::set-output name=image_tag::$IMAGE_TAG" # This sets the output for image tag 36 | 37 | - name: Build Docker image with specific tag 38 | run: | 39 | echo "Building Image: ghcr.io/${{ env.REPO_OWNER }}/ghcr-realestateapp:${{ env.IMAGE_TAG }}" 40 | docker build . -t ghcr.io/${{ env.REPO_OWNER }}/ghcr-realestateapp:${{ env.IMAGE_TAG }} 41 | 42 | - name: Push Docker image to GitHub Container Registry 43 | run: | 44 | echo "Pushing Image: ghcr.io/${{ env.REPO_OWNER }}/ghcr-realestateapp:${{ env.IMAGE_TAG }}" 45 | docker push ghcr.io/${{ env.REPO_OWNER }}/ghcr-realestateapp:${{ env.IMAGE_TAG }} 46 | 47 | - name: Tag and push Docker image with commit SHA 48 | run: | 49 | echo "Tagging and Pushing SHA: ${{ github.sha }}" 50 | docker tag ghcr.io/${{ env.REPO_OWNER }}/ghcr-realestateapp:${{ env.IMAGE_TAG }} ghcr.io/${{ env.REPO_OWNER }}/ghcr-realestateapp:${{ github.sha }} 51 | docker push ghcr.io/${{ env.REPO_OWNER }}/ghcr-realestateapp:${{ github.sha }} 52 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |