This commit is contained in:
@@ -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)
|
||||
@@ -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}>"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user