커서가 망쳐놓은 듯
This commit is contained in:
0
kiwoom_rest_api/core/__init__.py
Normal file
0
kiwoom_rest_api/core/__init__.py
Normal file
49
kiwoom_rest_api/core/async_client.py
Normal file
49
kiwoom_rest_api/core/async_client.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import httpx
|
||||
|
||||
from kiwoom_rest_api.core.base import prepare_request_params, process_response_async
|
||||
|
||||
async def make_request_async(
|
||||
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,
|
||||
**kwargs # Add **kwargs
|
||||
) -> Dict[str, Any]:
|
||||
"""Make an asynchronous HTTP request to the Kiwoom API"""
|
||||
request_params = prepare_request_params(
|
||||
endpoint=endpoint,
|
||||
method=method,
|
||||
params=params,
|
||||
data=data,
|
||||
headers=headers,
|
||||
access_token=access_token,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
# Handle 'json' data from kwargs
|
||||
json_data = kwargs.get('json')
|
||||
if json_data and method in ["POST", "PUT", "PATCH"]:
|
||||
# Prioritize explicitly passed 'json' data
|
||||
request_params["json"] = json_data
|
||||
# If 'data' was also prepared, 'json' takes precedence here.
|
||||
# Remove 'data' if 'json' is being used to avoid conflicts in httpx
|
||||
request_params.pop("data", None)
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
# This should return an httpx.Response object
|
||||
response: httpx.Response = await client.request(
|
||||
method=request_params["method"],
|
||||
url=request_params["url"],
|
||||
params=request_params.get("params"),
|
||||
json=request_params.get("json"),
|
||||
data=request_params.get("data"),
|
||||
headers=request_params["headers"],
|
||||
timeout=request_params["timeout"],
|
||||
)
|
||||
|
||||
return await process_response_async(response)
|
||||
224
kiwoom_rest_api/core/base.py
Normal file
224
kiwoom_rest_api/core/base.py
Normal file
@@ -0,0 +1,224 @@
|
||||
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)})
|
||||
56
kiwoom_rest_api/core/base_api.py
Normal file
56
kiwoom_rest_api/core/base_api.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from typing import Optional
|
||||
from kiwoom_rest_api.core.sync_client import make_request
|
||||
from kiwoom_rest_api.core.async_client import make_request_async
|
||||
|
||||
class KiwoomBaseAPI:
|
||||
def __init__(
|
||||
self,
|
||||
base_url: str = None,
|
||||
token_manager=None,
|
||||
use_async: bool = False,
|
||||
resource_url: str = ""
|
||||
):
|
||||
self.base_url = base_url
|
||||
self.token_manager = token_manager
|
||||
self.use_async = use_async
|
||||
self.resource_url = resource_url
|
||||
self._request_func = make_request_async if use_async else make_request
|
||||
|
||||
def _get_access_token(self) -> Optional[str]:
|
||||
if self.token_manager:
|
||||
return self.token_manager.get_token()
|
||||
return None
|
||||
|
||||
async def _get_access_token_async(self) -> Optional[str]:
|
||||
if self.token_manager and hasattr(self.token_manager, 'get_token_async'):
|
||||
return await self.token_manager.get_token_async()
|
||||
return self._get_access_token()
|
||||
|
||||
def _make_request(self, method: str, url: str, **kwargs):
|
||||
headers = kwargs.pop("headers", {})
|
||||
headers["content-type"] = "application/json;charset=UTF-8"
|
||||
if self.token_manager:
|
||||
access_token = self._get_access_token()
|
||||
headers["Authorization"] = f"Bearer {access_token}"
|
||||
return make_request(endpoint=url, method=method, headers=headers, **kwargs)
|
||||
|
||||
async def _make_request_async(self, method: str, url: str, **kwargs):
|
||||
headers = kwargs.pop("headers", {})
|
||||
headers["content-type"] = "application/json;charset=UTF-8"
|
||||
if self.token_manager:
|
||||
access_token = await self._get_access_token_async()
|
||||
headers["Authorization"] = f"Bearer {access_token}"
|
||||
return await make_request_async(endpoint=url, method=method, headers=headers, **kwargs)
|
||||
|
||||
def _execute_request(self, method: str, resource_url: str = None, **kwargs):
|
||||
# resource_url이 제공되면 임시로 사용, 아니면 기본값 사용
|
||||
url_resource = resource_url if resource_url is not None else self.resource_url
|
||||
#url = f"{self.base_url}{url_resource}" if self.base_url else f"/{url_resource}"
|
||||
if self.base_url:
|
||||
# base_url 끝의 /와 url_resource 앞의 /를 모두 떼고 중간에 / 하나만 넣음
|
||||
url = f"{self.base_url.rstrip('/')}/{url_resource.lstrip('/')}"
|
||||
else:
|
||||
url = f"/{url_resource.lstrip('/')}"
|
||||
if self.use_async:
|
||||
return self._make_request_async(method, url, **kwargs)
|
||||
return self._make_request(method, url, **kwargs)
|
||||
43
kiwoom_rest_api/core/sync_client.py
Normal file
43
kiwoom_rest_api/core/sync_client.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import httpx
|
||||
|
||||
from kiwoom_rest_api.core.base import prepare_request_params, process_response
|
||||
|
||||
def make_request(
|
||||
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,
|
||||
**kwargs: Any
|
||||
) -> Dict[str, Any]:
|
||||
|
||||
"""Make a synchronous HTTP request to the Kiwoom API"""
|
||||
request_params = prepare_request_params(
|
||||
endpoint=endpoint,
|
||||
method=method,
|
||||
params=params,
|
||||
data=data,
|
||||
headers=headers,
|
||||
access_token=access_token,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
# 추가: kwargs에서 json 데이터 처리
|
||||
if 'json' in kwargs and method in ["POST", "PUT", "PATCH"]:
|
||||
request_params["json"] = kwargs['json']
|
||||
|
||||
with httpx.Client() as client:
|
||||
response = client.request(
|
||||
method=request_params["method"],
|
||||
url=request_params["url"],
|
||||
params=request_params.get("params"),
|
||||
json=request_params.get("json"),
|
||||
headers=request_params["headers"],
|
||||
timeout=request_params["timeout"],
|
||||
)
|
||||
|
||||
return process_response(response)
|
||||
Reference in New Issue
Block a user