Skip to content

Commit

Permalink
Feat: added API and Devops local changes (#169)
Browse files Browse the repository at this point in the history
Co-authored-by: Nakul Bhandare <[email protected]>
Co-authored-by: Nakul Bhandare <[email protected]>
Co-authored-by: Henglay-Eung <[email protected]>
Co-authored-by: ravi-p-k1 <[email protected]>
  • Loading branch information
5 people authored Feb 22, 2025
1 parent 07632ec commit f307546
Show file tree
Hide file tree
Showing 31 changed files with 23,508 additions and 15,198 deletions.
18 changes: 18 additions & 0 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Base image
FROM python:3.9-slim

# Set working directory inside the container
WORKDIR /app

# Copy the entire backend folder into the container
# Adjusting the path to copy from /backend folder
COPY . /app

# Install dependencies
RUN pip install --no-cache-dir -r requirements.txt

# Expose the port your app runs on
EXPOSE 8000

# Command to run the application
CMD ["python", "main.py"]
6 changes: 5 additions & 1 deletion backend/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
Test feature to close ticket automatically using Closes #18. Hello
<<<<<<< HEAD
Test feature to close ticket automatically using Closes #18. Hello
=======
Test feature to close ticket automatically using Closes #18. Hello
>>>>>>> fe68087a11103b291e8a14f454bdff774de1db60
14 changes: 14 additions & 0 deletions backend/config/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from psycopg2 import pool
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base

DATABASE_URL = "postgresql://neondb_owner:npg_SbW8PXa4dpzK@ep-summer-poetry-a84hp8dp-pooler.eastus2.azure.neon.tech/neondb?sslmode=require"
connection_pool = pool.SimpleConnectionPool(1, 10, DATABASE_URL)
engine = create_engine(DATABASE_URL)
Base = declarative_base()

def get_connection():
return connection_pool.getconn()

def release_connection(conn):
connection_pool.putconn(conn)
48 changes: 48 additions & 0 deletions backend/controller/plant_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from fastapi import APIRouter, Depends, HTTPException
from schemas.plant_schema import PlantSchema
from schemas.user_schema import UserSchema
from services.plant_service import get_service, PlantService
from fastapi import Depends
from fastapi.responses import JSONResponse
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from services.user_service import get_user_service, UserService


create_plant = APIRouter()

security = HTTPBasic()


def verify_credentials(credentials: HTTPBasicCredentials = Depends(security), user_service: UserService = Depends(get_user_service)):
username = credentials.username
password = credentials.password
user_schema = UserSchema(username=username, password=password)
print(user_service.verify_user(user_schema))
if user_service.verify_user(user_schema) == 0:
raise HTTPException(
status_code= 401,
detail="Invalid credentials",
headers={"WWW-Authenticate": "Basic"},
)
return username


@create_plant.post("/api/plant/data", response_model=PlantSchema)
def create_plant_entry(plant: PlantSchema, service: PlantService = Depends(get_service)):
try:
# Call the service layer to create the plant
response = service.create_plant(plant)

# Check if the response contains an error (we assume error in the response means failure)
if "error" in response:
# If error is present, return the error response with an appropriate status code (400 or 500)
status_code = 400 if "Duplicate" in response["error"] else 500
return JSONResponse(status_code=status_code, content={"status": "error", "error": response["error"]})

# If the response is successful, return the response with a 201 status code
return JSONResponse(status_code=201, content=response)

except Exception as e:
# If an unexpected error occurs during processing, return a 500 status code
return JSONResponse(status_code=500, content={"status": "error", "error": f"Unexpected error: {str(e)}"})

79 changes: 79 additions & 0 deletions backend/dal/plant_dal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from config.database import get_connection, release_connection
from schemas.plant_schema import PlantSchema
import psycopg2
from psycopg2 import DatabaseError, IntegrityError

class PlantDAL:
def __init__(self):
self.conn = get_connection()
self.cursor = self.conn.cursor()

def create_plant(self, plant: PlantSchema):
try:
# Validate input data (optional, based on your requirements)
if not plant.PlantName or not plant.ScientificName or not isinstance(plant.Threshhold, (int, float)):
raise ValueError("Invalid input data")

# Execute the query to insert the plant data
self.cursor.execute("""
INSERT INTO plant (PlantID, PlantName, ScientificName, Threshhold)
VALUES (%s, %s, %s, %s) RETURNING PlantID;
""", (plant.PlantID, plant.PlantName, plant.ScientificName, plant.Threshhold))

# Commit the transaction
self.conn.commit()

# Get the returned PlantID
plant_id = self.cursor.fetchone()[0]

# Return the response in JSON format
return {
"status": "success",
"PlantID": plant_id,
"PlantName": plant.PlantName,
"ScientificName": plant.ScientificName,
"Threshhold": plant.Threshhold
}

except IntegrityError as e:
# Handle duplicate key error (unique constraint violation)
self.conn.rollback() # Rollback transaction on error
error_message = f"Duplicate entry for PlantID: {plant.PlantID}. A plant with this ID already exists."
print(f"IntegrityError: {e}")
return {
"status": "error",
"error": error_message
}

except (psycopg2.Error, DatabaseError) as db_error:
# Handle other database errors
self.conn.rollback() # Rollback transaction on error
error_message = f"Database error: {db_error}"
print(f"Database error: {db_error}")
return {
"status": "error",
"error": error_message
}

except ValueError as val_error:
# Handle input validation errors
error_message = f"Invalid input: {val_error}"
print(f"Input error: {val_error}")
return {
"status": "error",
"error": error_message
}

except Exception as e:
# Catch any other unexpected errors
self.conn.rollback() # Rollback transaction on error
error_message = f"Unexpected error: {e}"
print(f"Unexpected error: {e}")
return {
"status": "error",
"error": error_message
}

finally:
# Ensure that the connection is released
release_connection(self.conn)
41 changes: 41 additions & 0 deletions backend/dal/user_dal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from config.database import get_connection, release_connection
from schemas.user_schema import UserSchema
import psycopg2
from psycopg2 import DatabaseError

class UserDAL:
def __init__(self):
self.conn = get_connection()
self.cursor = self.conn.cursor()

def verify_user(self, user: UserSchema):
try:
if not user.username or not user.password:
raise ValueError("Username and password must not be empty.")

self.cursor.execute("""
SELECT EXISTS (
SELECT 1 FROM users WHERE username = %s AND password = %s
);
""", (user.username, user.password))

return self.cursor.fetchone()[0] # Returns True or False

except (psycopg2.Error, DatabaseError) as db_error:
self.conn.rollback()
print(f"Database error: {db_error}")
return {"status": "error", "error": str(db_error)}

except ValueError as val_error:
print(f"Input error: {val_error}")
return {"status": "error", "error": str(val_error)}

except Exception as e:
self.conn.rollback()
print(f"Unexpected error: {e}")
return {"status": "error", "error": str(e)}

def __del__(self):
""" Ensure the connection is closed when the object is destroyed. """
if self.conn:
release_connection(self.conn)
26 changes: 26 additions & 0 deletions backend/docker/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
version: '3.8'

services:
api:
build: ../
container_name: sproutly-api
networks:
- sproutly-network
expose:
- "8000"

nginx:
image: nginx:latest
container_name: nginx
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
networks:
- sproutly-network
depends_on:
- api

networks:
sproutly-network:
driver: bridge
12 changes: 12 additions & 0 deletions backend/docker/nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
server {
listen 80;
server_name sproutly.com;

location /api/plant/data {
proxy_pass http://sproutly-api:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
24 changes: 24 additions & 0 deletions backend/docker/nginx_ssl.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
server {
listen 80;
server_name sproutly.com;

location / {
return 301 https://$host$request_uri;
}
}

server {
listen 443 ssl;
server_name sproutly.com;

ssl_certificate /etc/nginx/ssl/sproutly.crt;
ssl_certificate_key /etc/nginx/ssl/sproutly.key;

location /api/plant/data {
proxy_pass http://sproutly-api:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
76 changes: 76 additions & 0 deletions backend/local_setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/bin/bash


print_message() {
echo -e "\033[1;34m-------------------------------------\033[0m"
echo -e "\033[1;36m$1\033[0m"
echo -e "\033[1;34m-------------------------------------\033[0m"
}


print_welcome() {
echo -e "\033[1;32m"
echo "**********************************************"
echo " WELCOME TO THE SPROUTLY SETUP! "
echo "**********************************************"
echo -e "\033[1;33m Let's get your environment set up for success! \033[0m"
echo -e "\033[1;32m"
echo "**********************************************"
echo -e "\033[0m"
}

# Step 1: Print the welcome message
print_welcome

# Step 2: Navigate to the backend folder
print_message "Navigating to the backend folder..."
cd "$(dirname "$0")" || exit 1

# Step 3: Ensure Docker is installed
print_message "Checking if Docker is installed..."
if command -v docker &> /dev/null; then
echo -e "\033[1;32mDocker is already installed! Let's keep moving.\033[0m"
else
print_message "Docker is not installed. Please install Docker and try again."
exit 1
fi

# Step 4: Ensure Docker Compose is installed
print_message "Checking if Docker Compose is installed..."
if command -v docker-compose &> /dev/null; then
echo -e "\033[1;32mDocker Compose is good to go!\033[0m"
else
print_message "Docker Compose is not installed. Please install Docker Compose and try again."
exit 1
fi

# Step 5: Navigate to the docker directory where docker-compose.yml is located
print_message "Setting up the Docker environment..."
DOCKER_DIR="./docker"

# Step 6: Build and start the Docker containers
print_message "Starting Docker containers with Docker Compose..."
docker-compose -f "$DOCKER_DIR/docker-compose.yml" down
docker-compose -f "$DOCKER_DIR/docker-compose.yml" up -d --build

# Step 7: Verify containers are running
print_message "Checking if the containers are up and running..."
docker ps

# Step 8: Add sproutly.com to /etc/hosts for local testing
print_message "Updating your /etc/hosts for local testing..."
if ! grep -q "sproutly.com" /etc/hosts; then
echo "127.0.0.1 sproutly.com" | sudo tee -a /etc/hosts
echo -e "\033[1;32mAdded sproutly.com to /etc/hosts. Now you can test locally!\033[0m"
else
echo -e "\033[1;33msproutly.com is already in /etc/hosts.\033[0m"
fi

# Step 9: Show Docker containers status at the end
print_message "🎉 Setup Complete! 🎉"
echo -e "\033[1;32mThe following Docker containers are now running:\033[0m"
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"

# Final completion message with a bit more flair
echo -e "\033[1;34mThank you for setting up Sproutly with us! 🌱 Your environment is now ready.\033[0m"
echo -e "\033[1;32mYou can start testing at: http://sproutly.com/api/plant/data\033[0m"
25 changes: 25 additions & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from fastapi import FastAPI
from controller.plant_controller import create_plant
from config.database import Base, engine
from fastapi.middleware.cors import CORSMiddleware

# Initialize database
Base.metadata.create_all(bind=engine)

# FastAPI App
app = FastAPI()

app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Allow all origins (you can restrict this in production)
allow_credentials=True, # Allow cookies or authorization headers
allow_methods=["*"], # Allow all HTTP methods (GET, POST, PUT, DELETE, etc.)
allow_headers=["*"], # Allow all headers
)

# Include Routes
app.include_router(create_plant)

if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
9 changes: 9 additions & 0 deletions backend/models/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from sqlalchemy import Column, Integer, String, Float
from config.database import Base

class Plant(Base):
__tablename__ = "plants"
PlantID = Column(Integer, primary_key=True, index=True)
PlantName = Column(String(50), nullable=False)
ScientificName = Column(String(50), nullable=False)
Threshhold = Column(Float, nullable=False)
Loading

0 comments on commit f307546

Please sign in to comment.