Initial commit: NextGen PPR System
- FastAPI backend with JWT authentication - MySQL database with full schema - Docker Compose orchestration - CSV data import for 43,208 airports and 519,999 aircraft - Complete PPR management API - Modernized replacement for PHP-based system
This commit is contained in:
26
backend/Dockerfile
Normal file
26
backend/Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
default-libmysqlclient-dev \
|
||||
pkg-config \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy requirements first for better caching
|
||||
COPY requirements.txt .
|
||||
|
||||
# Install Python dependencies
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8000
|
||||
|
||||
# Run the application
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
1
backend/app/__init__.py
Normal file
1
backend/app/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Empty __init__.py files to make Python packages
|
||||
1
backend/app/api/__init__.py
Normal file
1
backend/app/api/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Empty __init__.py files to make Python packages
|
||||
8
backend/app/api/api.py
Normal file
8
backend/app/api/api.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from fastapi import APIRouter
|
||||
from app.api.endpoints import auth, pprs, public
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
api_router.include_router(auth.router, prefix="/auth", tags=["authentication"])
|
||||
api_router.include_router(pprs.router, prefix="/pprs", tags=["pprs"])
|
||||
api_router.include_router(public.router, prefix="/public", tags=["public"])
|
||||
47
backend/app/api/deps.py
Normal file
47
backend/app/api/deps.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from typing import Generator
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from sqlalchemy.orm import Session
|
||||
from app.db.session import SessionLocal
|
||||
from app.core.security import verify_token
|
||||
from app.crud.crud_user import user as crud_user
|
||||
|
||||
security = HTTPBearer()
|
||||
|
||||
|
||||
def get_db() -> Generator:
|
||||
"""Database dependency"""
|
||||
try:
|
||||
db = SessionLocal()
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
async def get_current_user(
|
||||
db: Session = Depends(get_db),
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security)
|
||||
):
|
||||
"""Get current authenticated user"""
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
username = verify_token(credentials.credentials)
|
||||
if username is None:
|
||||
raise credentials_exception
|
||||
|
||||
user = crud_user.get_by_username(db, username=username)
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
|
||||
return user
|
||||
|
||||
|
||||
def get_current_active_user(
|
||||
current_user = Depends(get_current_user),
|
||||
):
|
||||
"""Get current active user (for future use if we add user status)"""
|
||||
return current_user
|
||||
1
backend/app/api/endpoints/__init__.py
Normal file
1
backend/app/api/endpoints/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Empty __init__.py files to make Python packages
|
||||
41
backend/app/api/endpoints/auth.py
Normal file
41
backend/app/api/endpoints/auth.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from datetime import timedelta
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlalchemy.orm import Session
|
||||
from app.api.deps import get_db
|
||||
from app.core.config import settings
|
||||
from app.core.security import create_access_token
|
||||
from app.crud.crud_user import user as crud_user
|
||||
from app.schemas.ppr import Token
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login_for_access_token(
|
||||
db: Session = Depends(get_db),
|
||||
form_data: OAuth2PasswordRequestForm = Depends()
|
||||
):
|
||||
"""OAuth2 compatible token login, get an access token for future requests"""
|
||||
user = crud_user.authenticate(
|
||||
db, username=form_data.username, password=form_data.password
|
||||
)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect username or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
access_token_expires = timedelta(minutes=settings.access_token_expire_minutes)
|
||||
access_token = create_access_token(
|
||||
subject=user.username, expires_delta=access_token_expires
|
||||
)
|
||||
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
|
||||
|
||||
@router.post("/test-token")
|
||||
async def test_token(current_user = Depends(get_db)):
|
||||
"""Test access token"""
|
||||
return current_user
|
||||
162
backend/app/api/endpoints/pprs.py
Normal file
162
backend/app/api/endpoints/pprs.py
Normal file
@@ -0,0 +1,162 @@
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import date
|
||||
from app.api.deps import get_db, get_current_active_user
|
||||
from app.crud.crud_ppr import ppr as crud_ppr
|
||||
from app.schemas.ppr import PPR, PPRCreate, PPRUpdate, PPRStatus, PPRStatusUpdate
|
||||
from app.models.ppr import User
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[PPR])
|
||||
async def get_pprs(
|
||||
request: Request,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
status: Optional[PPRStatus] = None,
|
||||
date_from: Optional[date] = None,
|
||||
date_to: Optional[date] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Get PPR records with optional filtering"""
|
||||
pprs = crud_ppr.get_multi(
|
||||
db, skip=skip, limit=limit, status=status,
|
||||
date_from=date_from, date_to=date_to
|
||||
)
|
||||
return pprs
|
||||
|
||||
|
||||
@router.post("/", response_model=PPR)
|
||||
async def create_ppr(
|
||||
request: Request,
|
||||
ppr_in: PPRCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Create a new PPR record"""
|
||||
ppr = crud_ppr.create(db, obj_in=ppr_in, created_by=current_user.username)
|
||||
|
||||
# Send real-time update via WebSocket
|
||||
if hasattr(request.app.state, 'connection_manager'):
|
||||
await request.app.state.connection_manager.broadcast({
|
||||
"type": "ppr_created",
|
||||
"data": {
|
||||
"id": ppr.id,
|
||||
"ac_reg": ppr.ac_reg,
|
||||
"status": ppr.status.value
|
||||
}
|
||||
})
|
||||
|
||||
return ppr
|
||||
|
||||
|
||||
@router.get("/{ppr_id}", response_model=PPR)
|
||||
async def get_ppr(
|
||||
ppr_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Get a specific PPR record"""
|
||||
ppr = crud_ppr.get(db, ppr_id=ppr_id)
|
||||
if not ppr:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="PPR record not found"
|
||||
)
|
||||
return ppr
|
||||
|
||||
|
||||
@router.put("/{ppr_id}", response_model=PPR)
|
||||
async def update_ppr(
|
||||
request: Request,
|
||||
ppr_id: int,
|
||||
ppr_in: PPRUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Update a PPR record"""
|
||||
db_ppr = crud_ppr.get(db, ppr_id=ppr_id)
|
||||
if not db_ppr:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="PPR record not found"
|
||||
)
|
||||
|
||||
ppr = crud_ppr.update(db, db_obj=db_ppr, obj_in=ppr_in)
|
||||
|
||||
# Send real-time update
|
||||
if hasattr(request.app.state, 'connection_manager'):
|
||||
await request.app.state.connection_manager.broadcast({
|
||||
"type": "ppr_updated",
|
||||
"data": {
|
||||
"id": ppr.id,
|
||||
"ac_reg": ppr.ac_reg,
|
||||
"status": ppr.status.value
|
||||
}
|
||||
})
|
||||
|
||||
return ppr
|
||||
|
||||
|
||||
@router.patch("/{ppr_id}/status", response_model=PPR)
|
||||
async def update_ppr_status(
|
||||
request: Request,
|
||||
ppr_id: int,
|
||||
status_update: PPRStatusUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Update PPR status (LANDED, DEPARTED, etc.)"""
|
||||
ppr = crud_ppr.update_status(db, ppr_id=ppr_id, status=status_update.status)
|
||||
if not ppr:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="PPR record not found"
|
||||
)
|
||||
|
||||
# Log the status change (you might want to create a journal entry here)
|
||||
|
||||
# Send real-time update
|
||||
if hasattr(request.app.state, 'connection_manager'):
|
||||
await request.app.state.connection_manager.broadcast({
|
||||
"type": "status_update",
|
||||
"data": {
|
||||
"id": ppr.id,
|
||||
"ac_reg": ppr.ac_reg,
|
||||
"status": ppr.status.value,
|
||||
"timestamp": ppr.landed_dt.isoformat() if ppr.landed_dt else None
|
||||
}
|
||||
})
|
||||
|
||||
return ppr
|
||||
|
||||
|
||||
@router.delete("/{ppr_id}", response_model=PPR)
|
||||
async def delete_ppr(
|
||||
request: Request,
|
||||
ppr_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Delete (soft delete) a PPR record"""
|
||||
ppr = crud_ppr.delete(db, ppr_id=ppr_id)
|
||||
if not ppr:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="PPR record not found"
|
||||
)
|
||||
|
||||
# Send real-time update
|
||||
if hasattr(request.app.state, 'connection_manager'):
|
||||
await request.app.state.connection_manager.broadcast({
|
||||
"type": "ppr_deleted",
|
||||
"data": {
|
||||
"id": ppr.id,
|
||||
"ac_reg": ppr.ac_reg
|
||||
}
|
||||
})
|
||||
|
||||
return ppr
|
||||
22
backend/app/api/endpoints/public.py
Normal file
22
backend/app/api/endpoints/public.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from app.api.deps import get_db
|
||||
from app.crud.crud_ppr import ppr as crud_ppr
|
||||
from app.schemas.ppr import PPR
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/arrivals", response_model=List[PPR])
|
||||
async def get_public_arrivals(db: Session = Depends(get_db)):
|
||||
"""Get today's arrivals for public display"""
|
||||
arrivals = crud_ppr.get_arrivals_today(db)
|
||||
return arrivals
|
||||
|
||||
|
||||
@router.get("/departures", response_model=List[PPR])
|
||||
async def get_public_departures(db: Session = Depends(get_db)):
|
||||
"""Get today's departures for public display"""
|
||||
departures = crud_ppr.get_departures_today(db)
|
||||
return departures
|
||||
1
backend/app/core/__init__.py
Normal file
1
backend/app/core/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Empty __init__.py files to make Python packages
|
||||
43
backend/app/core/config.py
Normal file
43
backend/app/core/config.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
# Database settings
|
||||
db_host: str = "db" # Docker service name
|
||||
db_user: str = "ppr_user"
|
||||
db_password: str = "ppr_password123"
|
||||
db_name: str = "ppr_nextgen"
|
||||
db_port: int = 3306
|
||||
|
||||
# Security settings
|
||||
secret_key: str = "your-secret-key-change-this-in-production"
|
||||
algorithm: str = "HS256"
|
||||
access_token_expire_minutes: int = 30
|
||||
|
||||
# Mail settings
|
||||
mail_host: str = "send.one.com"
|
||||
mail_port: int = 465
|
||||
mail_username: str = "noreply@swansea-airport.wales"
|
||||
mail_password: str = "SASAGoForward2155"
|
||||
mail_from: str = "noreply@swansea-airport.wales"
|
||||
mail_from_name: str = "Swansea Airport"
|
||||
|
||||
# Application settings
|
||||
api_v1_str: str = "/api/v1"
|
||||
project_name: str = "Airfield PPR API"
|
||||
base_url: str = "https://pprdev.swansea-airport.wales"
|
||||
|
||||
# Redis settings (for future use)
|
||||
redis_url: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
case_sensitive = False
|
||||
|
||||
@property
|
||||
def database_url(self) -> str:
|
||||
return f"mysql+pymysql://{self.db_user}:{self.db_password}@{self.db_host}:{self.db_port}/{self.db_name}"
|
||||
|
||||
|
||||
settings = Settings()
|
||||
47
backend/app/core/security.py
Normal file
47
backend/app/core/security.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, Union
|
||||
from jose import JWTError, jwt
|
||||
from passlib.context import CryptContext
|
||||
from fastapi import HTTPException, status
|
||||
from app.core.config import settings
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
def create_access_token(
|
||||
subject: Union[str, int], expires_delta: Optional[timedelta] = None
|
||||
) -> str:
|
||||
"""Create a new access token"""
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(
|
||||
minutes=settings.access_token_expire_minutes
|
||||
)
|
||||
|
||||
to_encode = {"exp": expire, "sub": str(subject)}
|
||||
encoded_jwt = jwt.encode(to_encode, settings.secret_key, algorithm=settings.algorithm)
|
||||
return encoded_jwt
|
||||
|
||||
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
"""Verify a password against its hash"""
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
|
||||
def get_password_hash(password: str) -> str:
|
||||
"""Generate password hash"""
|
||||
return pwd_context.hash(password)
|
||||
|
||||
|
||||
def verify_token(token: str) -> Optional[str]:
|
||||
"""Verify JWT token and return username"""
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token, settings.secret_key, algorithms=[settings.algorithm]
|
||||
)
|
||||
username: str = payload.get("sub")
|
||||
if username is None:
|
||||
return None
|
||||
return username
|
||||
except JWTError:
|
||||
return None
|
||||
1
backend/app/crud/__init__.py
Normal file
1
backend/app/crud/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Empty __init__.py files to make Python packages
|
||||
120
backend/app/crud/crud_ppr.py
Normal file
120
backend/app/crud/crud_ppr.py
Normal file
@@ -0,0 +1,120 @@
|
||||
from typing import List, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, or_, func, desc
|
||||
from datetime import date, datetime
|
||||
from app.models.ppr import PPRRecord, PPRStatus
|
||||
from app.schemas.ppr import PPRCreate, PPRUpdate
|
||||
|
||||
|
||||
class CRUDPPR:
|
||||
def get(self, db: Session, ppr_id: int) -> Optional[PPRRecord]:
|
||||
return db.query(PPRRecord).filter(PPRRecord.id == ppr_id).first()
|
||||
|
||||
def get_multi(
|
||||
self,
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
status: Optional[PPRStatus] = None,
|
||||
date_from: Optional[date] = None,
|
||||
date_to: Optional[date] = None
|
||||
) -> List[PPRRecord]:
|
||||
query = db.query(PPRRecord)
|
||||
|
||||
if status:
|
||||
query = query.filter(PPRRecord.status == status)
|
||||
|
||||
if date_from:
|
||||
query = query.filter(
|
||||
or_(
|
||||
func.date(PPRRecord.eta) >= date_from,
|
||||
func.date(PPRRecord.etd) >= date_from
|
||||
)
|
||||
)
|
||||
|
||||
if date_to:
|
||||
query = query.filter(
|
||||
or_(
|
||||
func.date(PPRRecord.eta) <= date_to,
|
||||
func.date(PPRRecord.etd) <= date_to
|
||||
)
|
||||
)
|
||||
|
||||
return query.order_by(desc(PPRRecord.submitted_dt)).offset(skip).limit(limit).all()
|
||||
|
||||
def get_arrivals_today(self, db: Session) -> List[PPRRecord]:
|
||||
"""Get today's arrivals"""
|
||||
today = date.today()
|
||||
return db.query(PPRRecord).filter(
|
||||
and_(
|
||||
func.date(PPRRecord.eta) == today,
|
||||
PPRRecord.status == PPRStatus.NEW
|
||||
)
|
||||
).order_by(PPRRecord.eta).all()
|
||||
|
||||
def get_departures_today(self, db: Session) -> List[PPRRecord]:
|
||||
"""Get today's departures"""
|
||||
today = date.today()
|
||||
return db.query(PPRRecord).filter(
|
||||
and_(
|
||||
func.date(PPRRecord.etd) == today,
|
||||
PPRRecord.status == PPRStatus.LANDED
|
||||
)
|
||||
).order_by(PPRRecord.etd).all()
|
||||
|
||||
def create(self, db: Session, obj_in: PPRCreate, created_by: str) -> PPRRecord:
|
||||
db_obj = PPRRecord(
|
||||
**obj_in.dict(),
|
||||
created_by=created_by,
|
||||
status=PPRStatus.NEW
|
||||
)
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
def update(self, db: Session, db_obj: PPRRecord, obj_in: PPRUpdate) -> PPRRecord:
|
||||
update_data = obj_in.dict(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(db_obj, field, value)
|
||||
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
def update_status(
|
||||
self,
|
||||
db: Session,
|
||||
ppr_id: int,
|
||||
status: PPRStatus
|
||||
) -> Optional[PPRRecord]:
|
||||
db_obj = self.get(db, ppr_id)
|
||||
if not db_obj:
|
||||
return None
|
||||
|
||||
db_obj.status = status
|
||||
|
||||
# Set timestamps based on status
|
||||
if status == PPRStatus.LANDED:
|
||||
db_obj.landed_dt = datetime.utcnow()
|
||||
elif status == PPRStatus.DEPARTED:
|
||||
db_obj.departed_dt = datetime.utcnow()
|
||||
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
def delete(self, db: Session, ppr_id: int) -> Optional[PPRRecord]:
|
||||
db_obj = self.get(db, ppr_id)
|
||||
if db_obj:
|
||||
# Soft delete by setting status
|
||||
db_obj.status = PPRStatus.DELETED
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
|
||||
ppr = CRUDPPR()
|
||||
39
backend/app/crud/crud_user.py
Normal file
39
backend/app/crud/crud_user.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from typing import Optional
|
||||
from sqlalchemy.orm import Session
|
||||
from app.models.ppr import User
|
||||
from app.schemas.ppr import UserCreate
|
||||
from app.core.security import get_password_hash, verify_password
|
||||
|
||||
|
||||
class CRUDUser:
|
||||
def get(self, db: Session, user_id: int) -> Optional[User]:
|
||||
return db.query(User).filter(User.id == user_id).first()
|
||||
|
||||
def get_by_username(self, db: Session, username: str) -> Optional[User]:
|
||||
return db.query(User).filter(User.username == username).first()
|
||||
|
||||
def create(self, db: Session, obj_in: UserCreate) -> User:
|
||||
hashed_password = get_password_hash(obj_in.password)
|
||||
db_obj = User(
|
||||
username=obj_in.username,
|
||||
password=hashed_password
|
||||
)
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
def authenticate(self, db: Session, username: str, password: str) -> Optional[User]:
|
||||
user = self.get_by_username(db, username=username)
|
||||
if not user:
|
||||
return None
|
||||
if not verify_password(password, user.password):
|
||||
return None
|
||||
return user
|
||||
|
||||
def is_active(self, user: User) -> bool:
|
||||
# For future use if we add user status
|
||||
return True
|
||||
|
||||
|
||||
user = CRUDUser()
|
||||
1
backend/app/db/__init__.py
Normal file
1
backend/app/db/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Empty __init__.py files to make Python packages
|
||||
15
backend/app/db/session.py
Normal file
15
backend/app/db/session.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from app.core.config import settings
|
||||
|
||||
engine = create_engine(
|
||||
settings.database_url,
|
||||
pool_pre_ping=True,
|
||||
pool_recycle=300,
|
||||
echo=False, # Set to True for SQL debugging
|
||||
)
|
||||
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
Base = declarative_base()
|
||||
78
backend/app/main.py
Normal file
78
backend/app/main.py
Normal file
@@ -0,0 +1,78 @@
|
||||
from fastapi import FastAPI, Depends, HTTPException, WebSocket, WebSocketDisconnect
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from typing import List
|
||||
import json
|
||||
from app.core.config import settings
|
||||
from app.api.api import api_router
|
||||
|
||||
app = FastAPI(
|
||||
title=settings.project_name,
|
||||
openapi_url=f"{settings.api_v1_str}/openapi.json",
|
||||
description="Prior Permission Required (PPR) system API for aircraft operations management",
|
||||
version="2.0.0"
|
||||
)
|
||||
|
||||
# Set up CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # Configure this properly for production
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# WebSocket connection manager for real-time updates
|
||||
class ConnectionManager:
|
||||
def __init__(self):
|
||||
self.active_connections: List[WebSocket] = []
|
||||
|
||||
async def connect(self, websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
self.active_connections.append(websocket)
|
||||
|
||||
def disconnect(self, websocket: WebSocket):
|
||||
self.active_connections.remove(websocket)
|
||||
|
||||
async def send_personal_message(self, message: str, websocket: WebSocket):
|
||||
await websocket.send_text(message)
|
||||
|
||||
async def broadcast(self, message: dict):
|
||||
message_str = json.dumps(message)
|
||||
for connection in self.active_connections:
|
||||
try:
|
||||
await connection.send_text(message_str)
|
||||
except:
|
||||
# Connection is dead, remove it
|
||||
self.active_connections.remove(connection)
|
||||
|
||||
manager = ConnectionManager()
|
||||
|
||||
@app.websocket("/ws/tower-updates")
|
||||
async def websocket_endpoint(websocket: WebSocket):
|
||||
await manager.connect(websocket)
|
||||
try:
|
||||
while True:
|
||||
# Keep connection alive
|
||||
data = await websocket.receive_text()
|
||||
# Echo back for heartbeat
|
||||
await websocket.send_text(f"Heartbeat: {data}")
|
||||
except WebSocketDisconnect:
|
||||
manager.disconnect(websocket)
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {
|
||||
"message": "Airfield PPR API",
|
||||
"version": "2.0.0",
|
||||
"docs": "/docs"
|
||||
}
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
return {"status": "healthy", "timestamp": "2024-01-01T00:00:00Z"}
|
||||
|
||||
# Include API router
|
||||
app.include_router(api_router, prefix=settings.api_v1_str)
|
||||
|
||||
# Make connection manager available to the app
|
||||
app.state.connection_manager = manager
|
||||
1
backend/app/models/__init__.py
Normal file
1
backend/app/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Empty __init__.py files to make Python packages
|
||||
88
backend/app/models/ppr.py
Normal file
88
backend/app/models/ppr.py
Normal file
@@ -0,0 +1,88 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Text, Enum as SQLEnum, BigInteger
|
||||
from sqlalchemy.sql import func
|
||||
from enum import Enum
|
||||
from app.db.session import Base
|
||||
|
||||
|
||||
class PPRStatus(str, Enum):
|
||||
NEW = "NEW"
|
||||
CONFIRMED = "CONFIRMED"
|
||||
CANCELED = "CANCELED"
|
||||
LANDED = "LANDED"
|
||||
DELETED = "DELETED"
|
||||
DEPARTED = "DEPARTED"
|
||||
|
||||
|
||||
class PPRRecord(Base):
|
||||
__tablename__ = "submitted"
|
||||
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
status = Column(SQLEnum(PPRStatus), nullable=False, default=PPRStatus.NEW)
|
||||
ac_reg = Column(String(16), nullable=False)
|
||||
ac_type = Column(String(32), nullable=False)
|
||||
ac_call = Column(String(16), nullable=True)
|
||||
captain = Column(String(64), nullable=False)
|
||||
fuel = Column(String(16), nullable=True)
|
||||
in_from = Column(String(64), nullable=False)
|
||||
eta = Column(DateTime, nullable=False)
|
||||
pob_in = Column(Integer, nullable=False)
|
||||
out_to = Column(String(64), nullable=True)
|
||||
etd = Column(DateTime, nullable=True)
|
||||
pob_out = Column(Integer, nullable=True)
|
||||
email = Column(String(128), nullable=True)
|
||||
phone = Column(String(16), nullable=True)
|
||||
notes = Column(String(2000), nullable=True)
|
||||
landed_dt = Column(DateTime, nullable=True)
|
||||
departed_dt = Column(DateTime, nullable=True)
|
||||
created_by = Column(String(16), nullable=True)
|
||||
submitted_dt = Column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
username = Column(String(50), nullable=False, unique=True, index=True)
|
||||
password = Column(String(255), nullable=False)
|
||||
|
||||
|
||||
class Journal(Base):
|
||||
__tablename__ = "journal"
|
||||
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
ppr_id = Column(Integer, nullable=False, index=True)
|
||||
entry = Column(Text, nullable=False)
|
||||
user = Column(Text, nullable=False)
|
||||
ip = Column(Text, nullable=False)
|
||||
entry_dt = Column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
||||
|
||||
class Airport(Base):
|
||||
__tablename__ = "airports"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
icao = Column(String(4), nullable=False, unique=True, index=True)
|
||||
iata = Column(String(3), nullable=True, index=True)
|
||||
name = Column(String(255), nullable=False)
|
||||
country = Column(String(100), nullable=False)
|
||||
city = Column(String(100), nullable=True)
|
||||
timezone = Column(String(50), nullable=True)
|
||||
latitude = Column(Text, nullable=True)
|
||||
longitude = Column(Text, nullable=True)
|
||||
elevation = Column(Integer, nullable=True)
|
||||
created_at = Column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
||||
|
||||
class Aircraft(Base):
|
||||
__tablename__ = "aircraft"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
icao24 = Column(String(6), nullable=True)
|
||||
registration = Column(String(25), nullable=True, index=True)
|
||||
manufacturer_icao = Column(String(50), nullable=True)
|
||||
type_code = Column(String(30), nullable=True)
|
||||
manufacturer_name = Column(String(255), nullable=True)
|
||||
model = Column(String(255), nullable=True)
|
||||
clean_reg = Column(String(25), nullable=True, index=True)
|
||||
created_at = Column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_at = Column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
1
backend/app/schemas/__init__.py
Normal file
1
backend/app/schemas/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Empty __init__.py files to make Python packages
|
||||
176
backend/app/schemas/ppr.py
Normal file
176
backend/app/schemas/ppr.py
Normal file
@@ -0,0 +1,176 @@
|
||||
from pydantic import BaseModel, EmailStr, validator
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class PPRStatus(str, Enum):
|
||||
NEW = "NEW"
|
||||
CONFIRMED = "CONFIRMED"
|
||||
CANCELED = "CANCELED"
|
||||
LANDED = "LANDED"
|
||||
DELETED = "DELETED"
|
||||
DEPARTED = "DEPARTED"
|
||||
|
||||
|
||||
class PPRBase(BaseModel):
|
||||
ac_reg: str
|
||||
ac_type: str
|
||||
ac_call: Optional[str] = None
|
||||
captain: str
|
||||
fuel: Optional[str] = None
|
||||
in_from: str
|
||||
eta: datetime
|
||||
pob_in: int
|
||||
out_to: Optional[str] = None
|
||||
etd: Optional[datetime] = None
|
||||
pob_out: Optional[int] = None
|
||||
email: Optional[EmailStr] = None
|
||||
phone: Optional[str] = None
|
||||
notes: Optional[str] = None
|
||||
|
||||
@validator('ac_reg')
|
||||
def validate_registration(cls, v):
|
||||
if not v or len(v.strip()) == 0:
|
||||
raise ValueError('Aircraft registration is required')
|
||||
return v.strip().upper()
|
||||
|
||||
@validator('pob_in')
|
||||
def validate_pob_in(cls, v):
|
||||
if v is not None and v < 0:
|
||||
raise ValueError('POB inbound must be non-negative')
|
||||
return v
|
||||
|
||||
@validator('pob_out')
|
||||
def validate_pob_out(cls, v):
|
||||
if v is not None and v < 0:
|
||||
raise ValueError('POB outbound must be non-negative')
|
||||
return v
|
||||
|
||||
|
||||
class PPRCreate(PPRBase):
|
||||
pass
|
||||
|
||||
|
||||
class PPRUpdate(BaseModel):
|
||||
ac_reg: Optional[str] = None
|
||||
ac_type: Optional[str] = None
|
||||
ac_call: Optional[str] = None
|
||||
captain: Optional[str] = None
|
||||
fuel: Optional[str] = None
|
||||
in_from: Optional[str] = None
|
||||
eta: Optional[datetime] = None
|
||||
pob_in: Optional[int] = None
|
||||
out_to: Optional[str] = None
|
||||
etd: Optional[datetime] = None
|
||||
pob_out: Optional[int] = None
|
||||
email: Optional[EmailStr] = None
|
||||
phone: Optional[str] = None
|
||||
notes: Optional[str] = None
|
||||
|
||||
|
||||
class PPRStatusUpdate(BaseModel):
|
||||
status: PPRStatus
|
||||
|
||||
|
||||
class PPRInDBBase(PPRBase):
|
||||
id: int
|
||||
status: PPRStatus
|
||||
landed_dt: Optional[datetime] = None
|
||||
departed_dt: Optional[datetime] = None
|
||||
created_by: Optional[str] = None
|
||||
submitted_dt: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class PPR(PPRInDBBase):
|
||||
pass
|
||||
|
||||
|
||||
class PPRInDB(PPRInDBBase):
|
||||
pass
|
||||
|
||||
|
||||
# User schemas
|
||||
class UserBase(BaseModel):
|
||||
username: str
|
||||
|
||||
|
||||
class UserCreate(UserBase):
|
||||
password: str
|
||||
|
||||
|
||||
class UserInDBBase(UserBase):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class User(UserInDBBase):
|
||||
pass
|
||||
|
||||
|
||||
class UserInDB(UserInDBBase):
|
||||
password: str
|
||||
|
||||
|
||||
# Authentication schemas
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
token_type: str
|
||||
|
||||
|
||||
class TokenData(BaseModel):
|
||||
username: Optional[str] = None
|
||||
|
||||
|
||||
# Journal schemas
|
||||
class JournalBase(BaseModel):
|
||||
ppr_id: int
|
||||
entry: str
|
||||
user: str
|
||||
ip: str
|
||||
|
||||
|
||||
class JournalCreate(JournalBase):
|
||||
pass
|
||||
|
||||
|
||||
class Journal(JournalBase):
|
||||
id: int
|
||||
entry_dt: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# Airport schemas
|
||||
class AirportBase(BaseModel):
|
||||
icao: str
|
||||
iata: Optional[str] = None
|
||||
name: str
|
||||
country: str
|
||||
|
||||
|
||||
class Airport(AirportBase):
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# Aircraft schemas
|
||||
class AircraftBase(BaseModel):
|
||||
icao24: Optional[str] = None
|
||||
registration: Optional[str] = None
|
||||
manufacturericao: Optional[str] = None
|
||||
typecode: Optional[str] = None
|
||||
manufacturername: Optional[str] = None
|
||||
model: Optional[str] = None
|
||||
clean_reg: Optional[str] = None
|
||||
|
||||
|
||||
class Aircraft(AircraftBase):
|
||||
class Config:
|
||||
from_attributes = True
|
||||
17
backend/requirements.txt
Normal file
17
backend/requirements.txt
Normal file
@@ -0,0 +1,17 @@
|
||||
fastapi[all]==0.104.1
|
||||
uvicorn[standard]==0.24.0
|
||||
sqlalchemy==2.0.23
|
||||
alembic==1.12.1
|
||||
pymysql==1.1.0
|
||||
cryptography==41.0.7
|
||||
python-jose[cryptography]==3.3.0
|
||||
passlib[bcrypt]==1.7.4
|
||||
bcrypt==4.0.1
|
||||
python-multipart==0.0.6
|
||||
email-validator==2.1.0
|
||||
pydantic[email]==2.5.0
|
||||
pydantic-settings==2.0.3
|
||||
pytest==7.4.3
|
||||
pytest-asyncio==0.21.1
|
||||
httpx==0.25.2
|
||||
redis==5.0.1
|
||||
132
backend/schema_dump.sql
Normal file
132
backend/schema_dump.sql
Normal file
@@ -0,0 +1,132 @@
|
||||
/*M!999999\- enable the sandbox mode */
|
||||
-- MariaDB dump 10.19 Distrib 10.6.22-MariaDB, for debian-linux-gnu (x86_64)
|
||||
--
|
||||
-- Host: sasaprod.pattinson.org Database: pprdevdb
|
||||
-- ------------------------------------------------------
|
||||
-- Server version 9.2.0
|
||||
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||
/*!40101 SET NAMES utf8mb4 */;
|
||||
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
|
||||
/*!40103 SET TIME_ZONE='+00:00' */;
|
||||
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
|
||||
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
|
||||
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
|
||||
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
|
||||
|
||||
--
|
||||
-- Table structure for table `aircraft`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `aircraft`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `aircraft` (
|
||||
`icao24` text,
|
||||
`registration` text,
|
||||
`manufacturericao` text,
|
||||
`typecode` text,
|
||||
`manufacturername` text,
|
||||
`model` text,
|
||||
`clean_reg` text GENERATED ALWAYS AS (regexp_replace(`registration`,_utf8mb4'[^a-zA-Z0-9]',_utf8mb4'')) STORED,
|
||||
KEY `reg_idx` (`registration`(8)),
|
||||
KEY `clean_idx` (`clean_reg`(8))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `airports`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `airports`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `airports` (
|
||||
`icao` text NOT NULL,
|
||||
`iata` text,
|
||||
`name` text NOT NULL,
|
||||
`country` text NOT NULL,
|
||||
KEY `icao_idx` (`icao`(4)),
|
||||
KEY `iata_idx` (`iata`(3))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `journal`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `journal`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `journal` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`ppr_id` int NOT NULL,
|
||||
`entry` text NOT NULL,
|
||||
`user` text NOT NULL,
|
||||
`ip` text NOT NULL,
|
||||
`entry_dt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `id_idx` (`ppr_id`) USING BTREE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=422 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `submitted`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `submitted`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `submitted` (
|
||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
|
||||
`status` enum('NEW','CONFIRMED','CANCELED','LANDED','DELETED','DEPARTED') CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL DEFAULT 'NEW',
|
||||
`ac_reg` varchar(16) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL,
|
||||
`ac_type` varchar(32) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL,
|
||||
`ac_call` varchar(16) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL,
|
||||
`captain` varchar(64) NOT NULL,
|
||||
`fuel` varchar(16) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL,
|
||||
`in_from` varchar(64) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL,
|
||||
`eta` datetime NOT NULL,
|
||||
`pob_in` int NOT NULL,
|
||||
`out_to` varchar(64) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL,
|
||||
`etd` datetime DEFAULT NULL,
|
||||
`pob_out` int DEFAULT NULL,
|
||||
`email` varchar(128) DEFAULT NULL,
|
||||
`phone` varchar(16) DEFAULT NULL,
|
||||
`notes` varchar(2000) DEFAULT NULL,
|
||||
`landed_dt` datetime DEFAULT NULL,
|
||||
`departed_dt` datetime DEFAULT NULL,
|
||||
`created_by` varchar(16) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL,
|
||||
`submitted_dt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY `id` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=271 DEFAULT CHARSET=latin1;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `users`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `users`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `users` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`username` varchar(50) NOT NULL,
|
||||
`password` varchar(255) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `username` (`username`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
|
||||
|
||||
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
||||
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
||||
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
|
||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||
|
||||
-- Dump completed on 2025-10-21 15:56:53
|
||||
18
backend/start.sh
Normal file
18
backend/start.sh
Normal file
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Startup script for the FastAPI backend
|
||||
echo "Starting Airfield PPR API..."
|
||||
|
||||
# Install dependencies if needed
|
||||
if [ ! -d "venv" ]; then
|
||||
echo "Creating virtual environment..."
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
else
|
||||
source venv/bin/activate
|
||||
fi
|
||||
|
||||
# Start the application
|
||||
echo "Starting FastAPI server on port 8000..."
|
||||
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
||||
42
backend/tests/test_main.py
Normal file
42
backend/tests/test_main.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from app.main import app
|
||||
from app.api.deps import get_db
|
||||
from app.db.session import Base
|
||||
|
||||
# Create test database
|
||||
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
|
||||
engine = create_engine(
|
||||
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
|
||||
)
|
||||
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
def override_get_db():
|
||||
try:
|
||||
db = TestingSessionLocal()
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
def test_read_main():
|
||||
response = client.get("/")
|
||||
assert response.status_code == 200
|
||||
assert "Airfield PPR API" in response.json()["message"]
|
||||
|
||||
def test_health_check():
|
||||
response = client.get("/health")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["status"] == "healthy"
|
||||
|
||||
def test_get_public_arrivals():
|
||||
response = client.get("/api/v1/public/arrivals")
|
||||
assert response.status_code == 200
|
||||
assert isinstance(response.json(), list)
|
||||
Reference in New Issue
Block a user