초기 커밋

This commit is contained in:
2026-03-01 07:44:19 +09:00
commit 09359f30be
146 changed files with 6120 additions and 0 deletions

0
app/services/__init__.py Normal file
View File

View 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
]

View 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)

View 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)

View 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,
)

View 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()

View 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,
)