ecos-reader 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ecos/__init__.py +109 -0
- ecos/cache.py +212 -0
- ecos/client.py +394 -0
- ecos/config.py +148 -0
- ecos/constants.py +155 -0
- ecos/exceptions.py +73 -0
- ecos/indicators/__init__.py +29 -0
- ecos/indicators/growth.py +186 -0
- ecos/indicators/interest_rate.py +226 -0
- ecos/indicators/money.py +173 -0
- ecos/indicators/prices.py +206 -0
- ecos/logging.py +205 -0
- ecos/metrics.py +247 -0
- ecos/parser.py +197 -0
- ecos/py.typed +0 -0
- ecos/types.py +84 -0
- ecos_reader-0.1.0.dist-info/METADATA +286 -0
- ecos_reader-0.1.0.dist-info/RECORD +20 -0
- ecos_reader-0.1.0.dist-info/WHEEL +4 -0
- ecos_reader-0.1.0.dist-info/licenses/LICENSE +21 -0
ecos/__init__.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ecos-reader: 한국은행 ECOS Open API Python 클라이언트
|
|
3
|
+
|
|
4
|
+
한국은행 ECOS Open API를 Python에서 쉽고 일관된 방식으로 사용할 수 있는 라이브러리입니다.
|
|
5
|
+
|
|
6
|
+
Examples
|
|
7
|
+
--------
|
|
8
|
+
>>> import ecos
|
|
9
|
+
>>> ecos.set_api_key("your_api_key")
|
|
10
|
+
|
|
11
|
+
# 기준금리 조회
|
|
12
|
+
>>> df = ecos.get_base_rate()
|
|
13
|
+
>>> df.head()
|
|
14
|
+
date value unit
|
|
15
|
+
0 2024-01-01 3.50 %
|
|
16
|
+
|
|
17
|
+
# 소비자물가지수 조회
|
|
18
|
+
>>> df = ecos.get_cpi(start_date="202301", end_date="202312")
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
__version__ = "0.1.0"
|
|
24
|
+
__author__ = "yeonguk"
|
|
25
|
+
|
|
26
|
+
# Config API
|
|
27
|
+
# Cache API
|
|
28
|
+
from .cache import clear_cache, disable_cache, enable_cache
|
|
29
|
+
|
|
30
|
+
# Client
|
|
31
|
+
from .client import EcosClient, get_client, reset_client, set_client
|
|
32
|
+
from .config import clear_api_key, get_api_key, load_env, set_api_key
|
|
33
|
+
|
|
34
|
+
# Exceptions
|
|
35
|
+
from .exceptions import (
|
|
36
|
+
EcosAPIError,
|
|
37
|
+
EcosConfigError,
|
|
38
|
+
EcosError,
|
|
39
|
+
EcosNetworkError,
|
|
40
|
+
EcosRateLimitError,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Indicator APIs
|
|
44
|
+
from .indicators import (
|
|
45
|
+
get_bank_lending,
|
|
46
|
+
# 금리 지표
|
|
47
|
+
get_base_rate,
|
|
48
|
+
get_core_cpi,
|
|
49
|
+
# 물가 지표
|
|
50
|
+
get_cpi,
|
|
51
|
+
# 성장 지표
|
|
52
|
+
get_gdp,
|
|
53
|
+
get_gdp_deflator,
|
|
54
|
+
# 통화 지표
|
|
55
|
+
get_money_supply,
|
|
56
|
+
get_ppi,
|
|
57
|
+
get_treasury_yield,
|
|
58
|
+
get_yield_spread,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Logging API
|
|
62
|
+
from .logging import setup_logging
|
|
63
|
+
|
|
64
|
+
# Metrics API
|
|
65
|
+
from .metrics import get_metrics_summary, reset_metrics
|
|
66
|
+
|
|
67
|
+
__all__ = [
|
|
68
|
+
# Version
|
|
69
|
+
"__version__",
|
|
70
|
+
# Config
|
|
71
|
+
"set_api_key",
|
|
72
|
+
"get_api_key",
|
|
73
|
+
"clear_api_key",
|
|
74
|
+
"load_env",
|
|
75
|
+
# Cache
|
|
76
|
+
"clear_cache",
|
|
77
|
+
"disable_cache",
|
|
78
|
+
"enable_cache",
|
|
79
|
+
# Client
|
|
80
|
+
"EcosClient",
|
|
81
|
+
"get_client",
|
|
82
|
+
"set_client",
|
|
83
|
+
"reset_client",
|
|
84
|
+
# Exceptions
|
|
85
|
+
"EcosError",
|
|
86
|
+
"EcosAPIError",
|
|
87
|
+
"EcosConfigError",
|
|
88
|
+
"EcosNetworkError",
|
|
89
|
+
"EcosRateLimitError",
|
|
90
|
+
# 금리 지표
|
|
91
|
+
"get_base_rate",
|
|
92
|
+
"get_treasury_yield",
|
|
93
|
+
"get_yield_spread",
|
|
94
|
+
# 물가 지표
|
|
95
|
+
"get_cpi",
|
|
96
|
+
"get_core_cpi",
|
|
97
|
+
"get_ppi",
|
|
98
|
+
# 성장 지표
|
|
99
|
+
"get_gdp",
|
|
100
|
+
"get_gdp_deflator",
|
|
101
|
+
# 통화 지표
|
|
102
|
+
"get_money_supply",
|
|
103
|
+
"get_bank_lending",
|
|
104
|
+
# Metrics
|
|
105
|
+
"get_metrics_summary",
|
|
106
|
+
"reset_metrics",
|
|
107
|
+
# Logging
|
|
108
|
+
"setup_logging",
|
|
109
|
+
]
|
ecos/cache.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ecos-reader 캐시 레이어
|
|
3
|
+
|
|
4
|
+
동일 요청에 대한 응답을 캐싱하여 API 호출을 최소화합니다.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
import time
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from .logging import log_cache_operation
|
|
16
|
+
from .metrics import record_cache_clear, record_cache_hit, record_cache_miss, record_cache_set
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class CacheEntry:
|
|
21
|
+
"""캐시 항목"""
|
|
22
|
+
|
|
23
|
+
value: Any
|
|
24
|
+
created_at: float
|
|
25
|
+
ttl: int
|
|
26
|
+
|
|
27
|
+
def is_expired(self) -> bool:
|
|
28
|
+
"""캐시 만료 여부 확인"""
|
|
29
|
+
return time.time() - self.created_at > self.ttl
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Cache:
|
|
33
|
+
"""
|
|
34
|
+
인메모리 LRU 캐시
|
|
35
|
+
|
|
36
|
+
TTL(Time-To-Live) 기반으로 캐시를 관리합니다.
|
|
37
|
+
|
|
38
|
+
Parameters
|
|
39
|
+
----------
|
|
40
|
+
ttl : int
|
|
41
|
+
캐시 유효 시간 (초), 기본값 3600 (1시간)
|
|
42
|
+
maxsize : int
|
|
43
|
+
최대 캐시 항목 수, 기본값 100
|
|
44
|
+
|
|
45
|
+
Examples
|
|
46
|
+
--------
|
|
47
|
+
>>> cache = Cache(ttl=3600, maxsize=100)
|
|
48
|
+
>>> cache.set("key", {"data": "value"})
|
|
49
|
+
>>> cache.get("key")
|
|
50
|
+
{'data': 'value'}
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(self, ttl: int = 3600, maxsize: int = 100):
|
|
54
|
+
self.ttl = ttl
|
|
55
|
+
self.maxsize = maxsize
|
|
56
|
+
self._cache: dict[str, CacheEntry] = {}
|
|
57
|
+
self._access_order: list[str] = []
|
|
58
|
+
self._enabled: bool = True
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def enabled(self) -> bool:
|
|
62
|
+
"""캐시 활성화 상태"""
|
|
63
|
+
return self._enabled
|
|
64
|
+
|
|
65
|
+
@enabled.setter
|
|
66
|
+
def enabled(self, value: bool) -> None:
|
|
67
|
+
"""캐시 활성화 설정"""
|
|
68
|
+
self._enabled = value
|
|
69
|
+
|
|
70
|
+
def _make_key(self, *args: Any, **kwargs: Any) -> str:
|
|
71
|
+
"""
|
|
72
|
+
요청 파라미터로부터 캐시 키를 생성합니다.
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
----------
|
|
76
|
+
*args : Any
|
|
77
|
+
위치 인자
|
|
78
|
+
**kwargs : Any
|
|
79
|
+
키워드 인자
|
|
80
|
+
|
|
81
|
+
Returns
|
|
82
|
+
-------
|
|
83
|
+
str
|
|
84
|
+
해시된 캐시 키 (SHA256 전체 해시 사용으로 충돌 방지)
|
|
85
|
+
"""
|
|
86
|
+
key_data = json.dumps({"args": args, "kwargs": kwargs}, sort_keys=True)
|
|
87
|
+
return hashlib.sha256(key_data.encode()).hexdigest()
|
|
88
|
+
|
|
89
|
+
def get(self, key: str) -> Any | None:
|
|
90
|
+
"""
|
|
91
|
+
캐시에서 값을 조회합니다.
|
|
92
|
+
|
|
93
|
+
Parameters
|
|
94
|
+
----------
|
|
95
|
+
key : str
|
|
96
|
+
캐시 키
|
|
97
|
+
|
|
98
|
+
Returns
|
|
99
|
+
-------
|
|
100
|
+
Optional[Any]
|
|
101
|
+
캐시된 값, 없거나 만료된 경우 None
|
|
102
|
+
"""
|
|
103
|
+
if not self._enabled:
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
entry = self._cache.get(key)
|
|
107
|
+
if entry is None:
|
|
108
|
+
record_cache_miss()
|
|
109
|
+
log_cache_operation("get", key, hit=False)
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
if entry.is_expired():
|
|
113
|
+
self.invalidate(key)
|
|
114
|
+
record_cache_miss()
|
|
115
|
+
log_cache_operation("get", key, hit=False)
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
# LRU: 접근된 항목을 최신으로 이동
|
|
119
|
+
if key in self._access_order:
|
|
120
|
+
self._access_order.remove(key)
|
|
121
|
+
self._access_order.append(key)
|
|
122
|
+
|
|
123
|
+
record_cache_hit()
|
|
124
|
+
log_cache_operation("get", key, hit=True)
|
|
125
|
+
return entry.value
|
|
126
|
+
|
|
127
|
+
def set(self, key: str, value: Any) -> None:
|
|
128
|
+
"""
|
|
129
|
+
캐시에 값을 저장합니다.
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
key : str
|
|
134
|
+
캐시 키
|
|
135
|
+
value : Any
|
|
136
|
+
저장할 값
|
|
137
|
+
"""
|
|
138
|
+
if not self._enabled:
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
# 최대 크기 초과 시 가장 오래된 항목 제거
|
|
142
|
+
while len(self._cache) >= self.maxsize and self._access_order:
|
|
143
|
+
oldest_key = self._access_order.pop(0)
|
|
144
|
+
self._cache.pop(oldest_key, None)
|
|
145
|
+
|
|
146
|
+
self._cache[key] = CacheEntry(value=value, created_at=time.time(), ttl=self.ttl)
|
|
147
|
+
|
|
148
|
+
if key in self._access_order:
|
|
149
|
+
self._access_order.remove(key)
|
|
150
|
+
self._access_order.append(key)
|
|
151
|
+
|
|
152
|
+
record_cache_set()
|
|
153
|
+
log_cache_operation("set", key)
|
|
154
|
+
|
|
155
|
+
def invalidate(self, key: str) -> None:
|
|
156
|
+
"""
|
|
157
|
+
특정 캐시 항목을 무효화합니다.
|
|
158
|
+
|
|
159
|
+
Parameters
|
|
160
|
+
----------
|
|
161
|
+
key : str
|
|
162
|
+
무효화할 캐시 키
|
|
163
|
+
"""
|
|
164
|
+
self._cache.pop(key, None)
|
|
165
|
+
if key in self._access_order:
|
|
166
|
+
self._access_order.remove(key)
|
|
167
|
+
|
|
168
|
+
def clear(self) -> None:
|
|
169
|
+
"""모든 캐시를 삭제합니다."""
|
|
170
|
+
self._cache.clear()
|
|
171
|
+
self._access_order.clear()
|
|
172
|
+
record_cache_clear()
|
|
173
|
+
log_cache_operation("clear", "")
|
|
174
|
+
|
|
175
|
+
def __len__(self) -> int:
|
|
176
|
+
"""캐시된 항목 수"""
|
|
177
|
+
return len(self._cache)
|
|
178
|
+
|
|
179
|
+
def __contains__(self, key: str) -> bool:
|
|
180
|
+
"""캐시 키 존재 여부"""
|
|
181
|
+
return key in self._cache and not self._cache[key].is_expired()
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# 전역 캐시 인스턴스
|
|
185
|
+
_global_cache: Cache | None = None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def get_cache() -> Cache:
|
|
189
|
+
"""전역 캐시 인스턴스를 반환합니다."""
|
|
190
|
+
global _global_cache
|
|
191
|
+
if _global_cache is None:
|
|
192
|
+
from .config import Settings
|
|
193
|
+
|
|
194
|
+
_global_cache = Cache(ttl=Settings.CACHE_TTL, maxsize=Settings.CACHE_MAXSIZE)
|
|
195
|
+
return _global_cache
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def clear_cache() -> None:
|
|
199
|
+
"""전역 캐시를 초기화합니다."""
|
|
200
|
+
global _global_cache
|
|
201
|
+
if _global_cache is not None:
|
|
202
|
+
_global_cache.clear()
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def disable_cache() -> None:
|
|
206
|
+
"""캐시를 비활성화합니다."""
|
|
207
|
+
get_cache().enabled = False
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def enable_cache() -> None:
|
|
211
|
+
"""캐시를 활성화합니다."""
|
|
212
|
+
get_cache().enabled = True
|
ecos/client.py
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ecos-reader API 클라이언트
|
|
3
|
+
|
|
4
|
+
ECOS API와 HTTP 통신을 담당하는 핵심 클라이언트입니다.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import contextlib
|
|
10
|
+
import time
|
|
11
|
+
from typing import Any, cast
|
|
12
|
+
from urllib.parse import quote
|
|
13
|
+
|
|
14
|
+
import requests
|
|
15
|
+
|
|
16
|
+
from .cache import Cache, get_cache
|
|
17
|
+
from .config import Settings, get_api_key
|
|
18
|
+
from .exceptions import (
|
|
19
|
+
RETRYABLE_ERROR_CODES,
|
|
20
|
+
EcosAPIError,
|
|
21
|
+
EcosConfigError,
|
|
22
|
+
EcosNetworkError,
|
|
23
|
+
EcosRateLimitError,
|
|
24
|
+
)
|
|
25
|
+
from .logging import log_api_request, log_error_response, log_retry_attempt, logger
|
|
26
|
+
from .types import EcosService
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class EcosClient:
|
|
30
|
+
"""
|
|
31
|
+
ECOS API 클라이언트
|
|
32
|
+
|
|
33
|
+
한국은행 ECOS Open API와 HTTP 통신을 담당합니다.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
api_key : str, optional
|
|
38
|
+
ECOS API 인증키. 미제공 시 환경 변수에서 로드
|
|
39
|
+
timeout : int, optional
|
|
40
|
+
요청 타임아웃(초), 기본값 30
|
|
41
|
+
max_retries : int, optional
|
|
42
|
+
최대 재시도 횟수, 기본값 3
|
|
43
|
+
use_cache : bool, optional
|
|
44
|
+
캐시 사용 여부, 기본값 True
|
|
45
|
+
|
|
46
|
+
Examples
|
|
47
|
+
--------
|
|
48
|
+
>>> client = EcosClient()
|
|
49
|
+
>>> result = client.get_statistic_search(
|
|
50
|
+
... stat_code="722Y001",
|
|
51
|
+
... period="M",
|
|
52
|
+
... start_date="202301",
|
|
53
|
+
... end_date="202312",
|
|
54
|
+
... item_code1="0101000"
|
|
55
|
+
... )
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
BASE_URL = Settings.BASE_URL
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
api_key: str | None = None,
|
|
63
|
+
timeout: int = Settings.DEFAULT_TIMEOUT,
|
|
64
|
+
max_retries: int = Settings.MAX_RETRIES,
|
|
65
|
+
use_cache: bool = True,
|
|
66
|
+
):
|
|
67
|
+
self.api_key = api_key
|
|
68
|
+
self.timeout = timeout
|
|
69
|
+
self.max_retries = max_retries
|
|
70
|
+
self.use_cache = use_cache
|
|
71
|
+
self.session = requests.Session()
|
|
72
|
+
self._cache: Cache | None = get_cache() if use_cache else None
|
|
73
|
+
|
|
74
|
+
def _get_api_key(self) -> str:
|
|
75
|
+
"""API Key를 반환합니다."""
|
|
76
|
+
if self.api_key:
|
|
77
|
+
return self.api_key
|
|
78
|
+
return get_api_key()
|
|
79
|
+
|
|
80
|
+
def _build_url(
|
|
81
|
+
self,
|
|
82
|
+
service: EcosService,
|
|
83
|
+
start: int,
|
|
84
|
+
end: int,
|
|
85
|
+
*path_params: str,
|
|
86
|
+
) -> str:
|
|
87
|
+
"""
|
|
88
|
+
ECOS API 요청 URL을 구성합니다.
|
|
89
|
+
|
|
90
|
+
URL 형식: {BASE_URL}/{service}/{api_key}/{format}/{lang}/{start}/{end}/{params...}/
|
|
91
|
+
"""
|
|
92
|
+
api_key = self._get_api_key()
|
|
93
|
+
parts = [
|
|
94
|
+
self.BASE_URL.rstrip("/"),
|
|
95
|
+
service,
|
|
96
|
+
api_key,
|
|
97
|
+
Settings.DEFAULT_FORMAT,
|
|
98
|
+
Settings.DEFAULT_LANG,
|
|
99
|
+
str(start),
|
|
100
|
+
str(end),
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
# 추가 파라미터 (통계코드, 주기, 날짜 등)
|
|
104
|
+
for param in path_params:
|
|
105
|
+
if param: # 빈 문자열은 제외
|
|
106
|
+
parts.append(quote(str(param), safe=""))
|
|
107
|
+
|
|
108
|
+
return "/".join(parts)
|
|
109
|
+
|
|
110
|
+
@log_api_request
|
|
111
|
+
def _make_request(self, url: str) -> dict[str, Any]:
|
|
112
|
+
"""
|
|
113
|
+
HTTP GET 요청을 수행합니다.
|
|
114
|
+
|
|
115
|
+
Parameters
|
|
116
|
+
----------
|
|
117
|
+
url : str
|
|
118
|
+
요청 URL
|
|
119
|
+
|
|
120
|
+
Returns
|
|
121
|
+
-------
|
|
122
|
+
dict
|
|
123
|
+
JSON 응답
|
|
124
|
+
|
|
125
|
+
Raises
|
|
126
|
+
------
|
|
127
|
+
EcosNetworkError
|
|
128
|
+
네트워크 에러 발생 시
|
|
129
|
+
EcosAPIError
|
|
130
|
+
API 에러 응답 시
|
|
131
|
+
EcosRateLimitError
|
|
132
|
+
Rate Limit 초과 시
|
|
133
|
+
"""
|
|
134
|
+
last_exception: Exception | None = None
|
|
135
|
+
|
|
136
|
+
for attempt in range(self.max_retries):
|
|
137
|
+
try:
|
|
138
|
+
logger.debug(f"API 요청 전송: {url}")
|
|
139
|
+
response = self.session.get(url, timeout=self.timeout)
|
|
140
|
+
response.raise_for_status()
|
|
141
|
+
data = cast(dict[str, Any], response.json())
|
|
142
|
+
|
|
143
|
+
# 에러 응답 확인
|
|
144
|
+
self._check_error_response(data, url)
|
|
145
|
+
|
|
146
|
+
logger.debug(f"API 응답 성공: {len(data)} 바이트 수신")
|
|
147
|
+
return data
|
|
148
|
+
|
|
149
|
+
except requests.exceptions.Timeout:
|
|
150
|
+
last_exception = EcosNetworkError(f"요청 타임아웃 ({self.timeout}초)")
|
|
151
|
+
if attempt < self.max_retries - 1:
|
|
152
|
+
log_retry_attempt(attempt + 1, self.max_retries, last_exception)
|
|
153
|
+
|
|
154
|
+
except requests.exceptions.ConnectionError as e:
|
|
155
|
+
last_exception = EcosNetworkError(f"네트워크 연결 오류: {e}")
|
|
156
|
+
if attempt < self.max_retries - 1:
|
|
157
|
+
log_retry_attempt(attempt + 1, self.max_retries, last_exception)
|
|
158
|
+
|
|
159
|
+
except requests.exceptions.HTTPError as e:
|
|
160
|
+
last_exception = EcosNetworkError(f"HTTP 오류: {e}")
|
|
161
|
+
if attempt < self.max_retries - 1:
|
|
162
|
+
log_retry_attempt(attempt + 1, self.max_retries, last_exception)
|
|
163
|
+
|
|
164
|
+
except (EcosAPIError, EcosRateLimitError) as e:
|
|
165
|
+
# 재시도 가능한 에러인지 확인
|
|
166
|
+
error_key = f"ERROR-{e.code}" if hasattr(e, "code") else ""
|
|
167
|
+
if error_key in RETRYABLE_ERROR_CODES:
|
|
168
|
+
last_exception = e
|
|
169
|
+
if attempt < self.max_retries - 1:
|
|
170
|
+
log_retry_attempt(attempt + 1, self.max_retries, last_exception)
|
|
171
|
+
else:
|
|
172
|
+
raise
|
|
173
|
+
|
|
174
|
+
# 재시도 전 대기 (exponential backoff)
|
|
175
|
+
if attempt < self.max_retries - 1:
|
|
176
|
+
wait_time = Settings.RETRY_BACKOFF_FACTOR * (2**attempt)
|
|
177
|
+
logger.debug(f"재시도 전 대기: {wait_time:.2f}초")
|
|
178
|
+
time.sleep(wait_time)
|
|
179
|
+
|
|
180
|
+
# 모든 재시도 실패
|
|
181
|
+
if last_exception:
|
|
182
|
+
raise last_exception
|
|
183
|
+
|
|
184
|
+
raise EcosNetworkError("알 수 없는 네트워크 오류")
|
|
185
|
+
|
|
186
|
+
def _check_error_response(self, data: dict[str, Any], url: str = "") -> None:
|
|
187
|
+
"""
|
|
188
|
+
API 응답에서 에러를 확인합니다.
|
|
189
|
+
|
|
190
|
+
Parameters
|
|
191
|
+
----------
|
|
192
|
+
data : dict
|
|
193
|
+
API 응답 데이터
|
|
194
|
+
url : str, optional
|
|
195
|
+
요청 URL (로깅용)
|
|
196
|
+
"""
|
|
197
|
+
# RESULT 키 확인
|
|
198
|
+
result = data.get("RESULT")
|
|
199
|
+
if not result:
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
code = result.get("CODE", "")
|
|
203
|
+
message = result.get("MESSAGE", "")
|
|
204
|
+
|
|
205
|
+
# 정보 코드 200: 데이터 없음 - 정상 처리 (빈 결과)
|
|
206
|
+
if code == "INFO-200":
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
# 정보 코드 100: 인증키 오류
|
|
210
|
+
if code == "INFO-100":
|
|
211
|
+
log_error_response(code, message, url)
|
|
212
|
+
raise EcosConfigError(message)
|
|
213
|
+
|
|
214
|
+
# 에러 코드
|
|
215
|
+
if code.startswith("ERROR"):
|
|
216
|
+
error_num = code.split("-")[-1] if "-" in code else code.replace("ERROR", "")
|
|
217
|
+
log_error_response(code, message, url)
|
|
218
|
+
|
|
219
|
+
if error_num == "602":
|
|
220
|
+
raise EcosRateLimitError(message)
|
|
221
|
+
|
|
222
|
+
raise EcosAPIError(error_num, message)
|
|
223
|
+
|
|
224
|
+
def get_statistic_search(
|
|
225
|
+
self,
|
|
226
|
+
stat_code: str,
|
|
227
|
+
period: str,
|
|
228
|
+
start_date: str,
|
|
229
|
+
end_date: str,
|
|
230
|
+
item_code1: str = "",
|
|
231
|
+
item_code2: str = "",
|
|
232
|
+
item_code3: str = "",
|
|
233
|
+
item_code4: str = "",
|
|
234
|
+
start: int = 1,
|
|
235
|
+
end: int = 100000,
|
|
236
|
+
) -> dict[str, Any]:
|
|
237
|
+
"""
|
|
238
|
+
통계 조회 (StatisticSearch)
|
|
239
|
+
|
|
240
|
+
Parameters
|
|
241
|
+
----------
|
|
242
|
+
stat_code : str
|
|
243
|
+
통계표코드
|
|
244
|
+
period : str
|
|
245
|
+
주기 (D: 일, M: 월, Q: 분기, A: 연)
|
|
246
|
+
start_date : str
|
|
247
|
+
조회 시작일
|
|
248
|
+
end_date : str
|
|
249
|
+
조회 종료일
|
|
250
|
+
item_code1 : str, optional
|
|
251
|
+
통계항목코드1
|
|
252
|
+
item_code2 : str, optional
|
|
253
|
+
통계항목코드2
|
|
254
|
+
item_code3 : str, optional
|
|
255
|
+
통계항목코드3
|
|
256
|
+
item_code4 : str, optional
|
|
257
|
+
통계항목코드4
|
|
258
|
+
start : int, optional
|
|
259
|
+
시작 건수, 기본값 1
|
|
260
|
+
end : int, optional
|
|
261
|
+
종료 건수, 기본값 100000
|
|
262
|
+
|
|
263
|
+
Returns
|
|
264
|
+
-------
|
|
265
|
+
dict
|
|
266
|
+
API 응답 데이터
|
|
267
|
+
"""
|
|
268
|
+
# 캐시 확인
|
|
269
|
+
if self._cache and self.use_cache:
|
|
270
|
+
cache_key = self._cache._make_key(
|
|
271
|
+
"StatisticSearch",
|
|
272
|
+
stat_code,
|
|
273
|
+
period,
|
|
274
|
+
start_date,
|
|
275
|
+
end_date,
|
|
276
|
+
item_code1,
|
|
277
|
+
item_code2,
|
|
278
|
+
item_code3,
|
|
279
|
+
item_code4,
|
|
280
|
+
)
|
|
281
|
+
cached = self._cache.get(cache_key)
|
|
282
|
+
if cached is not None:
|
|
283
|
+
return cast(dict[str, Any], cached)
|
|
284
|
+
|
|
285
|
+
url = self._build_url(
|
|
286
|
+
"StatisticSearch",
|
|
287
|
+
start,
|
|
288
|
+
end,
|
|
289
|
+
stat_code,
|
|
290
|
+
period,
|
|
291
|
+
start_date,
|
|
292
|
+
end_date,
|
|
293
|
+
item_code1,
|
|
294
|
+
item_code2,
|
|
295
|
+
item_code3,
|
|
296
|
+
item_code4,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
result = self._make_request(url)
|
|
300
|
+
|
|
301
|
+
# 캐시 저장
|
|
302
|
+
if self._cache and self.use_cache:
|
|
303
|
+
self._cache.set(cache_key, result)
|
|
304
|
+
|
|
305
|
+
return result
|
|
306
|
+
|
|
307
|
+
def get_statistic_item_list(
|
|
308
|
+
self,
|
|
309
|
+
stat_code: str,
|
|
310
|
+
start: int = 1,
|
|
311
|
+
end: int = 10000,
|
|
312
|
+
) -> dict[str, Any]:
|
|
313
|
+
"""
|
|
314
|
+
통계 항목 목록 조회 (StatisticItemList)
|
|
315
|
+
|
|
316
|
+
Parameters
|
|
317
|
+
----------
|
|
318
|
+
stat_code : str
|
|
319
|
+
통계표코드
|
|
320
|
+
start : int, optional
|
|
321
|
+
시작 건수, 기본값 1
|
|
322
|
+
end : int, optional
|
|
323
|
+
종료 건수, 기본값 10000
|
|
324
|
+
|
|
325
|
+
Returns
|
|
326
|
+
-------
|
|
327
|
+
dict
|
|
328
|
+
API 응답 데이터
|
|
329
|
+
"""
|
|
330
|
+
url = self._build_url("StatisticItemList", start, end, stat_code)
|
|
331
|
+
return self._make_request(url)
|
|
332
|
+
|
|
333
|
+
def get_statistic_table_list(
|
|
334
|
+
self,
|
|
335
|
+
stat_code: str = "",
|
|
336
|
+
start: int = 1,
|
|
337
|
+
end: int = 10000,
|
|
338
|
+
) -> dict[str, Any]:
|
|
339
|
+
"""
|
|
340
|
+
통계표 목록 조회 (StatisticTableList)
|
|
341
|
+
|
|
342
|
+
Parameters
|
|
343
|
+
----------
|
|
344
|
+
stat_code : str, optional
|
|
345
|
+
통계표코드 (검색어로 활용)
|
|
346
|
+
start : int, optional
|
|
347
|
+
시작 건수, 기본값 1
|
|
348
|
+
end : int, optional
|
|
349
|
+
종료 건수, 기본값 10000
|
|
350
|
+
|
|
351
|
+
Returns
|
|
352
|
+
-------
|
|
353
|
+
dict
|
|
354
|
+
API 응답 데이터
|
|
355
|
+
"""
|
|
356
|
+
url = self._build_url("StatisticTableList", start, end, stat_code)
|
|
357
|
+
return self._make_request(url)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
# 전역 클라이언트 인스턴스
|
|
361
|
+
_global_client: EcosClient | None = None
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def get_client() -> EcosClient:
|
|
365
|
+
"""전역 클라이언트 인스턴스를 반환합니다."""
|
|
366
|
+
global _global_client
|
|
367
|
+
if _global_client is None:
|
|
368
|
+
_global_client = EcosClient()
|
|
369
|
+
return _global_client
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def set_client(client: EcosClient | None) -> None:
|
|
373
|
+
"""
|
|
374
|
+
전역 기본 클라이언트를 설정합니다.
|
|
375
|
+
|
|
376
|
+
Notes
|
|
377
|
+
-----
|
|
378
|
+
- indicator 함수들은 기본적으로 이 전역 클라이언트를 사용합니다.
|
|
379
|
+
- 기존 전역 클라이언트가 존재하면 세션을 close()한 뒤 교체합니다.
|
|
380
|
+
"""
|
|
381
|
+
global _global_client
|
|
382
|
+
if _global_client is not None and _global_client is not client:
|
|
383
|
+
with contextlib.suppress(Exception):
|
|
384
|
+
_global_client.session.close()
|
|
385
|
+
_global_client = client
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def reset_client() -> None:
|
|
389
|
+
"""전역 클라이언트를 초기화합니다."""
|
|
390
|
+
global _global_client
|
|
391
|
+
if _global_client is not None:
|
|
392
|
+
with contextlib.suppress(Exception):
|
|
393
|
+
_global_client.session.close()
|
|
394
|
+
_global_client = None
|