diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index 5e8d02b..eec765c 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -13,7 +13,7 @@ on: jobs: lint: - runs-on: ubuntu-latest + runs-on: self-hosted steps: - name: Checkout repository @@ -28,7 +28,7 @@ jobs: run: | python -m venv venv source venv/bin/activate - pip install -r requirements.txt + pip install -r backend/requirements.txt - name: Run pylint on Python code run: | @@ -36,7 +36,7 @@ jobs: pylint backend/main.py backend/db.py backend/init_db.py # Add more files as needed test: - runs-on: ubuntu-latest + runs-on: self-hosted needs: lint # Ensure this job runs after linting steps: @@ -52,7 +52,7 @@ jobs: run: | python -m venv venv source venv/bin/activate - pip install -r requirements.txt + pip install -r backend/requirements.txt - name: Run tests with pytest run: | diff --git a/backend/.github/workflows/backend-ci.yml b/backend/.github/workflows/backend-ci.yml index 93a0191..4effa39 100644 --- a/backend/.github/workflows/backend-ci.yml +++ b/backend/.github/workflows/backend-ci.yml @@ -1,39 +1,74 @@ -name: Backend CI +name: Backend CI Pipeline on: push: - paths: - - 'backend/**' + branches: + - develop + - main pull_request: - paths: - - 'backend/**' + branches: + - develop + - main jobs: - backend-ci: - runs-on: ubuntu-latest + lint: + runs-on: self-hosted steps: - - name: Checkout code - uses: actions/checkout@v3 + - name: Checkout repository + uses: actions/checkout@v2 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v2 with: python-version: '3.11' - name: Install dependencies - working-directory: ./backend run: | - pip install -r requirements.txt + python -m venv venv + source venv/bin/activate + pip install -r backend/requirements.txt - - name: Run pylint on backend - working-directory: ./backend + - name: Run pylint on Python code run: | - pylint main.py db.py init_db.py + source venv/bin/activate + pylint backend/main.py backend/db.py backend/init_db.py - - name: Run Pytest - working-directory: ./backend + test: + runs-on: self-hosted + needs: lint # Ensure this job runs after linting + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + python -m venv venv + source venv/bin/activate + pip install -r backend/requirements.txt + + - name: Start the API server + run: | + nohup python backend/main.py & # Start the API server in the background + # Wait for the server to be ready + for i in {1..10}; do + if curl --silent --fail http://127.0.0.1:5000/; then + echo "Server is up and running." + break + fi + echo "Waiting for server to start... Attempt $i/10" + sleep 3 + done + + - name: Run tests with pytest + env: + BASE_URL: http://127.0.0.1:5000 # Assuming your API is running on this address run: | - python main.py & # Run server - sleep 5 # Give it time to start - pytest tests/ + source venv/bin/activate + pytest backend/tests --maxfail=1 --disable-warnings -q diff --git a/backend/db.py b/backend/db.py index 93f618f..7945515 100644 --- a/backend/db.py +++ b/backend/db.py @@ -1,5 +1,8 @@ +"""Database utility functions for managing the pet adoption database.""" + import sqlite3 import os +from utils import create_tables def init_db(): """Initialize the database, creating tables and inserting initial data.""" @@ -9,34 +12,14 @@ def init_db(): print("Database already initialized.") return - # Connect to SQLite database (it will create the file if it doesn't exist) conn = sqlite3.connect(db_path) - cursor = conn.cursor() - - # Create tables - cursor.execute('''CREATE TABLE IF NOT EXISTS animals ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - species TEXT NOT NULL, - breed TEXT, - age INTEGER, - personality TEXT, - image_path TEXT, - adoption_status TEXT DEFAULT 'Available' - )''') + create_tables(conn) - cursor.execute('''CREATE TABLE IF NOT EXISTS adopters ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - password TEXT NOT NULL, - email TEXT UNIQUE, - join_date TEXT DEFAULT CURRENT_TIMESTAMP - )''') - - # Insert initial data (e.g., a test animal) - cursor.execute('''INSERT INTO animals (name, species, breed, age, personality, image_path) - VALUES (?, ?, ?, ?, ?, ?)''', - ('TestDog', 'Dog', 'Mixed', 2, 'Friendly', '/images/test.jpg')) + cursor = conn.cursor() + cursor.execute(''' + INSERT INTO animals (name, species, breed, age, personality, image_path) + VALUES (?, ?, ?, ?, ?, ?)''', + ('TestDog', 'Dog', 'Mixed', 2, 'Friendly', '/images/test.jpg')) conn.commit() conn.close() diff --git a/backend/init_db.py b/backend/init_db.py index 8c52eea..8908b2a 100644 --- a/backend/init_db.py +++ b/backend/init_db.py @@ -1,53 +1,42 @@ +"""Script to initialize the database and populate it with initial data.""" + import sqlite3 -import random +import os +from utils import create_tables -def create_database_tables(conn): - cursor = conn.cursor() - cursor.execute(''' - CREATE TABLE IF NOT EXISTS animals ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - species TEXT NOT NULL, - breed TEXT, - age INTEGER, - personality TEXT, - image_path TEXT, - adoption_status TEXT DEFAULT 'Available' - ) - ''') - cursor.execute(''' - CREATE TABLE IF NOT EXISTS adopters ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - password TEXT NOT NULL, - email TEXT UNIQUE, - join_date TEXT DEFAULT CURRENT_TIMESTAMP - ) - ''') - conn.commit() +# Constants +DB_PATH = os.path.join(os.path.dirname(__file__), 'animal_shelter.db') def populate_initial_data(conn): + """Populate the database with initial data for testing.""" cursor = conn.cursor() - - # Sample animals data + animals = [ - ('Luna', 'Dog', 'Labrador Mix', 2, 'Playful and energetic', '/images/luna.jpg', 'Available'), - ('Oliver', 'Cat', 'Tabby', 4, 'Independent but affectionate', '/images/oliver.jpg', 'Available'), - ('Max', 'Dog', 'German Shepherd', 3, 'Loyal and intelligent', '/images/max.jpg', 'Available') + ('Luna', 'Dog', 'Labrador Mix', 2, + 'Playful and energetic', '/images/luna.jpg', + 'Available'), + ('Oliver', 'Cat', 'Tabby', 4, + 'Independent but affectionate', '/images/oliver.jpg', + 'Available'), + ('Max', 'Dog', 'German Shepherd', 3, + 'Loyal and intelligent', '/images/max.jpg', + 'Available') ] - + for animal in animals: cursor.execute(''' - INSERT INTO animals (name, species, breed, age, personality, image_path, adoption_status) - VALUES (?, ?, ?, ?, ?, ?, ?) - ''', animal) - + INSERT INTO animals (name, species, breed, age, personality, image_path, adoption_status) + VALUES (?, ?, ?, ?, ?, ?, ?)''', animal) + conn.commit() -if __name__ == "__main__": - print("Initializing pet adoption database...") - conn = sqlite3.connect('animal_shelter.db') - create_database_tables(conn) +def initialize_database(db_path): + """Initialize the database by creating tables and populating initial data.""" + conn = sqlite3.connect(db_path) + create_tables(conn) populate_initial_data(conn) - print("Database setup complete!") conn.close() + print("Database initialized successfully with initial data.") + +if __name__ == "__main__": + initialize_database(DB_PATH) diff --git a/backend/main.py b/backend/main.py index f0ab6ce..435fc78 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,46 +1,28 @@ -from flask import Flask, jsonify, request -from flasgger import Swagger +"""This is the main Flask application for the pet adoption API.""" + import sqlite3 import os -from backend.pets import add_pet, update_pet, delete_pet, search_pets, reset_pets +from flask import Flask, jsonify, request +from flasgger import Swagger +from utils import create_tables -# Initialize Flask app app = Flask(__name__) - -# Initialize Swagger (Flasgger) Swagger(app) -# SQLite database file DATABASE = 'animal_shelter.db' -# Helper function to get a database connection def get_db(): + """Establish a connection to the SQLite database.""" conn = sqlite3.connect(DATABASE) - conn.row_factory = sqlite3.Row # Allows row access by column name + conn.row_factory = sqlite3.Row return conn -# Database initialization function to create tables and initial data def init_db(): - """Create tables and add initial data if they don't exist.""" + """Initialize the database and insert sample data if not already present.""" if not os.path.exists(DATABASE): conn = get_db() - cursor = conn.cursor() + create_tables(conn) - # Create the animals table - cursor.execute(''' - CREATE TABLE IF NOT EXISTS animals ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - species TEXT NOT NULL, - breed TEXT, - age INTEGER, - personality TEXT, - image_path TEXT, - adoption_status TEXT DEFAULT 'Available' - ) - ''') - - # Insert initial pet data for testing pets = [ ('Buddy', 'Dog', 'Golden Retriever', 3, 'Friendly', '/images/buddy.jpg', 'Available'), ('Whiskers', 'Cat', 'Siamese', 2, 'Independent', '/images/whiskers.jpg', 'Available'), @@ -50,103 +32,39 @@ def init_db(): ('Bella', 'Dog', 'Bulldog', 2, 'Gentle', '/images/bella.jpg', 'Available') ] - # Insert data into the animals table + cursor = conn.cursor() cursor.executemany(''' INSERT INTO animals (name, species, breed, age, personality, image_path, adoption_status) - VALUES (?, ?, ?, ?, ?, ?, ?) - ''', pets) - + VALUES (?, ?, ?, ?, ?, ?, ?)''', pets) + conn.commit() conn.close() -# API Routes @app.route('/api/pets', methods=['GET']) def get_pets(): - """Retrieve all pets - --- - responses: - 200: - description: A list of pets - schema: - type: array - items: - type: object - properties: - id: - type: integer - name: - type: string - species: - type: string - breed: - type: string - age: - type: integer - personality: - type: string - image_path: - type: string - adoption_status: - type: string - """ + """Retrieve and return all pets from the database.""" try: conn = get_db() cursor = conn.cursor() cursor.execute("SELECT * FROM animals") pets = cursor.fetchall() - - # Convert rows to a list of dictionaries pet_list = [dict(pet) for pet in pets] conn.close() - return jsonify(pet_list), 200 - except Exception as e: + except sqlite3.Error as e: print(f"An error occurred: {e}") return jsonify({'error': 'Internal Server Error'}), 500 @app.route('/', methods=['GET']) def home(): - return jsonify({'message': 'Welcome to the Pet API! Visit /apidocs for the Swagger documentation.'}) - - + """Return a welcome message for the API.""" + return jsonify({ + 'message': 'Welcome to the Pet API! Visit /apidocs for the Swagger documentation.' + }) @app.route('/api/pets/', methods=['GET']) def get_pet(pet_id): - """Retrieve a single pet by ID - --- - parameters: - - name: pet_id - in: path - type: integer - required: true - description: The unique ID of the pet - responses: - 200: - description: A pet - schema: - type: object - properties: - id: - type: integer - name: - type: string - species: - type: string - breed: - type: string - age: - type: integer - personality: - type: string - image_path: - type: string - adoption_status: - type: string - 404: - description: Pet not found - 500: - description: Internal Server Error - """ + """Retrieve a pet by its ID.""" try: conn = get_db() cursor = conn.cursor() @@ -156,79 +74,45 @@ def get_pet(pet_id): if pet is None: return jsonify({'error': 'Pet not found'}), 404 - # Convert the SQLite row object to a dictionary pet_dict = {key: pet[key] for key in pet.keys()} conn.close() - return jsonify(pet_dict), 200 - except Exception as e: + except sqlite3.Error as e: print(f"An error occurred: {e}") return jsonify({'error': 'Internal Server Error'}), 500 @app.route('/api/pets', methods=['POST']) def add_pet(): - """Add a new pet - --- - parameters: - - name: pet - in: body - required: true - schema: - type: object - properties: - name: - type: string - species: - type: string - breed: - type: string - age: - type: integer - personality: - type: string - image_path: - type: string - adoption_status: - type: string - responses: - 201: - description: Pet created successfully - 400: - description: Invalid pet data - 500: - description: Internal Server Error - """ + """Add a new pet to the database.""" try: pet_data = request.get_json() - - # Validate required fields required_fields = ['name', 'species', 'breed', 'age', 'personality'] + for field in required_fields: if field not in pet_data: return jsonify({'error': f'Missing required field: {field}'}), 400 conn = get_db() cursor = conn.cursor() - cursor.execute(''' INSERT INTO animals (name, species, breed, age, personality, image_path, adoption_status) - VALUES (?, ?, ?, ?, ?, ?, ?) - ''', (pet_data['name'], pet_data['species'], pet_data['breed'], pet_data['age'], - pet_data['personality'], pet_data.get('image_path', '/images/default.jpg'), 'Available')) - + VALUES (?, ?, ?, ?, ?, ?, ?)''', + (pet_data['name'], pet_data['species'], pet_data['breed'], + pet_data['age'], pet_data['personality'], + pet_data.get('image_path', '/images/default.jpg'), 'Available')) + conn.commit() conn.close() - return jsonify({'message': 'Pet created successfully'}), 201 - except Exception as e: + except sqlite3.Error as e: print(f"An error occurred: {e}") return jsonify({'error': 'Internal Server Error'}), 500 -# Initialize database on first run -@app.before_first_request def before_first_request(): + """Run once before the first request to initialize the database.""" init_db() +app.before_request_funcs.setdefault(None, []).append(before_first_request) + if __name__ == "__main__": app.run(debug=True, use_reloader=False) - diff --git a/backend/requirements.txt b/backend/requirements.txt index a9f380d..2b74d94 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,5 +1,5 @@ -Flask==3.1.1 -Flask-Cors==5.0.2 +Flask==3.1.0 +Flask-Cors==5.0.1 flasgger==0.9.7.1 Werkzeug==3.1.3 Jinja2==3.1.5 @@ -9,4 +9,4 @@ click==8.1.8 blinker==1.9.0 PyYAML==6.0.2 mistune==3.1.2 -jsonschema==4.24.0 \ No newline at end of file +jsonschema==4.23.0 diff --git a/backend/utils.py b/backend/utils.py new file mode 100644 index 0000000..cfe1eaf --- /dev/null +++ b/backend/utils.py @@ -0,0 +1,31 @@ +# utils.py +import sqlite3 + +def create_tables(conn): + """Create tables for the database if they do not exist.""" + cursor = conn.cursor() + + cursor.execute(''' + CREATE TABLE IF NOT EXISTS animals ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + species TEXT NOT NULL, + breed TEXT, + age INTEGER, + personality TEXT, + image_path TEXT, + adoption_status TEXT DEFAULT 'Available' + ) + ''') + + cursor.execute(''' + CREATE TABLE IF NOT EXISTS adopters ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL, + email TEXT UNIQUE, + join_date TEXT DEFAULT CURRENT_TIMESTAMP + ) + ''') + + conn.commit()