manual-deploy 2026-06-07 00:13

This commit is contained in:
DESKTOP-TKLFCPR\ython 2026-06-07 00:13:38 +09:00
parent 20f6e23c4a
commit 5a4aa87b72
5 changed files with 619 additions and 0 deletions

View File

@ -69,6 +69,9 @@ from routers import (
sr_chat,
inventory,
system as system_router,
patches,
stats as stats_router,
cicd,
)
@ -529,6 +532,12 @@ app.include_router(search_router.router) # #50 통합 검색
app.include_router(sr_chat.router) # #98 SR 채팅 (REST + WebSocket)
app.include_router(system_router.router) # #77 시스템 정보/릴리즈 노트
app.include_router(approvals.changes_router) # #68 변경 달력 (/api/changes)
app.include_router(patches.router) # #82 #83 CVE 패치 현황
app.include_router(stats_router.router) # #93~#97 통계·보고
app.include_router(cicd.router) # #99 #100 CI/CD 빌드 상태
from routers import mobile2_ext
app.include_router(mobile2_ext.router) # #101~#200 2세대 보조 엔드포인트
# ── 개방망 보안 헤더 미들웨어 ────────────────────────────────────────────────

110
routers/cicd.py Normal file
View File

@ -0,0 +1,110 @@
"""
Jenkins CI/CD 상태 API (모바일 기능 #99, #100).
GET /api/cicd/builds 빌드 목록 (최근 20)
GET /api/cicd/builds/{id} 빌드 상세
POST /api/cicd/builds/trigger 빌드 트리거
GET /api/cicd/status 전체 파이프라인 상태
WS /ws/cicd-status 실시간 빌드 상태 스트림
"""
from __future__ import annotations
import asyncio
import json
import logging
import os
from datetime import datetime
from typing import Optional
from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect
from pydantic import BaseModel
from core.auth import get_current_user
from models import User
logger = logging.getLogger(__name__)
router = APIRouter(tags=["CI/CD"])
JENKINS_URL = os.getenv("JENKINS_URL", "http://localhost:8080")
_MOCK_BUILDS = [
{"id": 1, "project": "guardia-itsm", "branch": "main", "status": "SUCCESS", "started_at": "2026-06-06T10:00:00", "duration_sec": 125, "triggered_by": "admin"},
{"id": 2, "project": "guardia-messenger", "branch": "feature/100feat", "status": "SUCCESS", "started_at": "2026-06-06T09:30:00", "duration_sec": 340, "triggered_by": "ythong"},
{"id": 3, "project": "guardia-manager", "branch": "main", "status": "FAILURE", "started_at": "2026-06-06T08:00:00", "duration_sec": 60, "triggered_by": "admin"},
{"id": 4, "project": "zioinfo-web", "branch": "main", "status": "SUCCESS", "started_at": "2026-06-05T17:00:00", "duration_sec": 90, "triggered_by": "admin"},
{"id": 5, "project": "guardia-itsm", "branch": "develop", "status": "RUNNING", "started_at": "2026-06-06T10:30:00", "duration_sec": None, "triggered_by": "ythong"},
]
class TriggerIn(BaseModel):
project: str
branch: Optional[str] = "main"
@router.get("/api/cicd/builds")
async def list_builds(
limit: int = 20,
current_user: User = Depends(get_current_user),
):
return {"jenkins_url": JENKINS_URL, "items": _MOCK_BUILDS[:limit]}
@router.get("/api/cicd/builds/{build_id}")
async def get_build(
build_id: int,
current_user: User = Depends(get_current_user),
):
b = next((b for b in _MOCK_BUILDS if b["id"] == build_id), None)
if not b:
return {"error": "빌드를 찾을 수 없습니다."}
return b
@router.post("/api/cicd/builds/trigger", status_code=202)
async def trigger_build(
payload: TriggerIn,
current_user: User = Depends(get_current_user),
):
return {
"queued": True,
"project": payload.project,
"branch": payload.branch,
"triggered_by": current_user.username,
"message": f"{payload.project}@{payload.branch} 빌드가 대기열에 추가됐습니다.",
}
@router.get("/api/cicd/status")
async def pipeline_status(current_user: User = Depends(get_current_user)):
running = [b for b in _MOCK_BUILDS if b["status"] == "RUNNING"]
return {
"jenkins_connected": False,
"jenkins_url": JENKINS_URL,
"running_builds": len(running),
"latest_apk_url": "/api/cicd/apk/latest",
"apk_qr_data": "https://zioinfo.co.kr:8443/static/apk/guardia-latest.apk",
"builds": _MOCK_BUILDS[:5],
}
_cicd_clients: set[WebSocket] = set()
@router.websocket("/ws/cicd-status")
async def cicd_ws(websocket: WebSocket):
await websocket.accept()
_cicd_clients.add(websocket)
try:
while True:
await asyncio.sleep(10)
running = [b for b in _MOCK_BUILDS if b["status"] == "RUNNING"]
await websocket.send_text(json.dumps({
"type": "status",
"running": len(running),
"ts": datetime.now().isoformat(),
}))
except WebSocketDisconnect:
_cicd_clients.discard(websocket)
except Exception as e:
logger.warning("cicd ws error: %s", e)
_cicd_clients.discard(websocket)

209
routers/mobile2_ext.py Normal file
View File

@ -0,0 +1,209 @@
"""
GUARDiA Messenger 2세대(#101~#200) 전용 보조 엔드포인트.
기존 라우터에 없는 API를 파일로 통합 제공.
"""
from __future__ import annotations
import random
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
from sqlalchemy import func, select, text
from sqlalchemy.ext.asyncio import AsyncSession
from core.auth import get_current_user
from database import get_db
from models import SRRequest, User
router = APIRouter(prefix="/api/mobile2", tags=["Messenger 2세대"])
# ── 팀 리더보드 (#182) ──────────────────────────────────────────────────────
@router.get("/team-leaderboard")
async def team_leaderboard(db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
result = await db.execute(
select(User.username, User.full_name, func.count(SRRequest.id).label("cnt"))
.join(SRRequest, SRRequest.assignee_id == User.id, isouter=True)
.where(SRRequest.created_at >= datetime.utcnow() - timedelta(days=30))
.group_by(User.id, User.username, User.full_name)
.order_by(func.count(SRRequest.id).desc())
.limit(20)
)
rows = result.all()
return {"items": [{"rank": i+1, "username": r.username, "name": r.full_name or r.username, "count": r.cnt} for i, r in enumerate(rows)]}
# ── 시간대별 SR 패턴 히트맵 (#183) ─────────────────────────────────────────
@router.get("/hourly-pattern")
async def hourly_sr_pattern(db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
result = await db.execute(
text("""
SELECT EXTRACT(HOUR FROM created_at) AS h, EXTRACT(DOW FROM created_at) AS d, COUNT(*) AS cnt
FROM tb_sr_requests
WHERE created_at >= NOW() - INTERVAL '90 days'
GROUP BY h, d
""")
)
cells = [{"hour": int(r.h), "dow": int(r.d), "count": int(r.cnt)} for r in result.all()]
return {"cells": cells}
# ── 반복 장애 패턴 (#184) ───────────────────────────────────────────────────
@router.get("/incident-patterns")
async def incident_patterns(db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
result = await db.execute(
select(SRRequest.sr_type, SRRequest.priority, func.count().label("cnt"))
.where(SRRequest.created_at >= datetime.utcnow() - timedelta(days=90))
.group_by(SRRequest.sr_type, SRRequest.priority)
.order_by(func.count().desc())
.limit(10)
)
patterns = [{"type": r.sr_type, "priority": r.priority, "count": r.cnt} for r in result.all()]
return {"patterns": patterns}
# ── 만족도 트렌드 (#185) ────────────────────────────────────────────────────
@router.get("/satisfaction-trend")
async def satisfaction_trend(db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
weeks = []
for i in range(8, 0, -1):
start = datetime.utcnow() - timedelta(weeks=i)
end = datetime.utcnow() - timedelta(weeks=i-1)
weeks.append({
"week": start.strftime("%m/%d"),
"avg_score": round(random.uniform(3.5, 5.0), 1),
"count": random.randint(5, 30),
})
return {"trend": weeks}
# ── 원인별 분류 (#186) ──────────────────────────────────────────────────────
@router.get("/cause-breakdown")
async def cause_breakdown(db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
result = await db.execute(
select(SRRequest.sr_type, func.count().label("cnt"))
.where(SRRequest.created_at >= datetime.utcnow() - timedelta(days=30))
.group_by(SRRequest.sr_type)
.order_by(func.count().desc())
)
total = 0
items = []
for r in result.all():
items.append({"type": r.sr_type, "count": r.cnt})
total += r.cnt
for it in items:
it["pct"] = round(it["count"] / max(total, 1) * 100, 1)
return {"items": items, "total": total}
# ── 운영 성숙도 점수 (#187) ─────────────────────────────────────────────────
@router.get("/maturity-score")
async def maturity_score(_=Depends(get_current_user)):
return {
"overall": 72,
"dimensions": [
{"name": "자동화율", "score": 78, "max": 100},
{"name": "SLA 준수율", "score": 85, "max": 100},
{"name": "CSAP 준수율", "score": 65, "max": 100},
{"name": "보안 패치율", "score": 70, "max": 100},
{"name": "문서화율", "score": 60, "max": 100},
],
"month": datetime.utcnow().strftime("%Y-%m"),
}
# ── 비용 절감 효과 (#190) ────────────────────────────────────────────────────
@router.get("/savings-dashboard")
async def savings_dashboard(_=Depends(get_current_user)):
return {
"total_saved_krw": 12_500_000,
"items": [
{"category": "자동화로 절감한 인건비", "amount_krw": 5_000_000},
{"category": "클라우드 불필요 리소스 삭제", "amount_krw": 3_200_000},
{"category": "조기 패치로 인한 장애 예방", "amount_krw": 4_300_000},
],
"period": datetime.utcnow().strftime("%Y-%m"),
}
# ── AI 대화 이력 (#141) ──────────────────────────────────────────────────────
@router.get("/chatbot-history")
async def chatbot_history(
page: int = Query(0, ge=0),
size: int = Query(20, ge=1, le=100),
db: AsyncSession = Depends(get_db),
me: User = Depends(get_current_user),
):
from models import ChatMessage
try:
result = await db.execute(
select(ChatMessage)
.where(ChatMessage.user_id == me.id)
.order_by(ChatMessage.created_at.desc())
.offset(page * size).limit(size)
)
items = result.scalars().all()
return {"items": [{"id": m.id, "role": m.role, "content": m.content, "created_at": str(m.created_at)} for m in items]}
except Exception:
return {"items": []}
# ── 앱 버전 (#179) ────────────────────────────────────────────────────────────
@router.get("/app-version")
async def app_version(_=Depends(get_current_user)):
return {
"current": "1.2.0",
"latest": "1.2.0",
"update_required": False,
"changelog": "2세대 100개 기능 추가",
"download_url": None,
}
# ── 공지사항 (#127) ───────────────────────────────────────────────────────────
@router.get("/announcements")
async def announcements(_=Depends(get_current_user)):
return {"items": [
{"id": 1, "title": "GUARDiA 2.0 업데이트", "body": "2세대 100개 기능이 추가됐습니다.", "created_at": "2026-06-06T00:00:00", "pinned": True},
{"id": 2, "title": "정기 유지보수 안내", "body": "6월 15일 02:00~04:00 시스템 점검 예정", "created_at": "2026-06-05T09:00:00", "pinned": False},
]}
# ── 팀 온라인 상태 (#125) ─────────────────────────────────────────────────────
@router.get("/team-presence")
async def team_presence(db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
result = await db.execute(select(User.id, User.username, User.full_name).limit(20))
STATUSES = ["online", "away", "busy", "offline"]
users = result.all()
return {"members": [{"id": u.id, "name": u.full_name or u.username, "status": random.choice(STATUSES)} for u in users]}
# ── 인수인계 체크리스트 (#126) ────────────────────────────────────────────────
@router.get("/handover-checklist")
async def handover_checklist(_=Depends(get_current_user)):
return {"items": [
{"id": 1, "label": "미처리 SR 현황 공유", "done": False},
{"id": 2, "label": "진행 중 배포 상태 확인", "done": False},
{"id": 3, "label": "온콜 담당자 인계", "done": False},
{"id": 4, "label": "알림 규칙 설정 확인", "done": True},
{"id": 5, "label": "보안 이벤트 확인", "done": False},
{"id": 6, "label": "CSAP 미준수 항목 공유", "done": False},
]}
# ── 온콜 인계 인수 요약 (#126b) ───────────────────────────────────────────────
@router.get("/oncall-handover")
async def oncall_handover(db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
open_sr = await db.execute(
select(func.count()).select_from(SRRequest).where(SRRequest.status.in_(["RECEIVED", "IN_PROGRESS"]))
)
open_cnt = open_sr.scalar_one_or_none() or 0
return {
"open_sr_count": open_cnt,
"critical_sr_count": 0,
"active_deployments": 0,
"oncall_next": "홍길동",
"handover_note": "특이 사항 없음",
}

123
routers/patches.py Normal file
View File

@ -0,0 +1,123 @@
"""
CVE 패치 현황 API (모바일 기능 #82, #83).
GET /api/patches/cve CVE 목록 (severity 필터)
GET /api/patches/status 서버별 패치 적용률 (IP 노출 금지)
GET /api/patches/pending 미적용 패치 목록
GET /api/patches/pii-types PII 데이터 처리 유형 목록
POST /api/patches/{cve_id}/apply 패치 적용 SR 자동 생성
"""
from __future__ import annotations
import hashlib
from datetime import datetime, date
from typing import List, Optional
from fastapi import APIRouter, Depends
from pydantic import BaseModel, ConfigDict
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from core.auth import get_current_user
from database import get_db
from models import AuditLog, SRRequest, SRStatus, SRType, Priority, User
router = APIRouter(prefix="/api/patches", tags=["Patches"])
_MOCK_CVE = [
{"id": "CVE-2024-1001", "severity": "critical", "title": "OpenSSL 원격 코드 실행", "affected": "OpenSSL < 3.2.1", "cvss": 9.8, "patch_available": True, "patched_servers": 3, "total_servers": 10},
{"id": "CVE-2024-1002", "severity": "high", "title": "Apache httpd 디렉토리 탐색", "affected": "Apache < 2.4.59", "cvss": 7.5, "patch_available": True, "patched_servers": 8, "total_servers": 10},
{"id": "CVE-2024-1003", "severity": "high", "title": "Linux 커널 권한 상승", "affected": "kernel < 6.8.2", "cvss": 7.8, "patch_available": True, "patched_servers": 5, "total_servers": 10},
{"id": "CVE-2024-1004", "severity": "medium", "title": "SSH 취약 암호화 허용", "affected": "OpenSSH < 9.7", "cvss": 5.3, "patch_available": True, "patched_servers": 9, "total_servers": 10},
{"id": "CVE-2024-1005", "severity": "medium", "title": "Python urllib SSRF", "affected": "Python < 3.12.3", "cvss": 5.9, "patch_available": False, "patched_servers": 0, "total_servers": 10},
]
_MOCK_SERVERS = [
{"name": "WEB-01", "role": "웹서버", "patch_rate": 85, "pending": 2},
{"name": "WEB-02", "role": "웹서버", "patch_rate": 70, "pending": 4},
{"name": "APP-01", "role": "앱서버", "patch_rate": 95, "pending": 1},
{"name": "APP-02", "role": "앱서버", "patch_rate": 60, "pending": 5},
{"name": "DB-01", "role": "DB서버", "patch_rate": 100, "pending": 0},
]
_PII_TYPES = [
{"code": "PII_001", "name": "주민등록번호", "storage": "암호화 DB", "retention": "5년", "status": "compliant"},
{"code": "PII_002", "name": "연락처", "storage": "암호화 DB", "retention": "3년", "status": "compliant"},
{"code": "PII_003", "name": "이메일", "storage": "평문 로그", "retention": "미정", "status": "non_compliant"},
{"code": "PII_004", "name": "IP 주소", "storage": "감사 로그", "retention": "1년", "status": "compliant"},
]
class PatchApplyOut(BaseModel):
sr_id: int
message: str
@router.get("/cve")
async def list_cve(severity: Optional[str] = None):
data = _MOCK_CVE
if severity:
data = [c for c in data if c["severity"] == severity]
return {"total": len(data), "items": data}
@router.get("/status")
async def patch_status():
total_rate = round(sum(s["patch_rate"] for s in _MOCK_SERVERS) / len(_MOCK_SERVERS), 1)
return {"overall_patch_rate": total_rate, "servers": _MOCK_SERVERS}
@router.get("/pending")
async def pending_patches():
pending = [c for c in _MOCK_CVE if c["patched_servers"] < c["total_servers"]]
return {"total": len(pending), "items": pending}
@router.get("/pii-types")
async def pii_types():
return {"items": _PII_TYPES}
@router.post("/{cve_id}/apply", response_model=PatchApplyOut, status_code=201)
async def apply_patch(
cve_id: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""패치 적용 SR 자동 생성."""
cve = next((c for c in _MOCK_CVE if c["id"] == cve_id), None)
title = f"[패치] {cve['title'] if cve else cve_id} 적용"
sr = SRRequest(
sr_type=SRType.OTHER,
title=title,
description=f"CVE ID: {cve_id}\n적용 대상: {cve['affected'] if cve else '전체 서버'}",
status=SRStatus.RECEIVED,
priority=Priority.HIGH,
requested_by=current_user.username,
)
db.add(sr)
await db.flush()
prev = await db.execute(
select(AuditLog).order_by(AuditLog.id.desc()).limit(1)
)
prev_row = prev.scalar_one_or_none()
prev_hash = prev_row.log_hash if prev_row else "0" * 64
ts = datetime.now()
raw = f"{prev_hash}|{current_user.username}|PATCH_SR_CREATE|{title}|{ts.isoformat()}"
log_hash = hashlib.sha256(raw.encode()).hexdigest()
audit = AuditLog(
entity_type="sr_request",
entity_id=str(sr.sr_id),
actor=current_user.username,
action="PATCH_SR_CREATE",
detail=f"CVE {cve_id} 패치 적용 SR 생성",
log_hash=log_hash,
prev_hash=prev_hash,
created_at=ts,
)
db.add(audit)
await db.commit()
return PatchApplyOut(sr_id=sr.sr_id, message=f"SR #{sr.sr_id} 생성됨")

168
routers/stats.py Normal file
View File

@ -0,0 +1,168 @@
"""
통계·보고 API (모바일 기능 #93~#97).
GET /api/stats/my 나의 SR 처리 통계
GET /api/stats/institutions 기관별 SR 현황 비교
GET /api/stats/deploy-history 배포 이력 타임라인 (VibeSession)
GET /api/stats/kpi KPI 대시보드
GET /api/stats/export-pdf 리포트 JSON (앱에서 PDF 변환)
"""
from __future__ import annotations
from datetime import datetime, timedelta
from typing import Optional
from fastapi import APIRouter, Depends
from sqlalchemy import select, func, case
from sqlalchemy.ext.asyncio import AsyncSession
from core.auth import get_current_user
from database import get_db
from models import (
SRRequest, SRStatus, Institution, User, UserRole, VibeSession,
)
router = APIRouter(prefix="/api/stats", tags=["Statistics"])
def _this_month():
now = datetime.now()
return datetime(now.year, now.month, 1)
async def _inst_ids_for(user: User, db: AsyncSession):
if user.role != UserRole.CUSTOMER:
return None
rows = (await db.execute(
select(Institution.inst_id).where(Institution.inst_code == user.inst_code)
)).scalars().all()
return rows or [-1]
@router.get("/my")
async def my_stats(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
now = datetime.now()
this_m = datetime(now.year, now.month, 1)
last_m = datetime(now.year, now.month - 1, 1) if now.month > 1 else datetime(now.year - 1, 12, 1)
base = select(SRRequest).where(SRRequest.requested_by == current_user.username)
async def _count(q):
return (await db.execute(select(func.count()).select_from(q.subquery()))).scalar_one()
total = await _count(base)
this_done = await _count(base.where(SRRequest.status == SRStatus.COMPLETED, SRRequest.created_at >= this_m))
last_done = await _count(base.where(SRRequest.status == SRStatus.COMPLETED, SRRequest.created_at >= last_m, SRRequest.created_at < this_m))
this_all = await _count(base.where(SRRequest.created_at >= this_m))
last_all = await _count(base.where(SRRequest.created_at >= last_m, SRRequest.created_at < this_m))
return {
"total": total,
"this_month": {"created": this_all, "completed": this_done, "rate": round(this_done / this_all * 100, 1) if this_all else 0},
"last_month": {"created": last_all, "completed": last_done, "rate": round(last_done / last_all * 100, 1) if last_all else 0},
}
@router.get("/institutions")
async def institution_stats(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
q = (
select(
Institution.inst_id,
Institution.inst_name,
func.count(SRRequest.sr_id).label("total"),
func.sum(case((SRRequest.status == SRStatus.COMPLETED, 1), else_=0)).label("done"),
)
.outerjoin(SRRequest, SRRequest.inst_id == Institution.inst_id)
.group_by(Institution.inst_id, Institution.inst_name)
.order_by(func.count(SRRequest.sr_id).desc())
)
rows = (await db.execute(q)).all()
return {
"items": [
{
"inst_id": r.inst_id,
"inst_name": r.inst_name,
"total": r.total or 0,
"completed": r.done or 0,
"rate": round((r.done or 0) / r.total * 100, 1) if r.total else 0,
}
for r in rows
]
}
@router.get("/deploy-history")
async def deploy_history(
limit: int = 30,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
q = (
select(VibeSession)
.order_by(VibeSession.started_at.desc())
.limit(limit)
)
rows = (await db.execute(q)).scalars().all()
return {
"items": [
{
"id": r.id,
"project": r.project_name if hasattr(r, "project_name") else "N/A",
"status": r.status,
"started_at": r.started_at.isoformat() if r.started_at else None,
"deployed_at": r.deployed_at.isoformat() if r.deployed_at else None,
"duration_sec": int((r.deployed_at - r.started_at).total_seconds()) if r.deployed_at and r.started_at else None,
"deployed_by": r.requested_by if hasattr(r, "requested_by") else None,
}
for r in rows
]
}
@router.get("/kpi")
async def kpi_dashboard(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
now = datetime.now()
month_start = datetime(now.year, now.month, 1)
total_sr = (await db.execute(select(func.count(SRRequest.sr_id)).where(SRRequest.created_at >= month_start))).scalar_one()
done_sr = (await db.execute(select(func.count(SRRequest.sr_id)).where(SRRequest.created_at >= month_start, SRRequest.status == SRStatus.COMPLETED))).scalar_one()
breach = (await db.execute(select(func.count(SRRequest.sr_id)).where(SRRequest.created_at >= month_start, SRRequest.sla_breached == True))).scalar_one()
return {
"period": month_start.strftime("%Y-%m"),
"sr_completion_rate": round(done_sr / total_sr * 100, 1) if total_sr else 0,
"sla_compliance_rate": round((total_sr - breach) / total_sr * 100, 1) if total_sr else 100,
"total_sr": total_sr,
"completed_sr": done_sr,
"sla_breach": breach,
"csap_score": 82.5,
"targets": {
"sr_completion_rate": 90,
"sla_compliance_rate": 95,
"csap_score": 85,
},
}
@router.get("/export-pdf")
async def export_pdf_data(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
kpi = await kpi_dashboard(db=db, current_user=current_user)
my = await my_stats(db=db, current_user=current_user)
return {
"generated_at": datetime.now().isoformat(),
"generated_by": current_user.username,
"kpi": kpi,
"my_stats": my,
}