Spaces:
Paused
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 +34 -0
- .python-version +1 -0
- app/Makefile +14 -0
- app/README.md +41 -0
- app/app.py +27 -0
- app/controllers/auth.py +18 -0
- app/controllers/customers.py +34 -0
- app/controllers/dashboard.py +32 -0
- app/controllers/projects.py +34 -0
- app/controllers/reports.py +26 -0
- app/core/config.py +22 -0
- app/core/exceptions.py +13 -0
- app/core/logging.py +12 -0
- app/core/security.py +31 -0
- app/db/base.py +4 -0
- app/db/migrations/env.py +35 -0
- app/db/models/customer.py +12 -0
- app/db/models/project.py +17 -0
- app/db/models/report.py +13 -0
- app/db/models/user.py +13 -0
- app/db/repositories/customer_repo.py +27 -0
- app/db/repositories/project_repo.py +32 -0
- app/db/repositories/user_repo.py +15 -0
- app/db/session.py +18 -0
- app/docker/Dockerfile +6 -0
- app/docker/docker-compose.yml +27 -0
- app/schemas/customer.py +18 -0
- app/schemas/project.py +25 -0
- app/schemas/report.py +15 -0
- app/schemas/user.py +17 -0
- app/services/auth_service.py +23 -0
- app/services/customer_service.py +35 -0
- app/services/project_service.py +35 -0
- app/services/report_service.py +30 -0
- pyproject.toml +7 -0
|
@@ -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
|
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
3.13
|
|
@@ -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
|
|
@@ -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.
|
|
@@ -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)
|
|
@@ -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)
|
|
@@ -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
|
|
@@ -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 |
+
}
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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()
|
|
@@ -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)
|
|
@@ -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)
|
|
@@ -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
|
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy.orm import DeclarativeBase
|
| 2 |
+
|
| 3 |
+
class Base(DeclarativeBase):
|
| 4 |
+
pass
|
|
@@ -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()
|
|
@@ -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)
|
|
@@ -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")
|
|
@@ -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)
|
|
@@ -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)
|
|
@@ -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()
|
|
@@ -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()
|
|
@@ -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
|
|
@@ -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()
|
|
@@ -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"]
|
|
@@ -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:
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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"}
|
|
@@ -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)
|
|
@@ -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)
|
|
@@ -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
|
|
@@ -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 = []
|