Initial commit: FastAPI middleware server for solar power monitoring
This commit is contained in:
commit
8878303cac
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Virtual Environment
|
||||
venv/
|
||||
.venv/
|
||||
env/
|
||||
ENV/
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Distribution / Build
|
||||
dist/
|
||||
build/
|
||||
*.egg-info/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
1
app/__init__.py
Normal file
1
app/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
# Solar Power Monitoring API Server
|
||||
1
app/core/__init__.py
Normal file
1
app/core/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
# Core configuration module
|
||||
33
app/core/config.py
Normal file
33
app/core/config.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
"""
|
||||
환경변수 설정 모듈
|
||||
- python-dotenv를 사용하여 .env 파일에서 환경변수 로드
|
||||
"""
|
||||
|
||||
from pydantic_settings import BaseSettings
|
||||
from functools import lru_cache
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""애플리케이션 설정"""
|
||||
|
||||
# Supabase 설정
|
||||
SUPABASE_URL: str
|
||||
SUPABASE_KEY: str
|
||||
|
||||
# 앱 설정
|
||||
APP_NAME: str = "Solar Power Monitoring API"
|
||||
APP_VERSION: str = "1.0.0"
|
||||
DEBUG: bool = False
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
env_file_encoding = "utf-8"
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def get_settings() -> Settings:
|
||||
"""
|
||||
설정 싱글톤 인스턴스 반환
|
||||
lru_cache를 사용하여 한 번만 로드
|
||||
"""
|
||||
return Settings()
|
||||
28
app/core/database.py
Normal file
28
app/core/database.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
"""
|
||||
Supabase 데이터베이스 클라이언트 모듈
|
||||
- 싱글톤 패턴으로 클라이언트 인스턴스 관리
|
||||
"""
|
||||
|
||||
from supabase import create_client, Client
|
||||
from functools import lru_cache
|
||||
from .config import get_settings
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def get_supabase_client() -> Client:
|
||||
"""
|
||||
Supabase 클라이언트 싱글톤 인스턴스 반환
|
||||
lru_cache를 사용하여 한 번만 생성
|
||||
"""
|
||||
settings = get_settings()
|
||||
return create_client(
|
||||
settings.SUPABASE_URL,
|
||||
settings.SUPABASE_KEY
|
||||
)
|
||||
|
||||
|
||||
def get_db() -> Client:
|
||||
"""
|
||||
Supabase 클라이언트 반환 (의존성 주입용)
|
||||
"""
|
||||
return get_supabase_client()
|
||||
75
app/main.py
Normal file
75
app/main.py
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
"""
|
||||
FastAPI 애플리케이션 진입점
|
||||
- 태양광 발전 관제 시스템 미들웨어 서버
|
||||
"""
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from app.core.config import get_settings
|
||||
from app.routers import plants
|
||||
|
||||
# 설정 로드
|
||||
settings = get_settings()
|
||||
|
||||
# FastAPI 앱 생성
|
||||
app = FastAPI(
|
||||
title=settings.APP_NAME,
|
||||
version=settings.APP_VERSION,
|
||||
description="태양광 발전 관제 시스템을 위한 미들웨어 API 서버",
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc"
|
||||
)
|
||||
|
||||
# CORS 미들웨어 설정 (모든 도메인 허용 - 개발용)
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# 라우터 등록
|
||||
app.include_router(plants.router)
|
||||
|
||||
|
||||
@app.get("/", tags=["Health"])
|
||||
async def health_check() -> dict:
|
||||
"""
|
||||
서버 상태 확인 (Health Check)
|
||||
|
||||
Returns:
|
||||
서버 상태 및 버전 정보
|
||||
"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"app_name": settings.APP_NAME,
|
||||
"version": settings.APP_VERSION
|
||||
}
|
||||
|
||||
|
||||
@app.get("/health", tags=["Health"])
|
||||
async def detailed_health_check() -> dict:
|
||||
"""
|
||||
상세 서버 상태 확인
|
||||
|
||||
Returns:
|
||||
서버 상태 및 연결 정보
|
||||
"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"app_name": settings.APP_NAME,
|
||||
"version": settings.APP_VERSION,
|
||||
"supabase_connected": bool(settings.SUPABASE_URL)
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(
|
||||
"app.main:app",
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
reload=settings.DEBUG
|
||||
)
|
||||
1
app/routers/__init__.py
Normal file
1
app/routers/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
# API Routers module
|
||||
18
app/routers/auth.py
Normal file
18
app/routers/auth.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
"""
|
||||
인증 관련 API 엔드포인트 (추후 예정)
|
||||
- 로그인/로그아웃
|
||||
- 토큰 관리
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/auth",
|
||||
tags=["Authentication"]
|
||||
)
|
||||
|
||||
|
||||
# TODO: 추후 구현 예정
|
||||
# - POST /auth/login
|
||||
# - POST /auth/logout
|
||||
# - POST /auth/refresh
|
||||
140
app/routers/plants.py
Normal file
140
app/routers/plants.py
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
"""
|
||||
발전소 관련 API 엔드포인트
|
||||
- 발전소 목록 조회
|
||||
- 발전 현황 조회
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from supabase import Client
|
||||
from typing import List
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.schemas.plant import PlantsListResponse, PlantWithLatestLog, SolarLogBase
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/plants",
|
||||
tags=["Plants"]
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{company_id}", response_model=PlantsListResponse)
|
||||
async def get_plants_by_company(
|
||||
company_id: int,
|
||||
db: Client = Depends(get_db)
|
||||
) -> PlantsListResponse:
|
||||
"""
|
||||
특정 업체의 모든 발전소 목록과 최신 발전 현황을 조회합니다.
|
||||
|
||||
Args:
|
||||
company_id: 업체 ID
|
||||
|
||||
Returns:
|
||||
발전소 목록 및 각 발전소의 최신 발전 로그
|
||||
"""
|
||||
try:
|
||||
# 1. 해당 업체의 모든 발전소 조회
|
||||
plants_response = db.table("plants") \
|
||||
.select("*") \
|
||||
.eq("company_id", company_id) \
|
||||
.execute()
|
||||
|
||||
plants = plants_response.data
|
||||
|
||||
if not plants:
|
||||
return PlantsListResponse(
|
||||
status="success",
|
||||
data=[],
|
||||
total_count=0
|
||||
)
|
||||
|
||||
# 2. 각 발전소의 최신 발전 로그 조회
|
||||
result: List[PlantWithLatestLog] = []
|
||||
|
||||
for plant in plants:
|
||||
# 해당 발전소의 최신 로그 1건 조회
|
||||
log_response = db.table("solar_logs") \
|
||||
.select("*") \
|
||||
.eq("plant_id", plant["id"]) \
|
||||
.order("timestamp", desc=True) \
|
||||
.limit(1) \
|
||||
.execute()
|
||||
|
||||
latest_log = None
|
||||
if log_response.data:
|
||||
latest_log = SolarLogBase(**log_response.data[0])
|
||||
|
||||
plant_with_log = PlantWithLatestLog(
|
||||
**plant,
|
||||
latest_log=latest_log
|
||||
)
|
||||
result.append(plant_with_log)
|
||||
|
||||
return PlantsListResponse(
|
||||
status="success",
|
||||
data=result,
|
||||
total_count=len(result)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"데이터베이스 조회 중 오류가 발생했습니다: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{company_id}/{plant_id}", response_model=dict)
|
||||
async def get_plant_detail(
|
||||
company_id: int,
|
||||
plant_id: int,
|
||||
db: Client = Depends(get_db)
|
||||
) -> dict:
|
||||
"""
|
||||
특정 발전소의 상세 정보를 조회합니다.
|
||||
|
||||
Args:
|
||||
company_id: 업체 ID
|
||||
plant_id: 발전소 ID
|
||||
|
||||
Returns:
|
||||
발전소 상세 정보 및 최근 발전 로그
|
||||
"""
|
||||
try:
|
||||
# 발전소 조회
|
||||
plant_response = db.table("plants") \
|
||||
.select("*") \
|
||||
.eq("id", plant_id) \
|
||||
.eq("company_id", company_id) \
|
||||
.single() \
|
||||
.execute()
|
||||
|
||||
if not plant_response.data:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="발전소를 찾을 수 없습니다."
|
||||
)
|
||||
|
||||
plant = plant_response.data
|
||||
|
||||
# 최근 발전 로그 10건 조회
|
||||
logs_response = db.table("solar_logs") \
|
||||
.select("*") \
|
||||
.eq("plant_id", plant_id) \
|
||||
.order("timestamp", desc=True) \
|
||||
.limit(10) \
|
||||
.execute()
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"plant": plant,
|
||||
"recent_logs": logs_response.data or []
|
||||
}
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"데이터베이스 조회 중 오류가 발생했습니다: {str(e)}"
|
||||
)
|
||||
2
app/schemas/__init__.py
Normal file
2
app/schemas/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# Pydantic Schemas module
|
||||
from .plant import PlantBase, PlantResponse, PlantWithLatestLog, PlantsListResponse
|
||||
46
app/schemas/plant.py
Normal file
46
app/schemas/plant.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
"""
|
||||
발전소 관련 Pydantic 스키마
|
||||
- 요청/응답 데이터 검증
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class PlantBase(BaseModel):
|
||||
"""발전소 기본 정보 스키마"""
|
||||
id: int
|
||||
company_id: int
|
||||
name: str
|
||||
capacity: Optional[float] = Field(None, description="발전 용량 (kW)")
|
||||
location: Optional[str] = Field(None, description="발전소 위치")
|
||||
created_at: Optional[datetime] = None
|
||||
|
||||
|
||||
class SolarLogBase(BaseModel):
|
||||
"""발전 로그 기본 정보 스키마"""
|
||||
id: int
|
||||
plant_id: int
|
||||
timestamp: datetime
|
||||
power_output: Optional[float] = Field(None, description="현재 발전량 (kW)")
|
||||
daily_generation: Optional[float] = Field(None, description="일일 발전량 (kWh)")
|
||||
total_generation: Optional[float] = Field(None, description="누적 발전량 (kWh)")
|
||||
|
||||
|
||||
class PlantWithLatestLog(PlantBase):
|
||||
"""발전소 정보 + 최신 발전 현황"""
|
||||
latest_log: Optional[SolarLogBase] = Field(None, description="최신 발전 로그")
|
||||
|
||||
|
||||
class PlantResponse(BaseModel):
|
||||
"""단일 발전소 응답 스키마"""
|
||||
status: str = "success"
|
||||
data: PlantWithLatestLog
|
||||
|
||||
|
||||
class PlantsListResponse(BaseModel):
|
||||
"""발전소 목록 응답 스키마"""
|
||||
status: str = "success"
|
||||
data: List[PlantWithLatestLog]
|
||||
total_count: int = Field(description="총 발전소 수")
|
||||
6
requirements.txt
Normal file
6
requirements.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
fastapi>=0.104.0
|
||||
uvicorn[standard]>=0.24.0
|
||||
python-dotenv>=1.0.0
|
||||
supabase>=2.0.0
|
||||
pydantic>=2.0.0
|
||||
pydantic-settings>=2.0.0
|
||||
Loading…
Reference in New Issue
Block a user