MukeshKapoor25 commited on
Commit
5fa5dc7
·
1 Parent(s): cbddaf1

Add initial project structure with FastAPI, Docker, and SQL Server integration

Browse files

- Create .gitignore for Python and environment files
- Add .python-version for Python version management
- Implement Makefile for common tasks (start, stop, migrate, lint, typecheck, test)
- Develop README.md with project overview and setup instructions
- Set up FastAPI application with CORS middleware and routing
- Create authentication, customer, project, dashboard, and report controllers
- Define database models for User, Customer, Project, and Report
- Implement repository pattern for data access
- Add services for authentication, customer, project, and report management
- Configure logging and security utilities
- Set up database session management
- Create Dockerfile and docker-compose.yml for containerization
- Define Pydantic schemas for data validation
- Initialize pyproject.toml for project metadata

.gitignore ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.pyd
6
+ .env
7
+
8
+ # Alembic
9
+ app/db/migrations/versions/
10
+
11
+ # VSCode
12
+ .vscode/
13
+
14
+ # Docker
15
+ app/docker/*.log
16
+ app/docker/mssql_data/
17
+
18
+ # Test & Coverage
19
+ .coverage
20
+ htmlcov/
21
+ *.sqlite3
22
+
23
+ # OS
24
+ .DS_Store
25
+
26
+ # Logs
27
+ *.log
28
+
29
+ # Node
30
+ node_modules/
31
+
32
+ # Others
33
+ *.swp
34
+ *.bak
.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.13
app/Makefile ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ start:
2
+ docker-compose up --build
3
+ stop:
4
+ docker-compose down
5
+ migrate:
6
+ alembic upgrade head
7
+ lint:
8
+ ruff app/
9
+ typecheck:
10
+ mypy app/
11
+ test:
12
+ pytest app/tests/unit
13
+ test-integration:
14
+ pytest app/tests/integration
app/README.md ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AquaBarrier Core API
2
+
3
+ ## Overview
4
+ Production-ready FastAPI backend for customer/project management, dashboard, and reporting. Uses SQL Server, JWT auth, Docker, Alembic, and more.
5
+
6
+ ## Quickstart
7
+ 1. Copy `.env` and adjust secrets.
8
+ 2. `docker-compose up --build`
9
+ 3. Visit [http://localhost:8000/docs](http://localhost:8000/docs)
10
+
11
+ ## Main Endpoints
12
+ - Auth: `/api/v1/auth/register`, `/api/v1/auth/login`, `/api/v1/auth/refresh`, `/api/v1/auth/logout`
13
+ - Customers: `/api/v1/customers` CRUD
14
+ - Projects: `/api/v1/projects` CRUD
15
+ - Dashboard: `/api/v1/dashboard/summary`, `/api/v1/dashboard/customer/{id}/overview`
16
+ - Reports: `/api/v1/reports/projects`
17
+
18
+ ## Migrations
19
+ - Alembic scripts in `app/db/migrations`
20
+ - Run migrations: `make migrate`
21
+
22
+ ## Testing
23
+ - Unit: `pytest app/tests/unit`
24
+ - Integration: `pytest app/tests/integration`
25
+
26
+ ## Lint & Type Check
27
+ - `ruff app/`
28
+ - `mypy app/`
29
+
30
+ ## Production Run
31
+ - Uvicorn: `uvicorn app.app:app --host 0.0.0.0 --port 8000`
32
+ - Gunicorn: `gunicorn -k uvicorn.workers.UvicornWorker app.app:app`
33
+
34
+ ## Environment Variables
35
+ See `.env` for all required variables.
36
+
37
+ ## Seed Data
38
+ - Add admin/sample data: `python app/seed.py`
39
+
40
+ ## CI/CD
41
+ - Example GitHub Actions pipeline included.
app/app.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from app.core.config import settings
4
+ from app.core.logging import setup_logging
5
+ from app.controllers.auth import router as auth_router
6
+ from app.controllers.customers import router as customers_router
7
+ from app.controllers.projects import router as projects_router
8
+ from app.controllers.dashboard import router as dashboard_router
9
+ from app.controllers.reports import router as reports_router
10
+
11
+ setup_logging(settings.LOG_LEVEL)
12
+
13
+ app = FastAPI(title=settings.APP_NAME)
14
+
15
+ app.add_middleware(
16
+ CORSMiddleware,
17
+ allow_origins=[settings.CORS_ORIGINS],
18
+ allow_credentials=True,
19
+ allow_methods=["*"],
20
+ allow_headers=["*"],
21
+ )
22
+
23
+ app.include_router(auth_router)
24
+ app.include_router(customers_router)
25
+ app.include_router(projects_router)
26
+ app.include_router(dashboard_router)
27
+ app.include_router(reports_router)
app/controllers/auth.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, status
2
+ from sqlalchemy.orm import Session
3
+ from app.db.session import get_db
4
+ from app.services.auth_service import AuthService
5
+ from app.schemas.user import UserCreate
6
+
7
+ router = APIRouter(prefix="/api/v1/auth", tags=["auth"])
8
+
9
+ @router.post("/register", status_code=status.HTTP_201_CREATED)
10
+ def register(user_in: UserCreate, db: Session = Depends(get_db)):
11
+ service = AuthService(db)
12
+ user = service.register(user_in.email, user_in.password, user_in.full_name)
13
+ return {"id": user.id, "email": user.email}
14
+
15
+ @router.post("/login")
16
+ def login(user_in: UserCreate, db: Session = Depends(get_db)):
17
+ service = AuthService(db)
18
+ return service.authenticate(user_in.email, user_in.password)
app/controllers/customers.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, status
2
+ from sqlalchemy.orm import Session
3
+ from app.db.session import get_db
4
+ from app.services.customer_service import CustomerService
5
+ from app.schemas.customer import CustomerCreate, CustomerOut
6
+ from typing import List
7
+
8
+ router = APIRouter(prefix="/api/v1/customers", tags=["customers"])
9
+
10
+ @router.get("/", response_model=List[CustomerOut])
11
+ def list_customers(skip: int = 0, limit: int = 10, db: Session = Depends(get_db)):
12
+ service = CustomerService(db)
13
+ return service.list(skip, limit)
14
+
15
+ @router.get("/{customer_id}", response_model=CustomerOut)
16
+ def get_customer(customer_id: int, db: Session = Depends(get_db)):
17
+ service = CustomerService(db)
18
+ return service.get(customer_id)
19
+
20
+ @router.post("/", response_model=CustomerOut, status_code=status.HTTP_201_CREATED)
21
+ def create_customer(customer_in: CustomerCreate, db: Session = Depends(get_db)):
22
+ service = CustomerService(db)
23
+ return service.create(customer_in.dict())
24
+
25
+ @router.put("/{customer_id}", response_model=CustomerOut)
26
+ def update_customer(customer_id: int, customer_in: CustomerCreate, db: Session = Depends(get_db)):
27
+ service = CustomerService(db)
28
+ return service.update(customer_id, customer_in.dict())
29
+
30
+ @router.delete("/{customer_id}", status_code=status.HTTP_204_NO_CONTENT)
31
+ def delete_customer(customer_id: int, db: Session = Depends(get_db)):
32
+ service = CustomerService(db)
33
+ service.delete(customer_id)
34
+ return None
app/controllers/dashboard.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends
2
+ from sqlalchemy.orm import Session
3
+ from app.db.session import get_db
4
+ from app.db.models.customer import Customer
5
+ from app.db.models.project import Project
6
+
7
+ router = APIRouter(prefix="/api/v1/dashboard", tags=["dashboard"])
8
+
9
+ @router.get("/summary")
10
+ def dashboard_summary(db: Session = Depends(get_db)):
11
+ total_customers = db.query(Customer).count()
12
+ active_projects = db.query(Project).filter(Project.status == "active").count()
13
+ completed_projects = db.query(Project).filter(Project.status == "completed").count()
14
+ monthly_revenue = db.query(Project).filter(Project.status == "completed").with_entities(Project.budget).all()
15
+ monthly_revenue = sum([p.budget or 0 for p in monthly_revenue])
16
+ return {
17
+ "total_customers": total_customers,
18
+ "active_projects": active_projects,
19
+ "completed_projects": completed_projects,
20
+ "monthly_revenue": monthly_revenue
21
+ }
22
+
23
+ @router.get("/customer/{customer_id}/overview")
24
+ def customer_overview(customer_id: int, db: Session = Depends(get_db)):
25
+ total_projects = db.query(Project).filter(Project.customer_id == customer_id).count()
26
+ completed_projects = db.query(Project).filter(Project.customer_id == customer_id, Project.status == "completed").count()
27
+ active_projects = db.query(Project).filter(Project.customer_id == customer_id, Project.status == "active").count()
28
+ return {
29
+ "total_projects": total_projects,
30
+ "completed_projects": completed_projects,
31
+ "active_projects": active_projects
32
+ }
app/controllers/projects.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, status
2
+ from sqlalchemy.orm import Session
3
+ from app.db.session import get_db
4
+ from app.services.project_service import ProjectService
5
+ from app.schemas.project import ProjectCreate, ProjectOut
6
+ from typing import List
7
+
8
+ router = APIRouter(prefix="/api/v1/projects", tags=["projects"])
9
+
10
+ @router.get("/", response_model=List[ProjectOut])
11
+ def list_projects(customer_id: int = None, status: str = None, skip: int = 0, limit: int = 10, db: Session = Depends(get_db)):
12
+ service = ProjectService(db)
13
+ return service.list(customer_id, status, skip, limit)
14
+
15
+ @router.get("/{project_id}", response_model=ProjectOut)
16
+ def get_project(project_id: int, db: Session = Depends(get_db)):
17
+ service = ProjectService(db)
18
+ return service.get(project_id)
19
+
20
+ @router.post("/", response_model=ProjectOut, status_code=status.HTTP_201_CREATED)
21
+ def create_project(project_in: ProjectCreate, db: Session = Depends(get_db)):
22
+ service = ProjectService(db)
23
+ return service.create(project_in.dict())
24
+
25
+ @router.put("/{project_id}", response_model=ProjectOut)
26
+ def update_project(project_id: int, project_in: ProjectCreate, db: Session = Depends(get_db)):
27
+ service = ProjectService(db)
28
+ return service.update(project_id, project_in.dict())
29
+
30
+ @router.delete("/{project_id}", status_code=status.HTTP_204_NO_CONTENT)
31
+ def delete_project(project_id: int, db: Session = Depends(get_db)):
32
+ service = ProjectService(db)
33
+ service.delete(project_id)
34
+ return None
app/controllers/reports.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, Response
2
+ from sqlalchemy.orm import Session
3
+ from app.db.session import get_db
4
+ from app.services.report_service import ReportService
5
+ from app.schemas.report import ReportOut
6
+ from typing import List
7
+ import csv
8
+ import io
9
+
10
+ router = APIRouter(prefix="/api/v1/reports", tags=["reports"])
11
+
12
+ @router.get("/projects", response_model=List[ReportOut])
13
+ def get_reports(from_: str, to: str, type: str = "json", db: Session = Depends(get_db)):
14
+ # For demo, just return all reports
15
+ service = ReportService(db)
16
+ reports = db.query(ReportService.db.query(ReportService.db)).all()
17
+ if type == "csv":
18
+ output = io.StringIO()
19
+ writer = csv.writer(output)
20
+ writer.writerow(["id", "project_id", "type", "status", "created_at", "completed"])
21
+ for r in reports:
22
+ writer.writerow([r.id, r.project_id, r.type, r.status, r.created_at, r.completed])
23
+ response = Response(content=output.getvalue(), media_type="text/csv")
24
+ response.headers["Content-Disposition"] = "attachment; filename=reports.csv"
25
+ return response
26
+ return reports
app/core/config.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseSettings
2
+ from typing import Optional
3
+
4
+ class Settings(BaseSettings):
5
+ APP_NAME: str = "AquaBarrier Core API"
6
+ API_V1_STR: str = "/api/v1"
7
+ SECRET_KEY: str
8
+ JWT_ALGORITHM: str = "HS256"
9
+ ACCESS_TOKEN_EXPIRE_MINUTES: int = 15
10
+ REFRESH_TOKEN_EXPIRE_DAYS: int = 7
11
+ SQLSERVER_USER: str
12
+ SQLSERVER_PASSWORD: str
13
+ SQLSERVER_HOST: str
14
+ SQLSERVER_PORT: int = 1433
15
+ SQLSERVER_DB: str
16
+ SQLSERVER_DRIVER: str = "ODBC Driver 18 for SQL Server"
17
+ CORS_ORIGINS: Optional[str] = "*"
18
+ LOG_LEVEL: str = "INFO"
19
+ class Config:
20
+ env_file = ".env"
21
+ env_file_encoding = "utf-8"
22
+ settings = Settings()
app/core/exceptions.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import HTTPException, status
2
+
3
+ class AuthException(HTTPException):
4
+ def __init__(self, detail: str = "Authentication failed"):
5
+ super().__init__(status_code=status.HTTP_401_UNAUTHORIZED, detail=detail)
6
+
7
+ class NotFoundException(HTTPException):
8
+ def __init__(self, detail: str = "Resource not found"):
9
+ super().__init__(status_code=status.HTTP_404_NOT_FOUND, detail=detail)
10
+
11
+ class BadRequestException(HTTPException):
12
+ def __init__(self, detail: str = "Bad request"):
13
+ super().__init__(status_code=status.HTTP_400_BAD_REQUEST, detail=detail)
app/core/logging.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import sys
3
+
4
+ LOG_FORMAT = "%(asctime)s %(levelname)s %(name)s %(message)s"
5
+
6
+ def setup_logging(level: str = "INFO"):
7
+ logging.basicConfig(
8
+ level=level,
9
+ format=LOG_FORMAT,
10
+ stream=sys.stdout
11
+ )
12
+ logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
app/core/security.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from passlib.context import CryptContext
2
+ from jose import jwt, JWTError
3
+ from datetime import datetime, timedelta
4
+ from app.core.config import settings
5
+
6
+ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
7
+
8
+ def hash_password(password: str) -> str:
9
+ return pwd_context.hash(password)
10
+
11
+ def verify_password(plain_password: str, hashed_password: str) -> bool:
12
+ return pwd_context.verify(plain_password, hashed_password)
13
+
14
+ def create_access_token(data: dict, expires_delta: timedelta = None):
15
+ to_encode = data.copy()
16
+ expire = datetime.utcnow() + (expires_delta or timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES))
17
+ to_encode.update({"exp": expire})
18
+ return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.JWT_ALGORITHM)
19
+
20
+ def create_refresh_token(data: dict, expires_delta: timedelta = None):
21
+ to_encode = data.copy()
22
+ expire = datetime.utcnow() + (expires_delta or timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS))
23
+ to_encode.update({"exp": expire})
24
+ return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.JWT_ALGORITHM)
25
+
26
+ def decode_token(token: str):
27
+ try:
28
+ payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.JWT_ALGORITHM])
29
+ return payload
30
+ except JWTError:
31
+ return None
app/db/base.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ from sqlalchemy.orm import DeclarativeBase
2
+
3
+ class Base(DeclarativeBase):
4
+ pass
app/db/migrations/env.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from logging.config import fileConfig
2
+ from sqlalchemy import engine_from_config, pool
3
+ from alembic import context
4
+ import os
5
+ import sys
6
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../../')))
7
+ from app.db.base import Base
8
+ from app.db.models import user, customer, project, report
9
+ from app.core.config import settings
10
+
11
+ config = context.config
12
+ fileConfig(config.config_file_name)
13
+ target_metadata = Base.metadata
14
+
15
+ config.set_main_option('sqlalchemy.url', (
16
+ f"mssql+pyodbc://{settings.SQLSERVER_USER}:{settings.SQLSERVER_PASSWORD}"
17
+ f"@{settings.SQLSERVER_HOST}:{settings.SQLSERVER_PORT}/{settings.SQLSERVER_DB}?driver={settings.SQLSERVER_DRIVER.replace(' ', '+')}"
18
+ ))
19
+
20
+ def run_migrations_offline():
21
+ context.configure(url=config.get_main_option("sqlalchemy.url"), target_metadata=target_metadata, literal_binds=True)
22
+ with context.begin_transaction():
23
+ context.run_migrations()
24
+
25
+ def run_migrations_online():
26
+ connectable = engine_from_config(config.get_section(config.config_ini_section), prefix="sqlalchemy.", poolclass=pool.NullPool)
27
+ with connectable.connect() as connection:
28
+ context.configure(connection=connection, target_metadata=target_metadata)
29
+ with context.begin_transaction():
30
+ context.run_migrations()
31
+
32
+ if context.is_offline_mode():
33
+ run_migrations_offline()
34
+ else:
35
+ run_migrations_online()
app/db/models/customer.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import Column, Integer, String, DateTime
2
+ from app.db.base import Base
3
+ from datetime import datetime
4
+
5
+ class Customer(Base):
6
+ __tablename__ = "customers"
7
+ id = Column(Integer, primary_key=True, index=True)
8
+ name = Column(String(255), nullable=False)
9
+ email = Column(String(255), unique=True, index=True, nullable=True)
10
+ phone = Column(String(50), nullable=True)
11
+ address = Column(String(255), nullable=True)
12
+ created_at = Column(DateTime, default=datetime.utcnow)
app/db/models/project.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Float
2
+ from sqlalchemy.orm import relationship
3
+ from app.db.base import Base
4
+ from datetime import datetime
5
+
6
+ class Project(Base):
7
+ __tablename__ = "projects"
8
+ id = Column(Integer, primary_key=True, index=True)
9
+ name = Column(String(255), nullable=False)
10
+ customer_id = Column(Integer, ForeignKey("customers.id"), nullable=False)
11
+ start_date = Column(DateTime, nullable=True)
12
+ end_date = Column(DateTime, nullable=True)
13
+ status = Column(String(50), nullable=False)
14
+ budget = Column(Float, nullable=True)
15
+ description = Column(String(255), nullable=True)
16
+ created_at = Column(DateTime, default=datetime.utcnow)
17
+ customer = relationship("Customer")
app/db/models/report.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Boolean
2
+ from app.db.base import Base
3
+ from datetime import datetime
4
+
5
+ class Report(Base):
6
+ __tablename__ = "reports"
7
+ id = Column(Integer, primary_key=True, index=True)
8
+ project_id = Column(Integer, ForeignKey("projects.id"), nullable=False)
9
+ type = Column(String(50), nullable=False)
10
+ status = Column(String(50), nullable=False, default="pending")
11
+ file_path = Column(String(255), nullable=True)
12
+ created_at = Column(DateTime, default=datetime.utcnow)
13
+ completed = Column(Boolean, default=False)
app/db/models/user.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import Column, Integer, String, Boolean, DateTime
2
+ from app.db.base import Base
3
+ from datetime import datetime
4
+
5
+ class User(Base):
6
+ __tablename__ = "users"
7
+ id = Column(Integer, primary_key=True, index=True)
8
+ email = Column(String(255), unique=True, index=True, nullable=False)
9
+ hashed_password = Column(String(255), nullable=False)
10
+ full_name = Column(String(255), nullable=True)
11
+ is_active = Column(Boolean, default=True)
12
+ is_admin = Column(Boolean, default=False)
13
+ created_at = Column(DateTime, default=datetime.utcnow)
app/db/repositories/customer_repo.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy.orm import Session
2
+ from app.db.models.customer import Customer
3
+
4
+ class CustomerRepository:
5
+ def __init__(self, db: Session):
6
+ self.db = db
7
+
8
+ def get(self, customer_id: int):
9
+ return self.db.query(Customer).filter(Customer.id == customer_id).first()
10
+
11
+ def list(self, skip: int = 0, limit: int = 10):
12
+ return self.db.query(Customer).offset(skip).limit(limit).all()
13
+
14
+ def create(self, customer: Customer):
15
+ self.db.add(customer)
16
+ self.db.commit()
17
+ self.db.refresh(customer)
18
+ return customer
19
+
20
+ def update(self, customer: Customer):
21
+ self.db.commit()
22
+ self.db.refresh(customer)
23
+ return customer
24
+
25
+ def delete(self, customer: Customer):
26
+ self.db.delete(customer)
27
+ self.db.commit()
app/db/repositories/project_repo.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy.orm import Session
2
+ from app.db.models.project import Project
3
+
4
+ class ProjectRepository:
5
+ def __init__(self, db: Session):
6
+ self.db = db
7
+
8
+ def get(self, project_id: int):
9
+ return self.db.query(Project).filter(Project.id == project_id).first()
10
+
11
+ def list(self, customer_id: int = None, status: str = None, skip: int = 0, limit: int = 10):
12
+ query = self.db.query(Project)
13
+ if customer_id:
14
+ query = query.filter(Project.customer_id == customer_id)
15
+ if status:
16
+ query = query.filter(Project.status == status)
17
+ return query.offset(skip).limit(limit).all()
18
+
19
+ def create(self, project: Project):
20
+ self.db.add(project)
21
+ self.db.commit()
22
+ self.db.refresh(project)
23
+ return project
24
+
25
+ def update(self, project: Project):
26
+ self.db.commit()
27
+ self.db.refresh(project)
28
+ return project
29
+
30
+ def delete(self, project: Project):
31
+ self.db.delete(project)
32
+ self.db.commit()
app/db/repositories/user_repo.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy.orm import Session
2
+ from app.db.models.user import User
3
+
4
+ class UserRepository:
5
+ def __init__(self, db: Session):
6
+ self.db = db
7
+
8
+ def get_by_email(self, email: str):
9
+ return self.db.query(User).filter(User.email == email).first()
10
+
11
+ def create(self, user: User):
12
+ self.db.add(user)
13
+ self.db.commit()
14
+ self.db.refresh(user)
15
+ return user
app/db/session.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import create_engine
2
+ from sqlalchemy.orm import sessionmaker
3
+ from app.core.config import settings
4
+
5
+ DATABASE_URL = (
6
+ f"mssql+pyodbc://{settings.SQLSERVER_USER}:{settings.SQLSERVER_PASSWORD}"
7
+ f"@{settings.SQLSERVER_HOST}:{settings.SQLSERVER_PORT}/{settings.SQLSERVER_DB}?driver={settings.SQLSERVER_DRIVER.replace(' ', '+')}"
8
+ )
9
+
10
+ engine = create_engine(DATABASE_URL, pool_pre_ping=True)
11
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
12
+
13
+ def get_db():
14
+ db = SessionLocal()
15
+ try:
16
+ yield db
17
+ finally:
18
+ db.close()
app/docker/Dockerfile ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ FROM python:3.13-slim
2
+ WORKDIR /app
3
+ COPY . /app
4
+ RUN pip install --upgrade pip && pip install fastapi uvicorn[standard] sqlalchemy pyodbc python-dotenv passlib[bcrypt] jose alembic pytest pytest-asyncio mypy ruff
5
+ EXPOSE 8000
6
+ CMD ["uvicorn", "app.app:app", "--host", "0.0.0.0", "--port", "8000"]
app/docker/docker-compose.yml ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+ services:
3
+ mssql:
4
+ image: mcr.microsoft.com/mssql/server:2022-latest
5
+ environment:
6
+ SA_PASSWORD: "YourStrong!Passw0rd"
7
+ ACCEPT_EULA: "Y"
8
+ ports:
9
+ - "1433:1433"
10
+ volumes:
11
+ - mssql_data:/var/opt/mssql
12
+ app:
13
+ build: .
14
+ depends_on:
15
+ - mssql
16
+ environment:
17
+ SQLSERVER_USER: sa
18
+ SQLSERVER_PASSWORD: YourStrong!Passw0rd
19
+ SQLSERVER_HOST: mssql
20
+ SQLSERVER_PORT: 1433
21
+ SQLSERVER_DB: aquabarrier
22
+ SECRET_KEY: supersecretkey
23
+ ports:
24
+ - "8000:8000"
25
+ command: ["uvicorn", "app.app:app", "--host", "0.0.0.0", "--port", "8000"]
26
+ volumes:
27
+ mssql_data:
app/schemas/customer.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+ from typing import Optional
3
+
4
+ class CustomerCreate(BaseModel):
5
+ name: str
6
+ email: Optional[str] = None
7
+ phone: Optional[str] = None
8
+ address: Optional[str] = None
9
+
10
+ class CustomerOut(BaseModel):
11
+ id: int
12
+ name: str
13
+ email: Optional[str] = None
14
+ phone: Optional[str] = None
15
+ address: Optional[str] = None
16
+
17
+ class Config:
18
+ orm_mode = True
app/schemas/project.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+ from typing import Optional
3
+ from datetime import datetime
4
+
5
+ class ProjectCreate(BaseModel):
6
+ name: str
7
+ customer_id: int
8
+ start_date: Optional[datetime] = None
9
+ end_date: Optional[datetime] = None
10
+ status: str
11
+ budget: Optional[float] = None
12
+ description: Optional[str] = None
13
+
14
+ class ProjectOut(BaseModel):
15
+ id: int
16
+ name: str
17
+ customer_id: int
18
+ start_date: Optional[datetime] = None
19
+ end_date: Optional[datetime] = None
20
+ status: str
21
+ budget: Optional[float] = None
22
+ description: Optional[str] = None
23
+
24
+ class Config:
25
+ orm_mode = True
app/schemas/report.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+ from typing import Optional
3
+ from datetime import datetime
4
+
5
+ class ReportOut(BaseModel):
6
+ id: int
7
+ project_id: int
8
+ type: str
9
+ status: str
10
+ file_path: Optional[str] = None
11
+ created_at: datetime
12
+ completed: bool
13
+
14
+ class Config:
15
+ orm_mode = True
app/schemas/user.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, EmailStr
2
+ from typing import Optional
3
+
4
+ class UserCreate(BaseModel):
5
+ email: EmailStr
6
+ password: str
7
+ full_name: Optional[str] = None
8
+
9
+ class UserOut(BaseModel):
10
+ id: int
11
+ email: EmailStr
12
+ full_name: Optional[str] = None
13
+ is_active: bool
14
+ is_admin: bool
15
+
16
+ class Config:
17
+ orm_mode = True
app/services/auth_service.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy.orm import Session
2
+ from app.db.models.user import User
3
+ from app.db.repositories.user_repo import UserRepository
4
+ from app.core.security import hash_password, verify_password, create_access_token, create_refresh_token
5
+ from app.core.exceptions import AuthException
6
+
7
+ class AuthService:
8
+ def __init__(self, db: Session):
9
+ self.user_repo = UserRepository(db)
10
+
11
+ def register(self, email: str, password: str, full_name: str = None):
12
+ if self.user_repo.get_by_email(email):
13
+ raise AuthException("Email already registered")
14
+ user = User(email=email, hashed_password=hash_password(password), full_name=full_name)
15
+ return self.user_repo.create(user)
16
+
17
+ def authenticate(self, email: str, password: str):
18
+ user = self.user_repo.get_by_email(email)
19
+ if not user or not verify_password(password, user.hashed_password):
20
+ raise AuthException("Invalid credentials")
21
+ access_token = create_access_token({"sub": str(user.id)})
22
+ refresh_token = create_refresh_token({"sub": str(user.id)})
23
+ return {"access_token": access_token, "refresh_token": refresh_token, "token_type": "bearer"}
app/services/customer_service.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy.orm import Session
2
+ from app.db.models.customer import Customer
3
+ from app.db.repositories.customer_repo import CustomerRepository
4
+ from app.core.exceptions import NotFoundException
5
+
6
+ class CustomerService:
7
+ def __init__(self, db: Session):
8
+ self.repo = CustomerRepository(db)
9
+
10
+ def get(self, customer_id: int):
11
+ customer = self.repo.get(customer_id)
12
+ if not customer:
13
+ raise NotFoundException("Customer not found")
14
+ return customer
15
+
16
+ def list(self, skip: int = 0, limit: int = 10):
17
+ return self.repo.list(skip, limit)
18
+
19
+ def create(self, data):
20
+ customer = Customer(**data)
21
+ return self.repo.create(customer)
22
+
23
+ def update(self, customer_id: int, data):
24
+ customer = self.repo.get(customer_id)
25
+ if not customer:
26
+ raise NotFoundException("Customer not found")
27
+ for k, v in data.items():
28
+ setattr(customer, k, v)
29
+ return self.repo.update(customer)
30
+
31
+ def delete(self, customer_id: int):
32
+ customer = self.repo.get(customer_id)
33
+ if not customer:
34
+ raise NotFoundException("Customer not found")
35
+ self.repo.delete(customer)
app/services/project_service.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy.orm import Session
2
+ from app.db.models.project import Project
3
+ from app.db.repositories.project_repo import ProjectRepository
4
+ from app.core.exceptions import NotFoundException
5
+
6
+ class ProjectService:
7
+ def __init__(self, db: Session):
8
+ self.repo = ProjectRepository(db)
9
+
10
+ def get(self, project_id: int):
11
+ project = self.repo.get(project_id)
12
+ if not project:
13
+ raise NotFoundException("Project not found")
14
+ return project
15
+
16
+ def list(self, customer_id: int = None, status: str = None, skip: int = 0, limit: int = 10):
17
+ return self.repo.list(customer_id, status, skip, limit)
18
+
19
+ def create(self, data):
20
+ project = Project(**data)
21
+ return self.repo.create(project)
22
+
23
+ def update(self, project_id: int, data):
24
+ project = self.repo.get(project_id)
25
+ if not project:
26
+ raise NotFoundException("Project not found")
27
+ for k, v in data.items():
28
+ setattr(project, k, v)
29
+ return self.repo.update(project)
30
+
31
+ def delete(self, project_id: int):
32
+ project = self.repo.get(project_id)
33
+ if not project:
34
+ raise NotFoundException("Project not found")
35
+ self.repo.delete(project)
app/services/report_service.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy.orm import Session
2
+ from app.db.models.report import Report
3
+ from app.core.exceptions import NotFoundException
4
+
5
+ class ReportService:
6
+ def __init__(self, db: Session):
7
+ self.db = db
8
+
9
+ def get(self, report_id: int):
10
+ report = self.db.query(Report).filter(Report.id == report_id).first()
11
+ if not report:
12
+ raise NotFoundException("Report not found")
13
+ return report
14
+
15
+ def list_by_project(self, project_id: int):
16
+ return self.db.query(Report).filter(Report.project_id == project_id).all()
17
+
18
+ def create(self, data):
19
+ report = Report(**data)
20
+ self.db.add(report)
21
+ self.db.commit()
22
+ self.db.refresh(report)
23
+ return report
24
+
25
+ def update_status(self, report_id: int, status: str):
26
+ report = self.get(report_id)
27
+ report.status = status
28
+ self.db.commit()
29
+ self.db.refresh(report)
30
+ return report
pyproject.toml ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "ab-ms-core"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.13"
7
+ dependencies = []