초기 커밋

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/tasks/__init__.py Normal file
View File

View File

@@ -0,0 +1,56 @@
from __future__ import annotations
import structlog
from app.tasks.celery_app import celery_app
logger = structlog.get_logger("tasks.analytics")
@celery_app.task(name="app.tasks.analytics_tasks.run_daily_analytics")
def run_daily_analytics() -> dict:
"""Run daily aggregation analytics on telemetry data."""
import asyncio
from datetime import datetime, timedelta
from app.models.mongodb.analytics_result import AnalyticsResult
from app.models.mongodb.telemetry import TelemetryData
async def _run() -> dict:
yesterday = datetime.utcnow().replace(hour=0, minute=0, second=0) - timedelta(days=1)
today = yesterday + timedelta(days=1)
pipeline = [
{"$match": {"timestamp": {"$gte": yesterday, "$lt": today}}},
{"$group": {
"_id": "$device_id",
"count": {"$sum": 1},
"avg_metrics": {"$avg": "$metrics.value"},
}},
]
collection = TelemetryData.get_motor_collection()
results = await collection.aggregate(pipeline).to_list(length=1000)
for r in results:
await AnalyticsResult(
analysis_type="daily_telemetry",
device_id=r["_id"],
parameters={"date": yesterday.isoformat()},
result={"count": r["count"], "avg_value": r.get("avg_metrics")},
period_start=yesterday,
period_end=today,
).insert()
return {"devices_analyzed": len(results)}
result = asyncio.get_event_loop().run_until_complete(_run())
logger.info("daily_analytics_done", **result)
return result
@celery_app.task(name="app.tasks.analytics_tasks.run_device_analysis")
def run_device_analysis(device_id: str, analysis_type: str, params: dict) -> dict:
"""Run on-demand analysis for a specific device."""
logger.info("device_analysis_started", device_id=device_id, type=analysis_type)
return {"status": "completed", "device_id": device_id, "type": analysis_type}

47
app/tasks/auth_tasks.py Normal file
View File

@@ -0,0 +1,47 @@
from __future__ import annotations
import structlog
from app.tasks.celery_app import celery_app
logger = structlog.get_logger("tasks.auth")
@celery_app.task(name="app.tasks.auth_tasks.cleanup_expired_tokens")
def cleanup_expired_tokens() -> dict:
"""Remove expired and revoked refresh tokens from the database."""
import asyncio
from sqlalchemy import delete
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from datetime import datetime
from app.core.config import settings
from app.models.mariadb.auth import RefreshToken
async def _cleanup() -> int:
engine = create_async_engine(settings.MARIADB_DSN)
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async with async_session() as session:
stmt = delete(RefreshToken).where(
(RefreshToken.expires_at < datetime.utcnow()) | (RefreshToken.is_revoked == True) # noqa: E712
)
result = await session.execute(stmt)
await session.commit()
count = result.rowcount # type: ignore[assignment]
await engine.dispose()
return count
count = asyncio.get_event_loop().run_until_complete(_cleanup())
logger.info("tokens_cleaned", count=count)
return {"cleaned": count}
@celery_app.task(name="app.tasks.auth_tasks.send_verification_email")
def send_verification_email(user_id: int, email: str, token: str) -> dict:
"""Send email verification link to user."""
logger.info("verification_email_sent", user_id=user_id, email=email)
return {"status": "sent", "email": email}

49
app/tasks/celery_app.py Normal file
View File

@@ -0,0 +1,49 @@
from __future__ import annotations
from celery import Celery
from celery.schedules import crontab
from app.core.config import settings
celery_app = Celery(
"core_api",
broker=settings.CELERY_BROKER_URL,
backend=settings.CELERY_RESULT_BACKEND,
)
celery_app.conf.update(
task_serializer="json",
accept_content=["json"],
result_serializer="json",
timezone="Asia/Seoul",
enable_utc=True,
task_track_started=True,
task_routes={
"app.tasks.analytics_tasks.*": {"queue": "analytics"},
"app.tasks.notification_tasks.*": {"queue": "notifications"},
"app.tasks.device_tasks.*": {"queue": "devices"},
"app.tasks.auth_tasks.*": {"queue": "default"},
},
beat_schedule={
"cleanup-expired-tokens": {
"task": "app.tasks.auth_tasks.cleanup_expired_tokens",
"schedule": crontab(hour=3, minute=0),
},
"check-device-health": {
"task": "app.tasks.device_tasks.check_device_health",
"schedule": crontab(minute="*/5"),
},
"daily-analytics": {
"task": "app.tasks.analytics_tasks.run_daily_analytics",
"schedule": crontab(hour=1, minute=0),
},
},
)
celery_app.autodiscover_tasks([
"app.tasks.auth_tasks",
"app.tasks.device_tasks",
"app.tasks.notification_tasks",
"app.tasks.analytics_tasks",
"app.tasks.scheduled",
])

64
app/tasks/device_tasks.py Normal file
View File

@@ -0,0 +1,64 @@
from __future__ import annotations
import structlog
from app.tasks.celery_app import celery_app
logger = structlog.get_logger("tasks.device")
@celery_app.task(name="app.tasks.device_tasks.check_device_health")
def check_device_health() -> dict:
"""Check all devices for heartbeat timeout and mark offline."""
import asyncio
from datetime import datetime, timedelta
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker
from app.core.config import settings
from app.core.constants import DeviceStatus
from app.models.mariadb.device import Device
async def _check() -> int:
engine = create_async_engine(settings.MARIADB_DSN)
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
threshold = datetime.utcnow() - timedelta(minutes=10)
async with async_session() as session:
stmt = (
update(Device)
.where(
Device.status == DeviceStatus.ONLINE,
Device.last_seen_at < threshold,
Device.is_deleted == False, # noqa: E712
)
.values(status=DeviceStatus.OFFLINE)
)
result = await session.execute(stmt)
await session.commit()
count = result.rowcount # type: ignore[assignment]
await engine.dispose()
return count
count = asyncio.get_event_loop().run_until_complete(_check())
logger.info("device_health_check", offline_count=count)
return {"marked_offline": count}
@celery_app.task(name="app.tasks.device_tasks.batch_firmware_update")
def batch_firmware_update(device_uids: list[str], firmware_url: str) -> dict:
"""Trigger OTA firmware update for a batch of devices."""
import asyncio
from app.communication.mqtt.publisher import publish_ota
async def _update() -> int:
for uid in device_uids:
await publish_ota(uid, {"url": firmware_url, "action": "update"})
return len(device_uids)
count = asyncio.get_event_loop().run_until_complete(_update())
logger.info("batch_firmware_update", count=count)
return {"updated": count}

View File

@@ -0,0 +1,47 @@
from __future__ import annotations
import structlog
from app.tasks.celery_app import celery_app
logger = structlog.get_logger("tasks.notification")
@celery_app.task(name="app.tasks.notification_tasks.send_push_notification")
def send_push_notification(user_id: int, title: str, message: str) -> dict:
"""Send push notification to a user via Socket.IO."""
import asyncio
from app.communication.socketio.server import sio
async def _send() -> None:
await sio.emit(
"notification",
{"title": title, "message": message},
room=f"user:{user_id}",
namespace="/notification",
)
asyncio.get_event_loop().run_until_complete(_send())
logger.info("push_sent", user_id=user_id)
return {"status": "sent", "user_id": user_id}
@celery_app.task(name="app.tasks.notification_tasks.send_bulk_notification")
def send_bulk_notification(user_ids: list[int], title: str, message: str) -> dict:
"""Send notification to multiple users and store in MongoDB."""
import asyncio
from app.models.mongodb.notification import Notification
async def _bulk() -> int:
notifications = [
Notification(user_id=uid, title=title, message=message)
for uid in user_ids
]
await Notification.insert_many(notifications)
return len(notifications)
count = asyncio.get_event_loop().run_until_complete(_bulk())
logger.info("bulk_notification_sent", count=count)
return {"sent": count}

14
app/tasks/scheduled.py Normal file
View File

@@ -0,0 +1,14 @@
from __future__ import annotations
import structlog
from app.tasks.celery_app import celery_app
logger = structlog.get_logger("tasks.scheduled")
@celery_app.task(name="app.tasks.scheduled.system_health_report")
def system_health_report() -> dict:
"""Generate periodic system health report."""
logger.info("system_health_report_generated")
return {"status": "ok"}