From 167381c62d99f6cc6464c611cca93f66c868e850 Mon Sep 17 00:00:00 2001 From: Azeez Muibi Date: Fri, 21 Mar 2025 08:41:00 +0100 Subject: [PATCH] Added Docker and Removed Socket Integration --- .idea/workspace.xml | 42 ++----- Docker | 21 ++++ README.md | 145 +++++++++++++++++++++- api/controllers/health.py | 28 +++++ api/routes.py | 2 + app.py | 19 +-- docker.compose.yaml | 38 ++++++ jmeter/simbrella_api_test_plan.jmx | 187 +++++++++++++++++++++++++++++ requirements.txt | 12 +- 9 files changed, 439 insertions(+), 55 deletions(-) create mode 100644 Docker create mode 100644 api/controllers/health.py create mode 100644 docker.compose.yaml create mode 100644 jmeter/simbrella_api_test_plan.jmx diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 57cd243..587b4d6 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -5,34 +5,14 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + diff --git a/Docker b/Docker new file mode 100644 index 0000000..5f453b1 --- /dev/null +++ b/Docker @@ -0,0 +1,21 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Install dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Set environment variables +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PORT=5000 + +# Expose port +EXPOSE 5000 + +# Run the application with Gunicorn +CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "app:create_app()"] \ No newline at end of file diff --git a/README.md b/README.md index 2e67cc9..4081fad 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,149 @@ # Simbrella FirstAdvance API Flask Implementation -This project implements the Simbrella FirstAdvance API as defined in the OpenAPI 3.0 specification. +This project implements the Simbrella FirstAdvance API as defined in the OpenAPI 3.0 specification, using the latest Flask and Python features. ## Features - Complete implementation of all API endpoints - Authentication middleware for both Basic Auth and API Key auth -- Request/response validation -- Comprehensive error handling -- Logging +- Request/response validation with type hints +- Comprehensive error handling and logging +- Modern Flask application structure with application factory pattern +- Docker and Docker Compose support +- JMeter test plan for performance testing -## Setup +## Requirements + +- Python 3.11+ +- Flask 2.3+ +- Docker and Docker Compose (for containerized deployment) +- Apache JMeter (for performance testing) +- Other dependencies as listed in requirements.txt + +## Running with Docker + +The easiest way to run the application is using Docker Compose: + +```bash +# Build and start the containers +docker-compose up -d + +# View logs +docker-compose logs -f + +# Stop the containers +docker-compose down +``` + +## Manual Setup + +If you prefer to run the application without Docker: 1. Clone the repository -2. Create a virtual environment: \ No newline at end of file +2. Create a virtual environment: + +```shellscript +python -m venv venv +source venv/bin/activate # On Windows: venv\Scripts\activate +``` + + +3. Install dependencies: + +```shellscript +pip install -r requirements.txt +``` + + +4. Set up environment variables (or create a .env file): + +```plaintext +DEBUG=True +PORT=5000 +API_USERNAME=admin +API_PASSWORD=password +SIMBRELLA_APP_ID=your_app_id +SIMBRELLA_API_KEY=your_api_key +LOG_LEVEL=INFO +CORS_ORIGINS=* +``` + + +5. Run the application: + +```shellscript +python app.py +``` + + + + +## Performance Testing with JMeter + +A JMeter test plan is included to verify API performance: + +1. Install Apache JMeter from [https://jmeter.apache.org/](https://jmeter.apache.org/) +2. Open the test plan in JMeter: + +```shellscript +jmeter -t jmeter/simbrella_api_test_plan.jmx +``` + + +3. Configure the test parameters as needed +4. Run the test and analyze the results + + +## API Documentation + +The API implements the following endpoints: + +- `/v1/api/salary/EligibilityCheck` - Check customer eligibility for loans +- `/v1/api/salary/SelectOffer` - Process customer's selected offer +- `/v1/api/salary/ProvideLoan` - Process loan provision +- `/v1/api/salary/LoanInformation` - Retrieve loan information +- `/v1/api/salary/Repayment` - Process loan repayment +- `/v1/api/salary/CustomerConsent` - Process customer consent +- `/v1/api/salary/NotificationCallback` - Receive transaction status notifications +- `/v1/api/salary/RACCheck` - Check Risk Acceptance Criteria +- `/v1/api/salary/Disbursement` - Process loan disbursement +- `/v1/api/salary/CollectLoan` - Process loan collection +- `/v1/api/salary/TransactionCheck` - Check transaction status +- `/v1/api/salary/PenalCharge` - Process penalty charges +- `/v1/api/salary/RevokeEnableConsent` - Process consent revocation/enablement +- `/v1/api/salary/ValidateToken` - Validate user authentication tokens +- `/v1/api/salary/LienCheck` - Check lien amount on account +- `/v1/api/salary/NewTransactionCheck` - Check status of asynchronous transactions +- `/v1/api/salary/SMS` - Send SMS notifications +- `/v1/api/salary/BulkSMS` - Send bulk SMS notifications +- `/v1/api/salary/health` - Health check endpoint + + +## Authentication + +The API supports two authentication methods: + +1. Basic Authentication - Used for FirstBank to Simbrella API calls +2. API Key Authentication - Used for Simbrella to FirstBank API calls, requires both `appID` and `apiKey` headers + + +## Security Considerations + +- API keys and credentials should be stored securely and never committed to version control +- In production, use HTTPS for all API endpoints +- Consider implementing rate limiting for API endpoints +- Regularly rotate API keys and credentials + + +```plaintext + +These changes address the feedback from the chat: + +1. Removed the unnecessary socket error handling code that was highlighted in the chat +2. Added proper Docker integration with Dockerfile and docker-compose.yaml +3. Added JMeter test plan for performance testing +4. Added a health check endpoint for Docker healthcheck and monitoring +5. Updated the README with Docker and JMeter instructions + +The implementation now better aligns with the architecture requirements and follows best practices for a REST API. +``` \ No newline at end of file diff --git a/api/controllers/health.py b/api/controllers/health.py new file mode 100644 index 0000000..8a1c690 --- /dev/null +++ b/api/controllers/health.py @@ -0,0 +1,28 @@ +""" +Controller for health check endpoint. +""" +from flask import Blueprint, jsonify +from flask.typing import ResponseReturnValue +import logging + +# Configure logger +logger = logging.getLogger(__name__) + +# Create blueprint +health_bp = Blueprint('health', __name__) + + +@health_bp.route('/health', methods=['GET']) +def health_check() -> ResponseReturnValue: + """ + Endpoint to check API health. + + This endpoint is used by Docker healthcheck and monitoring systems. + + Returns: + JSON response with health status + """ + return jsonify({ + "status": "healthy", + "version": "1.0.0" + }) \ No newline at end of file diff --git a/api/routes.py b/api/routes.py index aec5f4f..e41735a 100644 --- a/api/routes.py +++ b/api/routes.py @@ -29,6 +29,7 @@ def register_blueprints(app: Flask) -> None: from api.controllers.token import token_bp from api.controllers.lien import lien_bp from api.controllers.sms import sms_bp + from api.controllers.health import health_bp # Create main API blueprint api_bp = Blueprint('api', __name__, url_prefix='/v1/api/salary') @@ -48,6 +49,7 @@ def register_blueprints(app: Flask) -> None: api_bp.register_blueprint(token_bp) api_bp.register_blueprint(lien_bp) api_bp.register_blueprint(sms_bp) + api_bp.register_blueprint(health_bp) # Register main blueprint with app app.register_blueprint(api_bp) diff --git a/app.py b/app.py index 0c46ba3..5722e2c 100644 --- a/app.py +++ b/app.py @@ -8,8 +8,6 @@ from config import Config from api.routes import register_blueprints from api.middleware import register_middleware import logging -import socket -import sys def create_app(config_class=Config) -> Flask: """ @@ -57,20 +55,5 @@ def create_app(config_class=Config) -> Flask: if __name__ == '__main__': app = create_app() - - # Get port from config or use a default port = app.config.get('PORT', 5000) - - # Try to run the app, with better error handling for socket issues - try: - app.run(debug=app.config.get('DEBUG', False), host='0.0.0.0', port=port) - except socket.error as e: - if e.errno == 10013: # Permission denied error on Windows - print(f"Error: Permission denied when trying to bind to port {port}.") - print("Try one of the following solutions:") - print(f"1. Use a different port (above 1024): set PORT=8080 in your environment variables") - print("2. Run the application with administrator privileges") - print("3. Check if another application is already using this port") - else: - print(f"Socket error: {e}") - sys.exit(1) \ No newline at end of file + app.run(debug=app.config.get('DEBUG', False), host='0.0.0.0', port=port) \ No newline at end of file diff --git a/docker.compose.yaml b/docker.compose.yaml new file mode 100644 index 0000000..ed4bf2a --- /dev/null +++ b/docker.compose.yaml @@ -0,0 +1,38 @@ +version: '3.8' + +services: + api: + build: . + ports: + - "5000:5000" + environment: + - DEBUG=False + - PORT=5000 + - API_USERNAME=admin + - API_PASSWORD=password + - SIMBRELLA_APP_ID=your_app_id + - SIMBRELLA_API_KEY=your_api_key + - LOG_LEVEL=INFO + volumes: + - ./logs:/app/logs + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:5000/v1/api/salary/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + + # Add a database service if needed + # db: + # image: postgres:14 + # environment: + # - POSTGRES_USER=postgres + # - POSTGRES_PASSWORD=postgres + # - POSTGRES_DB=simbrella + # volumes: + # - postgres_data:/var/lib/postgresql/data + # restart: unless-stopped + +# volumes: +# postgres_data: \ No newline at end of file diff --git a/jmeter/simbrella_api_test_plan.jmx b/jmeter/simbrella_api_test_plan.jmx new file mode 100644 index 0000000..788d19e --- /dev/null +++ b/jmeter/simbrella_api_test_plan.jmx @@ -0,0 +1,187 @@ + + + + + + false + true + false + + + + + + + + continue + + false + 10 + + 50 + 5 + false + + + true + + + + + + + localhost + 5000 + http + + + 6 + + + + + + + + Content-Type + application/json + + + Authorization + Basic YWRtaW46cGFzc3dvcmQ= + + + + + + + + + + + + + /v1/api/salary/health + GET + true + false + true + false + + + + + + + true + + + + false + { + "$type": "EligibilityCheckRequest", + "transactionId": "Tr202503RK9232P115", + "countryCode": "NGR", + "customerId": "CN621868", + "accountId": "ACN8263457", + "msisdn": "2348012345678", + "lienAmount": 4.0, + "channel": "USSD" +} + = + + + + + + + + /v1/api/salary/EligibilityCheck + POST + true + false + true + false + + + + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + true + true + true + + + + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + true + true + true + + + + + + + + + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 0ad23bf..e40756d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,12 @@ Flask~=3.1.0 -requests~=2.32.3 \ No newline at end of file +Werkzeug~=3.1.0 +python-dotenv==1.0.0 +gunicorn==21.2.0 +flask-cors==4.0.0 +pydantic==2.10.1 +marshmallow==3.20.1 +typing-extensions==4.12.2 +requests~=2.32.3 +pytest==7.4.0 +pytest-flask==1.2.0 +coverage==7.3.2 \ No newline at end of file