kcsc-mcp / src /kcsc_api_client.py
nicefree19's picture
Upload 105 files
0ee3c92 verified
Raw
History Blame Contribute Delete
9.2 kB
"""
ν•œκ΅­κ±΄μ„€κΈ°μ€€μ„Όν„°(KCSC) API ν΄λΌμ΄μ–ΈνŠΈ λͺ¨λ“ˆ
"""
import time
import json
import logging
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import Dict, List, Any, Optional, Union, Tuple
from src import config
from src.utils import (
HTTPClient,
APIError,
ConfigError,
DataProcessingError,
save_json_data,
normalize_doc_type
)
logger = config.setup_logger("kcsc_api")
class KCSCApiClient:
"""
ν•œκ΅­κ±΄μ„€κΈ°μ€€μ„Όν„°(KCSC) API와 ν†΅μ‹ ν•˜λŠ” ν΄λΌμ΄μ–ΈνŠΈ
"""
def __init__(self, api_key: str = None, base_url: str = None):
"""
KCSC API ν΄λΌμ΄μ–ΈνŠΈ μ΄ˆκΈ°ν™”
Args:
api_key: KCSC API ν‚€
base_url: KCSC API κΈ°λ³Έ URL
"""
self.api_key = api_key or config.KCSC_API_KEY
if not self.api_key:
raise ConfigError("KCSC API ν‚€κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. .env νŒŒμΌμ„ ν™•μΈν•˜μ„Έμš”.")
self.base_url = base_url or "https://www.kcsc.re.kr/OpenApi"
self.data_dir = config.DATA_DIR
# HTTP ν΄λΌμ΄μ–ΈνŠΈ μ΄ˆκΈ°ν™”
self.http_client = HTTPClient(
base_url=self.base_url,
default_headers={
"Accept": "application/json",
"Content-Type": "application/json"
}
)
logger.info(f"KCSC API ν΄λΌμ΄μ–ΈνŠΈ μ΄ˆκΈ°ν™” μ™„λ£Œ (κΈ°λ³Έ URL: {self.base_url})")
def get_code_list(self, doc_type: str = "KDS") -> List[Dict[str, Any]]:
"""
μ½”λ“œ λͺ©λ‘ κ°€μ Έμ˜€κΈ°
Args:
doc_type: λ¬Έμ„œ μœ ν˜• (예: "KDS", "KCS")
Returns:
μ½”λ“œ λͺ©λ‘
Raises:
APIError: API μš”μ²­ μ‹€νŒ¨ μ‹œ
"""
doc_type = normalize_doc_type(doc_type)
params = {
"Type": doc_type,
"key": self.api_key
}
try:
logger.info(f"{doc_type} μ½”λ“œ λͺ©λ‘ μš”μ²­ 쀑...")
response = self.http_client.get("CodeList", params=params)
data = response.json()
if "List" not in data:
raise APIError(f"{doc_type} μ½”λ“œ λͺ©λ‘μ— 'List' ν•„λ“œκ°€ μ—†μŠ΅λ‹ˆλ‹€.", response=data)
code_list = data["List"]
logger.info(f"{doc_type} μ½”λ“œ λͺ©λ‘ μš”μ²­ 성곡 (총 {len(code_list)}개 ν•­λͺ©)")
return code_list
except APIError:
raise
except Exception as e:
raise APIError(f"{doc_type} μ½”λ“œ λͺ©λ‘ μš”μ²­ μ‹€νŒ¨: {str(e)}")
def get_code_details(self, doc_type: str, code: str) -> Dict[str, Any]:
"""
μ½”λ“œ 상세 정보 κ°€μ Έμ˜€κΈ°
Args:
doc_type: λ¬Έμ„œ μœ ν˜• (예: "KDS", "KCS")
code: μ½”λ“œ 번호
Returns:
μ½”λ“œ 상세 정보
Raises:
APIError: API μš”μ²­ μ‹€νŒ¨ μ‹œ
"""
doc_type = normalize_doc_type(doc_type)
params = {
"key": self.api_key
}
try:
logger.info(f"{doc_type}/{code} 상세 정보 μš”μ²­ 쀑...")
endpoint = f"CodeViewer/{doc_type}/{code}"
response = self.http_client.get(endpoint, params=params)
data = response.json()
logger.info(f"{doc_type}/{code} 상세 정보 μš”μ²­ 성곡")
# 응닡에 λ¬Έμ„œ μœ ν˜• 정보 μΆ”κ°€
if data and isinstance(data, dict):
data["DocType"] = doc_type
return data
except APIError:
raise
except Exception as e:
raise APIError(f"{doc_type}/{code} 상세 정보 μš”μ²­ μ‹€νŒ¨: {str(e)}")
def collect_codes(self, doc_types: List[str] = None, save: bool = True) -> Dict[str, List[Dict[str, Any]]]:
"""
λ¬Έμ„œ μœ ν˜•λ³„ μ½”λ“œ μˆ˜μ§‘
Args:
doc_types: μˆ˜μ§‘ν•  λ¬Έμ„œ μœ ν˜• λͺ©λ‘
save: μˆ˜μ§‘ν•œ 데이터λ₯Ό 파일둜 μ €μž₯ν• μ§€ μ—¬λΆ€
Returns:
λ¬Έμ„œ μœ ν˜•λ³„ μ½”λ“œ λͺ©λ‘ λ”•μ…”λ„ˆλ¦¬
Raises:
APIError: API μš”μ²­ μ‹€νŒ¨ μ‹œ
"""
if doc_types is None:
doc_types = ["KDS", "KCS"]
all_codes = {}
for doc_type in doc_types:
normalized_type = normalize_doc_type(doc_type)
logger.info(f"{normalized_type} μ½”λ“œ μˆ˜μ§‘ μ‹œμž‘")
try:
code_list = self.get_code_list(normalized_type)
if not code_list:
logger.warning(f"{normalized_type} μ½”λ“œ λͺ©λ‘μ΄ λΉ„μ–΄ μžˆμŠ΅λ‹ˆλ‹€.")
continue
all_codes[normalized_type] = code_list
if save:
filename = f"{normalized_type}_codes.json"
save_json_data(code_list, filename)
except Exception as e:
logger.error(f"{normalized_type} μ½”λ“œ μˆ˜μ§‘ 쀑 였λ₯˜ λ°œμƒ: {str(e)}")
return all_codes
def fetch_code_details(
self,
doc_type: str,
codes: List[Dict[str, Any]],
save: bool = True,
max_workers: int = 5
) -> List[Dict[str, Any]]:
"""
νŠΉμ • λ¬Έμ„œ μœ ν˜•μ˜ μ—¬λŸ¬ μ½”λ“œμ— λŒ€ν•œ 상세 정보 κ°€μ Έμ˜€κΈ°
Args:
doc_type: λ¬Έμ„œ μœ ν˜•
codes: μ½”λ“œ λͺ©λ‘
save: μˆ˜μ§‘ν•œ 데이터λ₯Ό 파일둜 μ €μž₯ν• μ§€ μ—¬λΆ€
max_workers: 병렬 μ²˜λ¦¬ν•  μ΅œλŒ€ μž‘μ—…μž 수
Returns:
μ½”λ“œ 상세 정보 λͺ©λ‘
Raises:
APIError: API μš”μ²­ μ‹€νŒ¨ μ‹œ
"""
doc_type = normalize_doc_type(doc_type)
details = []
failed_codes = []
logger.info(f"{doc_type} 상세 정보 μˆ˜μ§‘ μ‹œμž‘ (총 {len(codes)}개 μ½”λ“œ)")
# 병렬 처리둜 μ—¬λŸ¬ μ½”λ“œ 정보 μˆ˜μ§‘
with ThreadPoolExecutor(max_workers=max_workers) as executor:
# 각 μ½”λ“œμ— λŒ€ν•œ μž‘μ—… 생성
future_to_code = {
executor.submit(self._fetch_single_code_detail, doc_type, code.get("Code")): code
for code in codes if code.get("Code")
}
# μž‘μ—… κ²°κ³Ό 처리
for i, future in enumerate(as_completed(future_to_code)):
code = future_to_code[future]
try:
detail = future.result()
if detail:
details.append(detail)
# μ§„ν–‰ 상황 λ‘œκΉ… (10% λ‹¨μœ„)
completion = (i + 1) / len(codes) * 100
if completion % 10 < 100 / len(codes):
logger.info(f"{doc_type} 상세 정보 μˆ˜μ§‘ μ§„ν–‰ 쀑: {i+1}/{len(codes)} ({completion:.1f}%)")
except Exception as e:
logger.error(f"{doc_type}/{code.get('Code')} 상세 정보 μˆ˜μ§‘ μ‹€νŒ¨: {str(e)}")
failed_codes.append(code.get("Code"))
logger.info(f"{doc_type} 상세 정보 μˆ˜μ§‘ μ™„λ£Œ: {len(details)}개 성곡, {len(failed_codes)}개 μ‹€νŒ¨")
if failed_codes:
logger.warning(f"μ‹€νŒ¨ν•œ μ½”λ“œ: {', '.join(failed_codes[:10])}{' μ™Έ {}개'.format(len(failed_codes)-10) if len(failed_codes) > 10 else ''}")
if save and details:
filename = f"{doc_type}_details.json"
save_json_data(details, filename)
return details
def _fetch_single_code_detail(self, doc_type: str, code: str) -> Optional[Dict[str, Any]]:
"""
단일 μ½”λ“œ 상세 정보 κ°€μ Έμ˜€κΈ° (λ‚΄λΆ€ λ©”μ„œλ“œ)
Args:
doc_type: λ¬Έμ„œ μœ ν˜•
code: μ½”λ“œ 번호
Returns:
μ½”λ“œ 상세 정보 λ˜λŠ” None (였λ₯˜ μ‹œ)
"""
try:
# API μš”μ²­
detail = self.get_code_details(doc_type, code)
# API μ„œλ²„ λΆ€ν•˜ λ°©μ§€λ₯Ό μœ„ν•œ μ§€μ—°
time.sleep(config.API_RETRY_DELAY)
return detail
except Exception as e:
logger.error(f"{doc_type}/{code} 상세 정보 κ°€μ Έμ˜€κΈ° μ‹€νŒ¨: {str(e)}")
return None
# μ‹€ν–‰ μ˜ˆμ‹œ
if __name__ == "__main__":
# μ„€μ • μ΄ˆκΈ°ν™”
config.init()
# API ν΄λΌμ΄μ–ΈνŠΈ μ΄ˆκΈ°ν™”
kcsc = KCSCApiClient()
# μ½”λ“œ λͺ©λ‘ μˆ˜μ§‘
all_codes = kcsc.collect_codes()
# μ½”λ“œλ³„ 상세 정보 μˆ˜μ§‘
for doc_type, codes in all_codes.items():
logger.info(f"{doc_type} 상세 정보 μˆ˜μ§‘ μ‹œμž‘ (총 {len(codes)}개 μ½”λ“œ)")
kcsc.fetch_code_details(doc_type, codes)