초기 커밋
This commit is contained in:
0
app/services/__init__.py
Normal file
0
app/services/__init__.py
Normal file
71
app/services/analytics_service.py
Normal file
71
app/services/analytics_service.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from app.processing.analyzers.device_analyzer import analyze_device_status
|
||||
from app.processing.analyzers.trend_analyzer import analyze_trend
|
||||
from app.processing.pipelines.report_pipeline import generate_device_report
|
||||
from app.processing.pipelines.telemetry_pipeline import aggregate_telemetry
|
||||
from app.processing.utils.dataframe_utils import to_records
|
||||
from app.repositories.analytics_repo import AnalyticsRepository
|
||||
from app.schemas.analytics import (
|
||||
AnalyticsResultRead,
|
||||
ReportResponse,
|
||||
TelemetryAggregateResponse,
|
||||
)
|
||||
|
||||
|
||||
class AnalyticsService:
|
||||
def __init__(self) -> None:
|
||||
self.analytics_repo = AnalyticsRepository()
|
||||
|
||||
async def get_telemetry_aggregate(
|
||||
self, device_id: str, start: datetime, end: datetime, interval: str = "1h"
|
||||
) -> TelemetryAggregateResponse:
|
||||
df = await aggregate_telemetry(device_id, start, end, interval)
|
||||
records = to_records(df) if len(df) > 0 else []
|
||||
return TelemetryAggregateResponse(
|
||||
device_id=device_id, records=records, count=len(records)
|
||||
)
|
||||
|
||||
async def generate_report(
|
||||
self, device_id: str, start: datetime, end: datetime
|
||||
) -> ReportResponse:
|
||||
result = await generate_device_report(device_id, start, end)
|
||||
return ReportResponse(
|
||||
report_id=str(result.id),
|
||||
device_id=device_id,
|
||||
status=result.result.get("status", {}),
|
||||
trends=result.result.get("trends", {}),
|
||||
created_at=result.created_at,
|
||||
)
|
||||
|
||||
async def get_device_status_analysis(
|
||||
self, device_id: str, start: datetime, end: datetime
|
||||
) -> dict:
|
||||
return await analyze_device_status(device_id, start, end)
|
||||
|
||||
async def get_trend_analysis(
|
||||
self, device_id: str, start: datetime, end: datetime
|
||||
) -> dict:
|
||||
return await analyze_trend(device_id, start, end)
|
||||
|
||||
async def list_results(
|
||||
self, analysis_type: str, device_id: str | None = None, skip: int = 0, limit: int = 20
|
||||
) -> list[AnalyticsResultRead]:
|
||||
results = await self.analytics_repo.get_by_type(
|
||||
analysis_type, device_id=device_id, skip=skip, limit=limit
|
||||
)
|
||||
return [
|
||||
AnalyticsResultRead(
|
||||
id=str(r.id),
|
||||
analysis_type=r.analysis_type,
|
||||
device_id=r.device_id,
|
||||
result=r.result,
|
||||
parameters=r.parameters,
|
||||
period_start=r.period_start,
|
||||
period_end=r.period_end,
|
||||
created_at=r.created_at,
|
||||
)
|
||||
for r in results
|
||||
]
|
||||
84
app/services/auth_service.py
Normal file
84
app/services/auth_service.py
Normal file
@@ -0,0 +1,84 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.exceptions import ConflictException, UnauthorizedException
|
||||
from app.core.security import (
|
||||
create_access_token,
|
||||
create_refresh_token,
|
||||
decode_token,
|
||||
hash_password,
|
||||
verify_password,
|
||||
)
|
||||
from app.models.mariadb.auth import RefreshToken
|
||||
from app.models.mariadb.user import User
|
||||
from app.repositories.auth_repo import AuthRepository
|
||||
from app.repositories.user_repo import UserRepository
|
||||
from app.schemas.auth import TokenResponse
|
||||
|
||||
|
||||
class AuthService:
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.user_repo = UserRepository(session)
|
||||
self.auth_repo = AuthRepository(session)
|
||||
|
||||
async def register(
|
||||
self, email: str, password: str, full_name: str = ""
|
||||
) -> User:
|
||||
existing = await self.user_repo.get_by_email(email)
|
||||
if existing:
|
||||
raise ConflictException("Email already registered")
|
||||
|
||||
user = User(
|
||||
email=email,
|
||||
hashed_password=hash_password(password),
|
||||
)
|
||||
return await self.user_repo.create_with_profile(user, full_name=full_name)
|
||||
|
||||
async def login(self, email: str, password: str) -> TokenResponse:
|
||||
user = await self.user_repo.get_by_email(email)
|
||||
if not user or not verify_password(password, user.hashed_password):
|
||||
raise UnauthorizedException("Invalid email or password")
|
||||
if not user.is_active:
|
||||
raise UnauthorizedException("Account is deactivated")
|
||||
|
||||
user.last_login_at = datetime.utcnow()
|
||||
|
||||
return await self._create_tokens(user)
|
||||
|
||||
async def refresh(self, refresh_token_str: str) -> TokenResponse:
|
||||
payload = decode_token(refresh_token_str)
|
||||
if not payload or payload.get("type") != "refresh":
|
||||
raise UnauthorizedException("Invalid refresh token")
|
||||
|
||||
stored = await self.auth_repo.get_by_token(refresh_token_str)
|
||||
if not stored:
|
||||
raise UnauthorizedException("Refresh token not found or expired")
|
||||
|
||||
stored.is_revoked = True
|
||||
|
||||
user = await self.user_repo.get_by_id(stored.user_id)
|
||||
if not user or not user.is_active:
|
||||
raise UnauthorizedException("User not found or deactivated")
|
||||
|
||||
return await self._create_tokens(user)
|
||||
|
||||
async def logout(self, user_id: int) -> None:
|
||||
await self.auth_repo.revoke_all_for_user(user_id)
|
||||
|
||||
async def _create_tokens(self, user: User) -> TokenResponse:
|
||||
access = create_access_token(user.id, user.role) # type: ignore[arg-type]
|
||||
refresh = create_refresh_token(user.id) # type: ignore[arg-type]
|
||||
|
||||
token_obj = RefreshToken(
|
||||
user_id=user.id, # type: ignore[arg-type]
|
||||
token=refresh,
|
||||
expires_at=datetime.utcnow()
|
||||
+ timedelta(days=settings.JWT_REFRESH_TOKEN_EXPIRE_DAYS),
|
||||
)
|
||||
await self.auth_repo.create(token_obj)
|
||||
|
||||
return TokenResponse(access_token=access, refresh_token=refresh)
|
||||
71
app/services/device_service.py
Normal file
71
app/services/device_service.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.exceptions import ConflictException, NotFoundException
|
||||
from app.models.mariadb.device import Device
|
||||
from app.repositories.device_repo import DeviceRepository
|
||||
from app.schemas.device import DeviceCreate, DeviceRead, DeviceUpdate
|
||||
|
||||
|
||||
class DeviceService:
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.device_repo = DeviceRepository(session)
|
||||
|
||||
async def get_device(self, device_id: int) -> DeviceRead:
|
||||
device = await self.device_repo.get_by_id(device_id)
|
||||
if not device or device.is_deleted:
|
||||
raise NotFoundException("Device not found")
|
||||
return DeviceRead.model_validate(device)
|
||||
|
||||
async def get_device_by_uid(self, device_uid: str) -> DeviceRead:
|
||||
device = await self.device_repo.get_by_uid(device_uid)
|
||||
if not device:
|
||||
raise NotFoundException("Device not found")
|
||||
return DeviceRead.model_validate(device)
|
||||
|
||||
async def list_devices(self, skip: int = 0, limit: int = 20) -> list[DeviceRead]:
|
||||
devices = await self.device_repo.get_all(
|
||||
skip=skip, limit=limit, filters={"is_deleted": False}
|
||||
)
|
||||
return [DeviceRead.model_validate(d) for d in devices]
|
||||
|
||||
async def count_devices(self) -> int:
|
||||
return await self.device_repo.count(filters={"is_deleted": False})
|
||||
|
||||
async def create_device(self, data: DeviceCreate) -> DeviceRead:
|
||||
existing = await self.device_repo.get_by_uid(data.device_uid)
|
||||
if existing:
|
||||
raise ConflictException("Device UID already registered")
|
||||
|
||||
device = Device(**data.model_dump())
|
||||
device = await self.device_repo.create(device)
|
||||
return DeviceRead.model_validate(device)
|
||||
|
||||
async def update_device(self, device_id: int, data: DeviceUpdate) -> DeviceRead:
|
||||
device = await self.device_repo.get_by_id(device_id)
|
||||
if not device or device.is_deleted:
|
||||
raise NotFoundException("Device not found")
|
||||
|
||||
update_data = data.model_dump(exclude_none=True)
|
||||
device = await self.device_repo.update(device, update_data)
|
||||
return DeviceRead.model_validate(device)
|
||||
|
||||
async def delete_device(self, device_id: int) -> None:
|
||||
device = await self.device_repo.get_by_id(device_id)
|
||||
if not device or device.is_deleted:
|
||||
raise NotFoundException("Device not found")
|
||||
await self.device_repo.update(
|
||||
device, {"is_deleted": True, "deleted_at": datetime.utcnow()}
|
||||
)
|
||||
|
||||
async def update_device_status(self, device_uid: str, status: str) -> DeviceRead:
|
||||
device = await self.device_repo.get_by_uid(device_uid)
|
||||
if not device:
|
||||
raise NotFoundException("Device not found")
|
||||
device = await self.device_repo.update(
|
||||
device, {"status": status, "last_seen_at": datetime.utcnow()}
|
||||
)
|
||||
return DeviceRead.model_validate(device)
|
||||
57
app/services/monitoring_service.py
Normal file
57
app/services/monitoring_service.py
Normal file
@@ -0,0 +1,57 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.constants import DeviceStatus
|
||||
from app.core.exceptions import NotFoundException
|
||||
from app.models.mariadb.monitoring import Alert, AlertRule
|
||||
from app.repositories.device_repo import DeviceRepository
|
||||
from app.repositories.monitoring_repo import AlertRepository, AlertRuleRepository
|
||||
from app.schemas.monitoring import AlertRead, AlertRuleCreate, AlertRuleRead, SystemHealthResponse
|
||||
|
||||
|
||||
class MonitoringService:
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.alert_rule_repo = AlertRuleRepository(session)
|
||||
self.alert_repo = AlertRepository(session)
|
||||
self.device_repo = DeviceRepository(session)
|
||||
|
||||
async def create_alert_rule(self, data: AlertRuleCreate, user_id: int) -> AlertRuleRead:
|
||||
rule = AlertRule(**data.model_dump(), created_by=user_id)
|
||||
rule = await self.alert_rule_repo.create(rule)
|
||||
return AlertRuleRead.model_validate(rule)
|
||||
|
||||
async def list_alert_rules(self) -> list[AlertRuleRead]:
|
||||
rules = await self.alert_rule_repo.get_all()
|
||||
return [AlertRuleRead.model_validate(r) for r in rules]
|
||||
|
||||
async def list_active_alerts(self, skip: int = 0, limit: int = 50) -> list[AlertRead]:
|
||||
alerts = await self.alert_repo.get_unacknowledged(skip=skip, limit=limit)
|
||||
return [AlertRead.model_validate(a) for a in alerts]
|
||||
|
||||
async def acknowledge_alert(self, alert_id: int, user_id: int) -> AlertRead:
|
||||
alert = await self.alert_repo.get_by_id(alert_id)
|
||||
if not alert:
|
||||
raise NotFoundException("Alert not found")
|
||||
alert = await self.alert_repo.update(alert, {
|
||||
"is_acknowledged": True,
|
||||
"acknowledged_by": user_id,
|
||||
"acknowledged_at": datetime.utcnow(),
|
||||
})
|
||||
return AlertRead.model_validate(alert)
|
||||
|
||||
async def get_system_health(self) -> SystemHealthResponse:
|
||||
active_devices = await self.device_repo.count(filters={"status": DeviceStatus.ONLINE})
|
||||
active_alerts = await self.alert_repo.count_active()
|
||||
|
||||
return SystemHealthResponse(
|
||||
status="ok",
|
||||
mariadb="connected",
|
||||
mongodb="connected",
|
||||
redis="connected",
|
||||
mqtt="connected",
|
||||
active_devices=active_devices,
|
||||
active_alerts=active_alerts,
|
||||
)
|
||||
49
app/services/notification_service.py
Normal file
49
app/services/notification_service.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from app.communication.socketio.server import sio
|
||||
from app.models.mongodb.notification import Notification
|
||||
|
||||
|
||||
class NotificationService:
|
||||
async def create_notification(
|
||||
self, user_id: int, title: str, message: str, notification_type: str = "info"
|
||||
) -> Notification:
|
||||
notification = Notification(
|
||||
user_id=user_id,
|
||||
title=title,
|
||||
message=message,
|
||||
notification_type=notification_type,
|
||||
)
|
||||
await notification.insert()
|
||||
|
||||
await sio.emit(
|
||||
"notification",
|
||||
{"title": title, "message": message, "type": notification_type},
|
||||
room=f"user:{user_id}",
|
||||
namespace="/notification",
|
||||
)
|
||||
|
||||
return notification
|
||||
|
||||
async def get_user_notifications(
|
||||
self, user_id: int, skip: int = 0, limit: int = 20, unread_only: bool = False
|
||||
) -> list[Notification]:
|
||||
query: dict = {"user_id": user_id}
|
||||
if unread_only:
|
||||
query["is_read"] = False
|
||||
return await (
|
||||
Notification.find(query)
|
||||
.sort("-created_at")
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.to_list()
|
||||
)
|
||||
|
||||
async def mark_as_read(self, notification_id: str, user_id: int) -> None:
|
||||
from datetime import datetime
|
||||
|
||||
notification = await Notification.get(notification_id)
|
||||
if notification and notification.user_id == user_id:
|
||||
notification.is_read = True
|
||||
notification.read_at = datetime.utcnow()
|
||||
await notification.save()
|
||||
94
app/services/user_service.py
Normal file
94
app/services/user_service.py
Normal file
@@ -0,0 +1,94 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.exceptions import ConflictException, NotFoundException
|
||||
from app.core.security import hash_password
|
||||
from app.models.mariadb.user import User
|
||||
from app.repositories.user_repo import UserRepository
|
||||
from app.schemas.user import UserCreate, UserRead, UserUpdate
|
||||
|
||||
|
||||
class UserService:
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.user_repo = UserRepository(session)
|
||||
|
||||
async def get_user(self, user_id: int) -> UserRead:
|
||||
user = await self.user_repo.get_with_profile(user_id)
|
||||
if not user:
|
||||
raise NotFoundException("User not found")
|
||||
return self._to_read(user)
|
||||
|
||||
async def list_users(self, skip: int = 0, limit: int = 20) -> list[UserRead]:
|
||||
users = await self.user_repo.get_all(skip=skip, limit=limit)
|
||||
return [self._to_read(u) for u in users]
|
||||
|
||||
async def count_users(self) -> int:
|
||||
return await self.user_repo.count()
|
||||
|
||||
async def create_user(self, data: UserCreate) -> UserRead:
|
||||
existing = await self.user_repo.get_by_email(data.email)
|
||||
if existing:
|
||||
raise ConflictException("Email already registered")
|
||||
|
||||
user = User(
|
||||
email=data.email,
|
||||
hashed_password=hash_password(data.password),
|
||||
role=data.role,
|
||||
)
|
||||
user = await self.user_repo.create_with_profile(
|
||||
user,
|
||||
full_name=data.full_name,
|
||||
phone=data.phone,
|
||||
organization=data.organization,
|
||||
)
|
||||
return self._to_read(user)
|
||||
|
||||
async def update_user(self, user_id: int, data: UserUpdate) -> UserRead:
|
||||
user = await self.user_repo.get_with_profile(user_id)
|
||||
if not user:
|
||||
raise NotFoundException("User not found")
|
||||
|
||||
user_fields = {}
|
||||
profile_fields = {}
|
||||
|
||||
if data.is_active is not None:
|
||||
user_fields["is_active"] = data.is_active
|
||||
if data.role is not None:
|
||||
user_fields["role"] = data.role
|
||||
|
||||
for field in ("full_name", "phone", "organization", "avatar_url"):
|
||||
val = getattr(data, field, None)
|
||||
if val is not None:
|
||||
profile_fields[field] = val
|
||||
|
||||
if user_fields:
|
||||
await self.user_repo.update(user, user_fields)
|
||||
if profile_fields and user.profile:
|
||||
for k, v in profile_fields.items():
|
||||
setattr(user.profile, k, v)
|
||||
|
||||
user = await self.user_repo.get_with_profile(user_id)
|
||||
return self._to_read(user) # type: ignore[arg-type]
|
||||
|
||||
async def delete_user(self, user_id: int) -> None:
|
||||
user = await self.user_repo.get_by_id(user_id)
|
||||
if not user:
|
||||
raise NotFoundException("User not found")
|
||||
await self.user_repo.update(user, {"is_deleted": True})
|
||||
|
||||
@staticmethod
|
||||
def _to_read(user: User) -> UserRead:
|
||||
profile = user.profile
|
||||
return UserRead(
|
||||
id=user.id, # type: ignore[arg-type]
|
||||
email=user.email,
|
||||
role=user.role,
|
||||
is_active=user.is_active,
|
||||
is_verified=user.is_verified,
|
||||
full_name=profile.full_name if profile else "",
|
||||
phone=profile.phone if profile else "",
|
||||
organization=profile.organization if profile else "",
|
||||
avatar_url=profile.avatar_url if profile else "",
|
||||
created_at=user.created_at,
|
||||
)
|
||||
Reference in New Issue
Block a user