From 4e221610882d7f3a41972e723a5ad9d80655f399 Mon Sep 17 00:00:00 2001 From: VivianDee <115420678+VivianDee@users.noreply.github.com> Date: Tue, 9 Sep 2025 10:57:47 +0100 Subject: [PATCH] [add]: refactor/clean up --- app/__init__.py | 26 --------- app/api/__init__.py | 21 +++++++ app/models/__init__.py | 4 ++ app/salary_analytics/__init__.py | 40 +++++++++++++ app/salary_analytics/core/state.py | 64 +++++++++++++++++++++ app/salary_analytics/events/lifecycle.py | 36 ++++++++++++ app/salary_analytics/helpers/data_checks.py | 12 ++++ app/salary_analytics/routes/base.py | 28 +++++++++ app/salary_analytics/routes/train.py | 33 +++++++++++ docker-compose.yml | 2 + 10 files changed, 240 insertions(+), 26 deletions(-) delete mode 100644 app/__init__.py create mode 100644 app/api/__init__.py create mode 100644 app/models/__init__.py create mode 100644 app/salary_analytics/__init__.py create mode 100644 app/salary_analytics/core/state.py create mode 100644 app/salary_analytics/events/lifecycle.py create mode 100644 app/salary_analytics/helpers/data_checks.py create mode 100644 app/salary_analytics/routes/base.py create mode 100644 app/salary_analytics/routes/train.py diff --git a/app/__init__.py b/app/__init__.py deleted file mode 100644 index 32f1b84..0000000 --- a/app/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -from flask import Flask -import os -from .extensions import db, migrate - - -""" -Salary Analytics Package -A package for analyzing and predicting salary patterns from transaction data. -""" - -__version__ = "0.1.0" - - -def create_app(): - app = Flask(__name__) - app.config.from_object('salary_analytics.config') - - # Initialize extensions - db.init_app(app) - migrate.init_app(app, db) - - # Register blueprints or CLI commands here if needed - from app.analytics.commands import commands - app.cli.add_command(commands.upload_xls_cli) - - return app \ No newline at end of file diff --git a/app/api/__init__.py b/app/api/__init__.py new file mode 100644 index 0000000..dde0fd0 --- /dev/null +++ b/app/api/__init__.py @@ -0,0 +1,21 @@ +from flask import Flask +import os +from app.extensions import db, migrate + + + +def create_app(): + app = Flask(__name__) + + # Load configuration from config.py + app.config.from_object('app.config') + + # Initialize extensions + db.init_app(app) + migrate.init_app(app, db) + + # Register blueprints or CLI commands here if needed + from app.api.commands import commands + app.cli.add_command(commands.upload_xls_cli) + + return app \ No newline at end of file diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..62baa90 --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1,4 @@ +from .raw_transaction import RawTransaction + + +__all__ = ['RawTransaction'] \ No newline at end of file diff --git a/app/salary_analytics/__init__.py b/app/salary_analytics/__init__.py new file mode 100644 index 0000000..babbabb --- /dev/null +++ b/app/salary_analytics/__init__.py @@ -0,0 +1,40 @@ +from fastapi import FastAPI +from app.salary_analytics.routes import analysis, reports, pipeline, load, base, train +from app.salary_analytics.middlewares.middleware import add_middlewares +from app.salary_analytics.events.lifecycle import register_events +from app.utils.logger import logger +import socket + +""" +Salary Analytics Package +A package for analyzing and predicting salary patterns from transaction data. +""" + +__version__ = "0.1.0" + + +def create_app() -> FastAPI: + app = FastAPI( + title="Salary Analytics API", + description="API for analyzing and predicting salary patterns from transaction data", + version="1.0.0" + ) + + # Middlewares + add_middlewares(app) + + # Events + register_events(app) + + # Routers + app.include_router(base.router, tags=["Base"]) + app.include_router(analysis.router, prefix="/analyze", tags=["Analysis"]) + app.include_router(reports.router, tags=["Reports"]) + app.include_router(pipeline.router, tags=["Pipeline"]) + app.include_router(load.router, tags=["Data"]) + app.include_router(train.router, tags=["Model Training"]) + + + return app + +app = create_app() diff --git a/app/salary_analytics/core/state.py b/app/salary_analytics/core/state.py new file mode 100644 index 0000000..4e59c2f --- /dev/null +++ b/app/salary_analytics/core/state.py @@ -0,0 +1,64 @@ +from app.salary_analytics.services.main import SalaryAnalyticsPipeline +from app.salary_analytics.services.data_loader import DataLoader + + +class GlobalState: + def __init__(self): + self._pipeline = None + self._data_loader = None + self.df = None + self.salary_predictor = None + self.salary_earner_analyzer = None + + # ---- Pipeline ---- + @property + def pipeline(self): + if self._pipeline is None: + self._pipeline = SalaryAnalyticsPipeline() + return self._pipeline + + @pipeline.setter + def pipeline(self, value): + self._pipeline = value + + # ---- Data Loader ---- + @property + def data_loader(self): + if self._data_loader is None: + self._data_loader = DataLoader() + return self._data_loader + + @data_loader.setter + def data_loader(self, value): + self._data_loader = value + + # ---- DataFrame ---- + @property + def df(self): + return self._df + + @df.setter + def df(self, value): + self._df = value + + # ---- Salary Predictor ---- + @property + def salary_predictor(self): + return self._salary_predictor + + @salary_predictor.setter + def salary_predictor(self, value): + self._salary_predictor = value + + # ---- Salary Earner Analyzer ---- + @property + def salary_earner_analyzer(self): + return self._salary_earner_analyzer + + @salary_earner_analyzer.setter + def salary_earner_analyzer(self, value): + self._salary_earner_analyzer = value + + +state = GlobalState() + diff --git a/app/salary_analytics/events/lifecycle.py b/app/salary_analytics/events/lifecycle.py new file mode 100644 index 0000000..635ab73 --- /dev/null +++ b/app/salary_analytics/events/lifecycle.py @@ -0,0 +1,36 @@ +import socket +from fastapi import FastAPI +from app.salary_analytics.integrations.salary_detect import SalaryDetect +from app.utils.logger import logger + +salary_detect = SalaryDetect() + + +def register_events(app: FastAPI): + @app.on_event("startup") + async def startup_event(): + """Initialize the pipeline on startup.""" + try: + logger.info("Initializing pipeline...") + + # Start autonomous salary detection loop + salary_detect.start() + logger.info("Started autonomous salary detection loop.") + + # Print network information + hostname = socket.gethostname() + ip_address = socket.gethostbyname(hostname) + logger.info(f"Server running on hostname: {hostname}") + logger.info(f"Server IP address: {ip_address}") + logger.info(f"Server is accessible at:") + logger.info(f"- http://localhost:8000") + logger.info(f"- http://127.0.0.1:8000") + logger.info(f"- http://{ip_address}:8000") + logger.info("Pipeline initialized successfully") + except Exception as e: + logger.error(f"Error during startup: {str(e)}") + raise + + @app.on_event("shutdown") + async def shutdown_event(): + logger.info("Shutting down Salary Analytics API...") diff --git a/app/salary_analytics/helpers/data_checks.py b/app/salary_analytics/helpers/data_checks.py new file mode 100644 index 0000000..47caa4a --- /dev/null +++ b/app/salary_analytics/helpers/data_checks.py @@ -0,0 +1,12 @@ +from fastapi import HTTPException +from app.salary_analytics.core.state import state + + +def check_data_loaded(): + """Raise HTTP 400 if no data is loaded into the pipeline.""" + if state.pipeline.df is None: + raise HTTPException( + status_code=400, + detail="No data loaded. Please load data first using the /load-data endpoint." + ) + return True diff --git a/app/salary_analytics/routes/base.py b/app/salary_analytics/routes/base.py new file mode 100644 index 0000000..c1319fb --- /dev/null +++ b/app/salary_analytics/routes/base.py @@ -0,0 +1,28 @@ +from fastapi import APIRouter +from app.utils.logger import logger +import time + + +router = APIRouter() + +@router.get("/") +async def root(): + """Root endpoint.""" + start_time = time.time() + logger.info("Root endpoint accessed") + response = {"message": "Welcome to Salary Analytics API"} + logger.info(f"Root endpoint completed in {time.time() - start_time:.2f} seconds") + return response + + +@router.get("/health") +async def health_check(): + """Health check endpoint.""" + start_time = time.time() + logger.info("Health check endpoint accessed") + response = {"status": "healthy"} + logger.info(f"Health check completed in {time.time() - start_time:.2f} seconds") + return response + + + diff --git a/app/salary_analytics/routes/train.py b/app/salary_analytics/routes/train.py new file mode 100644 index 0000000..94320cb --- /dev/null +++ b/app/salary_analytics/routes/train.py @@ -0,0 +1,33 @@ +import time +import logging +from fastapi import APIRouter, HTTPException +from app.salary_analytics.services.main import SalaryAnalyticsPipeline +from app.salary_analytics.helpers.data_checks import check_data_loaded +from app.salary_analytics.helpers.response_helpers import AnalysisResponse +from app.salary_analytics.core.state import state +from app.utils.logger import logger + + + +router = APIRouter() + + +@router.post("/train/models", response_model=AnalysisResponse) +async def train_models(): + """Train salary prediction models.""" + start_time = time.time() + try: + check_data_loaded() + logger.info("Starting model training...") + state.pipeline.train_salary_prediction_models() + logger.info("Models trained successfully") + response = AnalysisResponse( + message="Models trained successfully" + ) + logger.info(f"Model training endpoint completed in {time.time() - start_time:.2f} seconds") + return response + except Exception as e: + logger.error(f"Error in model training: {str(e)}") + logger.info(f"Model training endpoint failed after {time.time() - start_time:.2f} seconds") + raise HTTPException(status_code=500, detail=str(e)) + diff --git a/docker-compose.yml b/docker-compose.yml index 2f701b2..9205cb3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,8 @@ services: digifi-analytics: build: . + env_file: + - .env ports: - "${APP_PORT:-4800}:8000" environment: