Files
kis_bot/kiwoom_rest_api/core/base.py
2026-03-17 12:33:30 +09:00

225 lines
9.7 KiB
Python

from typing import Any, Dict, Optional, Union
import json
from urllib.parse import urljoin
import httpx
import inspect # Import inspect
from kiwoom_rest_api.config import get_base_url, get_headers, DEFAULT_TIMEOUT
class APIError(Exception):
"""Custom exception for API errors"""
def __init__(self, status_code: int, message: str, error_data: dict = None):
self.status_code = status_code
self.message = message
self.error_data = error_data or {}
super().__init__(f"API Error (HTTP {status_code}): {message}")
def __str__(self):
return f"API Error (HTTP {self.status_code}): {self.message}"
def make_url(endpoint: str) -> str:
"""Create a full URL from an endpoint"""
if endpoint.startswith(('http://', 'https://')):
return endpoint
# Ensure endpoint starts with a forward slash
if not endpoint.startswith('/'):
endpoint = f"/{endpoint}"
print("\n\n## full url ##\n\n", urljoin(get_base_url(), endpoint))
return urljoin(get_base_url(), endpoint)
def process_response(response: Any) -> Dict[str, Any]:
"""Process API response and handle errors"""
if not hasattr(response, 'status_code'):
raise ValueError(f"Invalid response object: {response}")
if 200 <= response.status_code < 300:
if not response.text:
return {}
try:
response_json = response.json()
access_control_expose_headers = response.headers.get("access-control-expose-headers")
if access_control_expose_headers:
access_control_expose_headers = access_control_expose_headers.split(",")
for header in access_control_expose_headers:
response_json[header] = response.headers.get(header)
return response_json
return response_json
except json.JSONDecodeError:
return {"content": response.text}
# Handle error responses
error_message = "Unknown error"
error_data = None
try:
error_data = response.json()
error_message = error_data.get("message", "Unknown error")
except (json.JSONDecodeError, AttributeError):
if response.text:
error_message = response.text
raise APIError(response.status_code, error_message, error_data)
def prepare_request_params(
endpoint: str,
method: str = "GET",
params: Optional[Dict[str, Any]] = None,
data: Optional[Dict[str, Any]] = None,
headers: Optional[Dict[str, Any]] = None,
access_token: Optional[str] = None,
timeout: Optional[float] = None,
) -> Dict[str, Any]:
"""Prepare request parameters for HTTP request"""
# 헤더 정규화
normalized_headers = {}
if headers:
for key, value in headers.items():
# 모든 헤더 키를 소문자로 변환하여 중복 방지
normalized_headers[key.lower()] = value
# 기본 헤더 설정
default_headers = {
"content-type": "application/json;charset=UTF-8",
}
# API 키 추가
from kiwoom_rest_api.config import get_api_key, get_api_secret
default_headers["appkey"] = get_api_key()
default_headers["appsecret"] = get_api_secret()
# 헤더 병합 (사용자 정의 헤더가 기본 헤더보다 우선)
merged_headers = {**default_headers, **normalized_headers}
# 액세스 토큰 추가
if access_token:
merged_headers["authorization"] = f"Bearer {access_token}"
# URL 구성
from kiwoom_rest_api.config import get_base_url
url = endpoint if endpoint.startswith(("http://", "https://")) else f"{get_base_url()}{endpoint}"
# 요청 파라미터 구성
request_params = {
"url": url,
"method": method,
"headers": merged_headers,
"timeout": timeout or DEFAULT_TIMEOUT,
}
# 쿼리 파라미터 추가
if params:
request_params["params"] = params
# POST/PUT/PATCH 요청용 데이터 추가
if method in ["POST", "PUT", "PATCH"] and data:
if merged_headers.get("content-type", "").startswith("application/json"):
request_params["json"] = data
else:
request_params["data"] = data
return request_params
async def process_response_async(response: httpx.Response) -> Dict[str, Any]:
if not isinstance(response, httpx.Response):
print("ERROR: process_response_async did not receive an httpx.Response object!")
raise TypeError(f"Expected httpx.Response, but got {type(response)}")
# --- 추가 디버깅 ---
json_method = getattr(response, 'json', None)
is_json_coro = inspect.iscoroutinefunction(json_method)
print(f"DEBUG: inspect.iscoroutinefunction(response.json) = {is_json_coro}")
# --- 추가 디버깅 끝 ---
try:
# 성공(200) 응답 처리
if response.status_code == 200:
try:
# 여기서 여전히 TypeError 발생 가능성 있음
json_data = await response.json()
access_control_expose_headers = response.headers.get("access-control-expose-headers")
if access_control_expose_headers:
access_control_expose_headers = access_control_expose_headers.split(",")
for header in access_control_expose_headers:
json_data[header] = response.headers.get(header)
if isinstance(json_data, dict) and str(json_data.get("return_code")) != "0":
error_message = json_data.get("return_msg", "Unknown API error message")
raise APIError(response.status_code, error_message, json_data)
return json_data
except json.JSONDecodeError:
# 여기서도 TypeError 발생 가능성 있음
raw_text_content = await response.text()
error_message = f"Failed to decode JSON response. Content: {raw_text_content[:200]}"
raise APIError(response.status_code, error_message, {"raw_content": raw_text_content})
except TypeError as te: # await 실패 시
print(f"ERROR: TypeError on SUCCESS path await: {te}")
# await 없이 직접 접근 시도 (진단용)
try:
json_data = response.json() # await 없이 호출
access_control_expose_headers = response.headers.get("access-control-expose-headers")
if access_control_expose_headers:
access_control_expose_headers = access_control_expose_headers.split(",")
for header in access_control_expose_headers:
json_data[header] = response.headers.get(header)
if isinstance(json_data, dict) and str(json_data.get("return_code")) != "0":
error_message = json_data.get("return_msg", "Unknown API error message")
raise APIError(response.status_code, error_message, json_data)
return json_data # 성공하면 반환
except Exception as direct_err:
print(f"ERROR: Direct access failed after TypeError: {direct_err}")
raw_text_content = getattr(response, 'text', 'N/A') # text 속성 접근 시도
raise APIError(response.status_code, f"TypeError processing SUCCESS response: {te}. Raw content: {raw_text_content[:200]}", {"raw_content": raw_text_content})
# HTTP 에러(400 등) 처리
else:
error_message = f"HTTP Error {response.status_code}"
error_data = {"status_code": response.status_code}
raw_text_content = "Could not retrieve error content"
# --- 진단: await 없이 text 속성 직접 접근 시도 ---
try:
if hasattr(response, 'text') and isinstance(response.text, str):
print("DEBUG: Accessing response.text directly as attribute.")
raw_text_content = response.text
error_data["raw_content"] = raw_text_content
error_message += f". Content: {raw_text_content[:500]}" # 내용 조금 더 보기
# 텍스트 내용으로 JSON 파싱 시도
try:
error_json = json.loads(raw_text_content)
error_msg1 = error_json.get("msg1", "No msg1 found in error JSON")
error_message = f"HTTP Error {response.status_code}: {error_msg1}" # 에러 메시지 개선
error_data.update(error_json)
except json.JSONDecodeError:
print("DEBUG: Error response body is not JSON.")
else:
print("DEBUG: response.text is not a direct string attribute.")
# 여기서 await response.text()를 시도하면 TypeError 발생 가능성 높음
except Exception as e_diag:
print(f"ERROR: Exception during diagnostic access of response text: {e_diag}")
# --- 진단 끝 ---
# 최종 에러 발생
raise APIError(response.status_code, error_message, error_data)
except httpx.RequestError as e:
# 네트워크 관련 에러
raise APIError(500, f"Request failed: {str(e)}", {"exception": str(e)})