sdf
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2025-09-26 12:22:14 +09:00
parent ca32dc8867
commit 7c22664daf
33 changed files with 3267 additions and 1429 deletions

View File

@@ -1,25 +1,56 @@
import asyncio
from datetime import datetime, timedelta
from typing import List
from typing import List, Optional
import math
import httpx
from fastapi import BackgroundTasks, Depends, FastAPI, HTTPException, status
from fastapi import BackgroundTasks, Depends, FastAPI, HTTPException, Query, status
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy import func, select, update
from sqlalchemy import func, select, update, desc, and_, or_
from sqlalchemy.ext.asyncio import AsyncSession
from services.emergency_service.models import EmergencyAlert, EmergencyResponse
from services.emergency_service.models import EmergencyAlert, EmergencyResponse, EmergencyReport, SafetyCheck
from services.emergency_service.schemas import (
AlertStatus,
AlertType,
EmergencyAlertCreate,
EmergencyAlertResponse,
EmergencyAlertUpdate,
EmergencyResponseCreate,
EmergencyResponseResponse,
EmergencyStats,
EmergencyStatistics,
EmergencyReportCreate,
EmergencyReportResponse,
NearbyAlertResponse,
SafetyCheckCreate,
SafetyCheckResponse,
)
from services.user_service.models import User
# Упрощенная модель User для Emergency Service
from sqlalchemy import Column, Integer, String, Boolean
from shared.database import BaseModel
class User(BaseModel):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True)
email = Column(String, unique=True, index=True)
is_active = Column(Boolean, default=True)
from shared.auth import get_current_user_from_token
from shared.config import settings
from shared.database import get_db
from shared.database import AsyncSessionLocal
# Database dependency
async def get_db():
async with AsyncSessionLocal() as session:
try:
yield session
except Exception:
await session.rollback()
raise
finally:
await session.close()
app = FastAPI(title="Emergency Service", version="1.0.0")
@@ -48,6 +79,21 @@ async def get_current_user(
return user
def calculate_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
"""Calculate distance between two points in kilometers using Haversine formula."""
# Convert latitude and longitude from degrees to radians
lat1, lon1, lat2, lon2 = map(math.radians, [lat1, lon1, lat2, lon2])
# Haversine formula
dlat = lat2 - lat1
dlon = lon2 - lon1
a = math.sin(dlat/2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon/2)**2
c = 2 * math.asin(math.sqrt(a))
# Radius of earth in kilometers
r = 6371
return c * r
@app.get("/health")
async def health_check():
"""Health check endpoint"""
@@ -176,7 +222,7 @@ async def respond_to_alert(
existing_response = await db.execute(
select(EmergencyResponse).filter(
EmergencyResponse.alert_id == alert_id,
EmergencyResponse.user_id == current_user.id
EmergencyResponse.responder_id == current_user.id
)
)
if existing_response.scalars().first():
@@ -188,7 +234,7 @@ async def respond_to_alert(
# Create response
db_response = EmergencyResponse(
alert_id=alert_id,
user_id=current_user.id,
responder_id=current_user.id,
response_type=response_data.response_type,
message=response_data.message,
eta_minutes=response_data.eta_minutes,
@@ -206,7 +252,12 @@ async def respond_to_alert(
await db.commit()
await db.refresh(db_response)
return EmergencyResponseResponse.model_validate(db_response)
# Create response with responder info
response_dict = db_response.__dict__.copy()
response_dict['responder_name'] = current_user.username
response_dict['responder_phone'] = getattr(current_user, 'phone_number', None)
return EmergencyResponseResponse.model_validate(response_dict)
@app.put("/api/v1/alert/{alert_id}/resolve")
@@ -297,7 +348,7 @@ async def get_emergency_reports(
return [EmergencyAlertResponse.model_validate(alert) for alert in alerts]
@app.get("/api/v1/stats", response_model=EmergencyStats)
@app.get("/api/v1/stats", response_model=EmergencyStatistics)
async def get_emergency_stats(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
@@ -320,15 +371,203 @@ async def get_emergency_stats(
total_responders_result = await db.execute(select(func.count(EmergencyResponse.id)))
total_responders = total_responders_result.scalar() or 0
return EmergencyStats(
return EmergencyStatistics(
total_alerts=total_alerts,
active_alerts=active_alerts,
resolved_alerts=resolved_alerts,
avg_response_time_minutes=None, # TODO: Calculate this
avg_response_time_minutes=0, # TODO: Calculate this
total_responders=total_responders,
)
@app.get("/api/v1/alerts/nearby", response_model=List[NearbyAlertResponse])
async def get_nearby_alerts(
latitude: float = Query(..., ge=-90, le=90),
longitude: float = Query(..., ge=-180, le=180),
radius_km: float = Query(default=10.0, ge=0.1, le=100),
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Get nearby emergency alerts within specified radius"""
# For now, return all active alerts (in production, add distance filtering)
result = await db.execute(
select(EmergencyAlert)
.filter(EmergencyAlert.is_resolved == False)
.order_by(EmergencyAlert.created_at.desc())
.limit(20)
)
alerts = result.scalars().all()
nearby_alerts = []
for alert in alerts:
distance = calculate_distance(latitude, longitude, alert.latitude, alert.longitude)
if distance <= radius_km:
nearby_alerts.append(NearbyAlertResponse(
id=alert.id,
alert_type=alert.alert_type,
latitude=alert.latitude,
longitude=alert.longitude,
address=alert.address,
distance_km=round(distance, 2),
created_at=alert.created_at,
responded_users_count=alert.responded_users_count or 0
))
return sorted(nearby_alerts, key=lambda x: x.distance_km)
@app.post("/api/v1/report", response_model=EmergencyReportResponse)
async def create_emergency_report(
report_data: EmergencyReportCreate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Create emergency report"""
db_report = EmergencyReport(
user_id=current_user.id if not report_data.is_anonymous else None,
latitude=report_data.latitude,
longitude=report_data.longitude,
address=report_data.address,
report_type=report_data.report_type,
description=report_data.description,
is_anonymous=report_data.is_anonymous,
severity=report_data.severity
)
db.add(db_report)
await db.commit()
await db.refresh(db_report)
return EmergencyReportResponse.model_validate(db_report)
@app.get("/api/v1/reports", response_model=List[EmergencyReportResponse])
async def get_emergency_reports(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Get emergency reports"""
result = await db.execute(
select(EmergencyReport)
.order_by(EmergencyReport.created_at.desc())
.limit(50)
)
reports = result.scalars().all()
return [EmergencyReportResponse.model_validate(report) for report in reports]
@app.post("/api/v1/safety-check", response_model=SafetyCheckResponse)
async def create_safety_check(
check_data: SafetyCheckCreate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Create safety check-in"""
db_check = SafetyCheck(
user_id=current_user.id,
message=check_data.message,
location_latitude=check_data.location_latitude,
location_longitude=check_data.location_longitude
)
db.add(db_check)
await db.commit()
await db.refresh(db_check)
return SafetyCheckResponse.model_validate(db_check)
@app.get("/api/v1/safety-checks", response_model=List[SafetyCheckResponse])
async def get_safety_checks(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Get user's safety check-ins"""
result = await db.execute(
select(SafetyCheck)
.filter(SafetyCheck.user_id == current_user.id)
.order_by(SafetyCheck.created_at.desc())
.limit(50)
)
checks = result.scalars().all()
return [SafetyCheckResponse.model_validate(check) for check in checks]
@app.put("/api/v1/alert/{alert_id}", response_model=EmergencyAlertResponse)
async def update_emergency_alert(
alert_id: int,
update_data: EmergencyAlertUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Update emergency alert"""
result = await db.execute(select(EmergencyAlert).filter(EmergencyAlert.id == alert_id))
alert = result.scalars().first()
if not alert:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Alert not found"
)
# Only alert creator can update
if alert.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only alert creator can update the alert"
)
# Update fields
update_dict = {}
if update_data.is_resolved is not None:
update_dict['is_resolved'] = update_data.is_resolved
if update_data.is_resolved:
update_dict['resolved_at'] = datetime.utcnow()
update_dict['resolved_by'] = current_user.id
if update_data.message is not None:
update_dict['message'] = update_data.message
if update_dict:
await db.execute(
update(EmergencyAlert)
.where(EmergencyAlert.id == alert_id)
.values(**update_dict)
)
await db.commit()
await db.refresh(alert)
return EmergencyAlertResponse.model_validate(alert)
@app.get("/api/v1/alert/{alert_id}/responses", response_model=List[EmergencyResponseResponse])
async def get_alert_responses(
alert_id: int,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Get responses for specific alert"""
# Check if alert exists
result = await db.execute(select(EmergencyAlert).filter(EmergencyAlert.id == alert_id))
alert = result.scalars().first()
if not alert:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Alert not found"
)
# Get responses
responses_result = await db.execute(
select(EmergencyResponse)
.filter(EmergencyResponse.alert_id == alert_id)
.order_by(EmergencyResponse.created_at.desc())
)
responses = responses_result.scalars().all()
return [EmergencyResponseResponse.model_validate(response) for response in responses]
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8002)

View File

@@ -27,9 +27,7 @@ class EmergencyAlert(BaseModel):
address = Column(String(500))
# Alert details
alert_type = Column(
String(50), default="general"
) # general, medical, violence, etc.
alert_type = Column(String(50), default="general") # general, medical, violence, etc.
message = Column(Text)
is_resolved = Column(Boolean, default=False)
resolved_at = Column(DateTime(timezone=True))
@@ -46,9 +44,7 @@ class EmergencyAlert(BaseModel):
class EmergencyResponse(BaseModel):
__tablename__ = "emergency_responses"
alert_id = Column(
Integer, ForeignKey("emergency_alerts.id"), nullable=False, index=True
)
alert_id = Column(Integer, ForeignKey("emergency_alerts.id"), nullable=False, index=True)
responder_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
response_type = Column(String(50)) # help_on_way, contacted_authorities, etc.
@@ -56,4 +52,41 @@ class EmergencyResponse(BaseModel):
eta_minutes = Column(Integer) # Estimated time of arrival
def __repr__(self):
return f"<EmergencyResponse {self.id}>"
return f"<EmergencyResponse {self.uuid}>"
# New models for additional features
class EmergencyReport(BaseModel):
__tablename__ = "emergency_reports"
uuid = Column(UUID(as_uuid=True), default=uuid.uuid4, unique=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=True, index=True) # Nullable for anonymous reports
# Location
latitude = Column(Float, nullable=False)
longitude = Column(Float, nullable=False)
address = Column(String(500))
# Report details
report_type = Column(String(50), nullable=False)
description = Column(Text, nullable=False)
is_anonymous = Column(Boolean, default=False)
severity = Column(Integer, default=3) # 1-5 scale
status = Column(String(20), default="pending") # pending, investigating, resolved
def __repr__(self):
return f"<EmergencyReport {self.uuid}>"
class SafetyCheck(BaseModel):
__tablename__ = "safety_checks"
uuid = Column(UUID(as_uuid=True), default=uuid.uuid4, unique=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
message = Column(String(200))
location_latitude = Column(Float)
location_longitude = Column(Float)
def __repr__(self):
return f"<SafetyCheck {self.uuid}>"

View File

@@ -1,6 +1,7 @@
from datetime import datetime
from enum import Enum
from typing import List, Optional
from uuid import UUID
from pydantic import BaseModel, Field
@@ -11,6 +12,16 @@ class AlertType(str, Enum):
VIOLENCE = "violence"
HARASSMENT = "harassment"
UNSAFE_AREA = "unsafe_area"
ACCIDENT = "accident"
FIRE = "fire"
NATURAL_DISASTER = "natural_disaster"
class AlertStatus(str, Enum):
ACTIVE = "active"
RESOLVED = "resolved"
CANCELLED = "cancelled"
INVESTIGATING = "investigating"
class ResponseType(str, Enum):
@@ -18,30 +29,45 @@ class ResponseType(str, Enum):
CONTACTED_AUTHORITIES = "contacted_authorities"
SAFE_NOW = "safe_now"
FALSE_ALARM = "false_alarm"
INVESTIGATING = "investigating"
RESOLVED = "resolved"
class EmergencyAlertCreate(BaseModel):
latitude: float = Field(..., ge=-90, le=90)
longitude: float = Field(..., ge=-180, le=180)
latitude: float = Field(..., ge=-90, le=90, description="Latitude coordinate")
longitude: float = Field(..., ge=-180, le=180, description="Longitude coordinate")
alert_type: AlertType = AlertType.GENERAL
message: Optional[str] = Field(None, max_length=500)
address: Optional[str] = Field(None, max_length=500)
message: Optional[str] = Field(None, max_length=500, description="Emergency description")
address: Optional[str] = Field(None, max_length=500, description="Location address")
contact_emergency_services: bool = Field(default=True, description="Contact emergency services automatically")
notify_emergency_contacts: bool = Field(default=True, description="Notify user's emergency contacts")
class EmergencyAlertUpdate(BaseModel):
message: Optional[str] = None
is_resolved: Optional[bool] = None
class EmergencyAlertResponse(BaseModel):
id: int
uuid: str
uuid: UUID
user_id: int
latitude: float
longitude: float
address: Optional[str]
alert_type: str
message: Optional[str]
is_resolved: bool
resolved_at: Optional[datetime]
notified_users_count: int
responded_users_count: int
address: Optional[str] = None
alert_type: AlertType
message: Optional[str] = None
is_resolved: bool = False
resolved_at: Optional[datetime] = None
resolved_notes: Optional[str] = None
notified_users_count: int = 0
responded_users_count: int = 0
created_at: datetime
updated_at: Optional[datetime] = None
# User information
user_name: Optional[str] = None
user_phone: Optional[str] = None
class Config:
from_attributes = True
@@ -49,33 +75,92 @@ class EmergencyAlertResponse(BaseModel):
class EmergencyResponseCreate(BaseModel):
response_type: ResponseType
message: Optional[str] = Field(None, max_length=500)
eta_minutes: Optional[int] = Field(None, ge=0, le=240) # Max 4 hours
message: Optional[str] = Field(None, max_length=500, description="Response message")
eta_minutes: Optional[int] = Field(None, ge=0, le=240, description="Estimated time of arrival in minutes")
class EmergencyResponseResponse(BaseModel):
id: int
alert_id: int
responder_id: int
response_type: str
message: Optional[str]
eta_minutes: Optional[int]
response_type: ResponseType
message: Optional[str] = None
eta_minutes: Optional[int] = None
created_at: datetime
# Responder information
responder_name: Optional[str] = None
responder_phone: Optional[str] = None
class Config:
from_attributes = True
# Report schemas
class EmergencyReportCreate(BaseModel):
latitude: float = Field(..., ge=-90, le=90)
longitude: float = Field(..., ge=-180, le=180)
report_type: str = Field(..., description="Type of emergency report")
description: str = Field(..., min_length=10, max_length=1000)
address: Optional[str] = Field(None, max_length=500)
is_anonymous: bool = Field(default=False)
severity: int = Field(default=3, ge=1, le=5, description="Severity level 1-5")
class EmergencyReportResponse(BaseModel):
id: int
uuid: UUID
user_id: Optional[int] = None
latitude: float
longitude: float
address: Optional[str] = None
report_type: str
description: str
is_anonymous: bool
severity: int
status: str = "pending"
created_at: datetime
class Config:
from_attributes = True
class NearbyUsersResponse(BaseModel):
user_id: int
distance_meters: float
# Statistics schemas
class EmergencyStatistics(BaseModel):
total_alerts: int = 0
active_alerts: int = 0
resolved_alerts: int = 0
total_responders: int = 0
avg_response_time_minutes: float = 0
# Location-based schemas
class NearbyAlertResponse(BaseModel):
id: int
alert_type: str
latitude: float
longitude: float
address: Optional[str] = None
distance_km: float
created_at: datetime
responded_users_count: int = 0
class EmergencyStats(BaseModel):
total_alerts: int
active_alerts: int
resolved_alerts: int
avg_response_time_minutes: Optional[float]
total_responders: int
# Safety check schemas
class SafetyCheckCreate(BaseModel):
message: Optional[str] = Field(None, max_length=200)
location_latitude: Optional[float] = Field(None, ge=-90, le=90)
location_longitude: Optional[float] = Field(None, ge=-180, le=180)
class SafetyCheckResponse(BaseModel):
id: int
uuid: UUID
user_id: int
message: Optional[str] = None
location_latitude: Optional[float] = None
location_longitude: Optional[float] = None
created_at: datetime
class Config:
from_attributes = True