yeonjae-universal-http-api-client 1.0.1__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.
- universal_http_api_client/__init__.py +35 -0
- universal_http_api_client/adapters.py +314 -0
- universal_http_api_client/client.py +572 -0
- universal_http_api_client/exceptions.py +209 -0
- universal_http_api_client/models.py +200 -0
- universal_http_api_client/py.typed +0 -0
- universal_http_api_client/utils.py +40 -0
- yeonjae_universal_http_api_client-1.0.1.dist-info/METADATA +77 -0
- yeonjae_universal_http_api_client-1.0.1.dist-info/RECORD +11 -0
- yeonjae_universal_http_api_client-1.0.1.dist-info/WHEEL +5 -0
- yeonjae_universal_http_api_client-1.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,572 @@
|
|
1
|
+
"""
|
2
|
+
HTTPAPIClient 핵심 클라이언트 구현
|
3
|
+
"""
|
4
|
+
|
5
|
+
import asyncio
|
6
|
+
import json
|
7
|
+
import logging
|
8
|
+
import time
|
9
|
+
from typing import Dict, Any, Optional, Callable, Union
|
10
|
+
from datetime import datetime, timedelta, timezone
|
11
|
+
|
12
|
+
import aiohttp
|
13
|
+
import requests
|
14
|
+
from requests.adapters import HTTPAdapter
|
15
|
+
from urllib3.util.retry import Retry
|
16
|
+
|
17
|
+
from .models import (
|
18
|
+
Platform, HTTPMethod, APIRequest, APIResponse,
|
19
|
+
RateLimitInfo, CacheKey, CachedResponse
|
20
|
+
)
|
21
|
+
from .adapters import AdapterFactory, PlatformAPIAdapter
|
22
|
+
from .exceptions import (
|
23
|
+
APIError, RateLimitError, AuthenticationError, NetworkError,
|
24
|
+
TimeoutError, handle_http_error
|
25
|
+
)
|
26
|
+
from .utils import ModuleIOLogger
|
27
|
+
|
28
|
+
|
29
|
+
class RateLimiter:
|
30
|
+
"""Rate Limiter 구현"""
|
31
|
+
|
32
|
+
def __init__(self):
|
33
|
+
self._limits: Dict[str, RateLimitInfo] = {}
|
34
|
+
self._requests: Dict[str, list] = {}
|
35
|
+
|
36
|
+
def can_make_request(self, platform: str, current_limit: Optional[RateLimitInfo] = None) -> bool:
|
37
|
+
"""요청 가능 여부 확인"""
|
38
|
+
if current_limit and current_limit.is_exhausted:
|
39
|
+
return False
|
40
|
+
|
41
|
+
# 플랫폼별 요청 이력 확인
|
42
|
+
now = time.time()
|
43
|
+
platform_requests = self._requests.get(platform, [])
|
44
|
+
|
45
|
+
# 1분 전 요청들 제거
|
46
|
+
recent_requests = [req_time for req_time in platform_requests if now - req_time < 60]
|
47
|
+
self._requests[platform] = recent_requests
|
48
|
+
|
49
|
+
# 분당 요청 제한 확인 (예: 60 요청/분)
|
50
|
+
return len(recent_requests) < 60
|
51
|
+
|
52
|
+
def record_request(self, platform: str):
|
53
|
+
"""요청 기록"""
|
54
|
+
now = time.time()
|
55
|
+
if platform not in self._requests:
|
56
|
+
self._requests[platform] = []
|
57
|
+
self._requests[platform].append(now)
|
58
|
+
|
59
|
+
def update_limits(self, platform: str, limit_info: RateLimitInfo):
|
60
|
+
"""Rate limit 정보 업데이트"""
|
61
|
+
self._limits[platform] = limit_info
|
62
|
+
|
63
|
+
def get_wait_time(self, platform: str) -> float:
|
64
|
+
"""대기 시간 계산"""
|
65
|
+
limit_info = self._limits.get(platform)
|
66
|
+
if limit_info and limit_info.is_exhausted:
|
67
|
+
return (limit_info.reset_time - datetime.now()).total_seconds()
|
68
|
+
return 0
|
69
|
+
|
70
|
+
|
71
|
+
class SimpleCache:
|
72
|
+
"""간단한 인메모리 캐시"""
|
73
|
+
|
74
|
+
def __init__(self, max_size: int = 1000):
|
75
|
+
self._cache: Dict[str, CachedResponse] = {}
|
76
|
+
self._max_size = max_size
|
77
|
+
|
78
|
+
def get(self, key: str) -> Optional[APIResponse]:
|
79
|
+
"""캐시에서 응답 조회"""
|
80
|
+
cached = self._cache.get(key)
|
81
|
+
if cached and not cached.is_expired:
|
82
|
+
response = cached.response
|
83
|
+
response.cached = True
|
84
|
+
return response
|
85
|
+
elif cached:
|
86
|
+
# 만료된 캐시 제거
|
87
|
+
del self._cache[key]
|
88
|
+
return None
|
89
|
+
|
90
|
+
def set(self, key: str, response: APIResponse, ttl: int = 300):
|
91
|
+
"""캐시에 응답 저장"""
|
92
|
+
if len(self._cache) >= self._max_size:
|
93
|
+
# 가장 오래된 항목 제거
|
94
|
+
oldest_key = min(self._cache.keys(), key=lambda k: self._cache[k].cached_at)
|
95
|
+
del self._cache[oldest_key]
|
96
|
+
|
97
|
+
self._cache[key] = CachedResponse(
|
98
|
+
response=response,
|
99
|
+
cached_at=datetime.now(timezone.utc),
|
100
|
+
ttl=ttl
|
101
|
+
)
|
102
|
+
|
103
|
+
def clear(self):
|
104
|
+
"""캐시 초기화"""
|
105
|
+
self._cache.clear()
|
106
|
+
|
107
|
+
|
108
|
+
class HTTPAPIClient:
|
109
|
+
"""범용 HTTP API 클라이언트"""
|
110
|
+
|
111
|
+
def __init__(
|
112
|
+
self,
|
113
|
+
platform: Platform,
|
114
|
+
auth_token: str,
|
115
|
+
enable_cache: bool = True,
|
116
|
+
enable_rate_limiting: bool = True,
|
117
|
+
max_retries: int = 3,
|
118
|
+
timeout: int = 30,
|
119
|
+
session: Optional[requests.Session] = None
|
120
|
+
):
|
121
|
+
self.platform = platform
|
122
|
+
self.auth_token = auth_token
|
123
|
+
self.enable_cache = enable_cache
|
124
|
+
self.enable_rate_limiting = enable_rate_limiting
|
125
|
+
self.max_retries = max_retries
|
126
|
+
self.timeout = timeout
|
127
|
+
|
128
|
+
# 어댑터 초기화
|
129
|
+
self.adapter = AdapterFactory.create_adapter(platform, auth_token)
|
130
|
+
|
131
|
+
# Rate Limiter 초기화
|
132
|
+
self.rate_limiter = RateLimiter() if enable_rate_limiting else None
|
133
|
+
|
134
|
+
# 캐시 초기화
|
135
|
+
self.cache = SimpleCache() if enable_cache else None
|
136
|
+
|
137
|
+
# HTTP 세션 설정
|
138
|
+
self.session = session or self._create_session()
|
139
|
+
|
140
|
+
# 로거 설정
|
141
|
+
self.logger = logging.getLogger(f"HTTPAPIClient.{platform.value}")
|
142
|
+
|
143
|
+
# 입출력 로거 설정
|
144
|
+
self.io_logger = ModuleIOLogger("HTTPAPIClient")
|
145
|
+
|
146
|
+
def _create_session(self) -> requests.Session:
|
147
|
+
"""HTTP 세션 생성"""
|
148
|
+
session = requests.Session()
|
149
|
+
|
150
|
+
# 재시도 정책 설정
|
151
|
+
retry_strategy = Retry(
|
152
|
+
total=self.max_retries,
|
153
|
+
status_forcelist=[429, 500, 502, 503, 504],
|
154
|
+
backoff_factor=1,
|
155
|
+
respect_retry_after_header=True
|
156
|
+
)
|
157
|
+
|
158
|
+
adapter = HTTPAdapter(max_retries=retry_strategy)
|
159
|
+
session.mount("http://", adapter)
|
160
|
+
session.mount("https://", adapter)
|
161
|
+
|
162
|
+
return session
|
163
|
+
|
164
|
+
def request(
|
165
|
+
self,
|
166
|
+
method: HTTPMethod,
|
167
|
+
endpoint: str,
|
168
|
+
params: Optional[Dict[str, Any]] = None,
|
169
|
+
data: Optional[Dict[str, Any]] = None,
|
170
|
+
headers: Optional[Dict[str, str]] = None,
|
171
|
+
timeout: Optional[int] = None,
|
172
|
+
cache_ttl: int = 300,
|
173
|
+
operation: str = "generic"
|
174
|
+
) -> APIResponse:
|
175
|
+
"""HTTP 요청 실행"""
|
176
|
+
start_time = time.time()
|
177
|
+
|
178
|
+
# API 요청 객체 생성
|
179
|
+
api_request = APIRequest(
|
180
|
+
platform=self.platform,
|
181
|
+
method=method,
|
182
|
+
endpoint=endpoint,
|
183
|
+
headers=headers,
|
184
|
+
params=params,
|
185
|
+
data=data,
|
186
|
+
timeout=timeout or self.timeout
|
187
|
+
)
|
188
|
+
|
189
|
+
try:
|
190
|
+
# 캐시 확인 (GET 요청만)
|
191
|
+
if method == HTTPMethod.GET and self.cache:
|
192
|
+
cache_key = self._get_cache_key(endpoint, params)
|
193
|
+
cached_response = self.cache.get(cache_key)
|
194
|
+
if cached_response:
|
195
|
+
self.logger.debug(f"Cache hit for {endpoint}")
|
196
|
+
return cached_response
|
197
|
+
|
198
|
+
# Rate limit 확인
|
199
|
+
if self.rate_limiter and not self.rate_limiter.can_make_request(self.platform.value):
|
200
|
+
wait_time = self.rate_limiter.get_wait_time(self.platform.value)
|
201
|
+
if wait_time > 0:
|
202
|
+
raise RateLimitError(
|
203
|
+
f"Rate limit exceeded, wait {wait_time:.1f}s",
|
204
|
+
retry_after=int(wait_time),
|
205
|
+
platform=self.platform.value
|
206
|
+
)
|
207
|
+
|
208
|
+
# 실제 HTTP 요청 실행
|
209
|
+
response = self._execute_request(api_request)
|
210
|
+
|
211
|
+
# Rate limit 정보 업데이트
|
212
|
+
if self.rate_limiter:
|
213
|
+
rate_limit_info = self.adapter.parse_rate_limit(response.headers)
|
214
|
+
if rate_limit_info:
|
215
|
+
self.rate_limiter.update_limits(self.platform.value, rate_limit_info)
|
216
|
+
|
217
|
+
# Rate limit 임계값 경고
|
218
|
+
if rate_limit_info.is_near_limit():
|
219
|
+
self.logger.warning(
|
220
|
+
f"Rate limit warning: {rate_limit_info.remaining} requests remaining"
|
221
|
+
)
|
222
|
+
|
223
|
+
self.rate_limiter.record_request(self.platform.value)
|
224
|
+
|
225
|
+
# 응답 파싱
|
226
|
+
parsed_data = self.adapter.parse_response(response.data, operation)
|
227
|
+
response.data = parsed_data
|
228
|
+
|
229
|
+
# 응답 시간 기록
|
230
|
+
response.response_time = time.time() - start_time
|
231
|
+
|
232
|
+
# 캐시 저장 (GET 요청 성공시만)
|
233
|
+
if (method == HTTPMethod.GET and self.cache and
|
234
|
+
response.success and 200 <= response.status_code < 300):
|
235
|
+
cache_key = self._get_cache_key(endpoint, params)
|
236
|
+
self.cache.set(cache_key, response, cache_ttl)
|
237
|
+
|
238
|
+
self.logger.info(
|
239
|
+
f"{method.value} {endpoint} - {response.status_code} "
|
240
|
+
f"({response.response_time:.2f}s)"
|
241
|
+
)
|
242
|
+
|
243
|
+
return response
|
244
|
+
|
245
|
+
except Exception as e:
|
246
|
+
error_time = time.time() - start_time
|
247
|
+
self.logger.error(
|
248
|
+
f"{method.value} {endpoint} failed ({error_time:.2f}s): {str(e)}"
|
249
|
+
)
|
250
|
+
raise
|
251
|
+
|
252
|
+
def _execute_request(self, api_request: APIRequest) -> APIResponse:
|
253
|
+
"""실제 HTTP 요청 실행"""
|
254
|
+
url = self.adapter.build_url(api_request.endpoint)
|
255
|
+
|
256
|
+
# 헤더 준비
|
257
|
+
headers = self.adapter.get_default_headers()
|
258
|
+
if api_request.headers:
|
259
|
+
headers.update(api_request.headers)
|
260
|
+
|
261
|
+
try:
|
262
|
+
# requests 라이브러리로 요청 실행
|
263
|
+
response = self.session.request(
|
264
|
+
method=api_request.method.value,
|
265
|
+
url=url,
|
266
|
+
params=api_request.params,
|
267
|
+
json=api_request.data if api_request.data else None,
|
268
|
+
headers=headers,
|
269
|
+
timeout=api_request.timeout
|
270
|
+
)
|
271
|
+
|
272
|
+
# 응답 데이터 파싱
|
273
|
+
try:
|
274
|
+
response_data = response.json() if response.content else {}
|
275
|
+
except json.JSONDecodeError:
|
276
|
+
response_data = {"raw_content": response.text}
|
277
|
+
|
278
|
+
# 성공 응답
|
279
|
+
if response.ok:
|
280
|
+
return APIResponse.success_response(
|
281
|
+
status_code=response.status_code,
|
282
|
+
data=response_data,
|
283
|
+
headers=dict(response.headers)
|
284
|
+
)
|
285
|
+
|
286
|
+
# 에러 응답
|
287
|
+
error_message = self._extract_error_message(response_data, response.text)
|
288
|
+
raise handle_http_error(response.status_code, error_message, self.platform.value)
|
289
|
+
|
290
|
+
except requests.exceptions.Timeout:
|
291
|
+
raise TimeoutError(
|
292
|
+
f"Request timeout after {api_request.timeout}s",
|
293
|
+
timeout=api_request.timeout,
|
294
|
+
platform=self.platform.value
|
295
|
+
)
|
296
|
+
except requests.exceptions.ConnectionError as e:
|
297
|
+
raise NetworkError(
|
298
|
+
f"Network connection failed: {str(e)}",
|
299
|
+
platform=self.platform.value
|
300
|
+
)
|
301
|
+
except requests.exceptions.RequestException as e:
|
302
|
+
raise APIError(
|
303
|
+
f"Request failed: {str(e)}",
|
304
|
+
platform=self.platform.value
|
305
|
+
)
|
306
|
+
|
307
|
+
def _extract_error_message(self, response_data: Dict[str, Any], raw_text: str) -> str:
|
308
|
+
"""에러 메시지 추출"""
|
309
|
+
# 플랫폼별 에러 메시지 형식에 맞게 추출
|
310
|
+
if isinstance(response_data, dict):
|
311
|
+
# GitHub 스타일
|
312
|
+
if "message" in response_data:
|
313
|
+
return response_data["message"]
|
314
|
+
|
315
|
+
# GitLab 스타일
|
316
|
+
if "error" in response_data:
|
317
|
+
error = response_data["error"]
|
318
|
+
if isinstance(error, str):
|
319
|
+
return error
|
320
|
+
elif isinstance(error, dict) and "message" in error:
|
321
|
+
return error["message"]
|
322
|
+
|
323
|
+
# 일반적인 형식들
|
324
|
+
for key in ["error_description", "detail", "details"]:
|
325
|
+
if key in response_data:
|
326
|
+
return str(response_data[key])
|
327
|
+
|
328
|
+
return raw_text[:200] if raw_text else "Unknown error"
|
329
|
+
|
330
|
+
def _get_cache_key(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> str:
|
331
|
+
"""캐시 키 생성"""
|
332
|
+
return self.adapter.get_cache_key(endpoint, params)
|
333
|
+
|
334
|
+
# 편의 메서드들
|
335
|
+
def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None, **kwargs) -> APIResponse:
|
336
|
+
"""GET 요청"""
|
337
|
+
return self.request(HTTPMethod.GET, endpoint, params=params, **kwargs)
|
338
|
+
|
339
|
+
def post(self, endpoint: str, data: Optional[Dict[str, Any]] = None, **kwargs) -> APIResponse:
|
340
|
+
"""POST 요청"""
|
341
|
+
return self.request(HTTPMethod.POST, endpoint, data=data, **kwargs)
|
342
|
+
|
343
|
+
def put(self, endpoint: str, data: Optional[Dict[str, Any]] = None, **kwargs) -> APIResponse:
|
344
|
+
"""PUT 요청"""
|
345
|
+
return self.request(HTTPMethod.PUT, endpoint, data=data, **kwargs)
|
346
|
+
|
347
|
+
def delete(self, endpoint: str, **kwargs) -> APIResponse:
|
348
|
+
"""DELETE 요청"""
|
349
|
+
return self.request(HTTPMethod.DELETE, endpoint, **kwargs)
|
350
|
+
|
351
|
+
def patch(self, endpoint: str, data: Optional[Dict[str, Any]] = None, **kwargs) -> APIResponse:
|
352
|
+
"""PATCH 요청"""
|
353
|
+
return self.request(HTTPMethod.PATCH, endpoint, data=data, **kwargs)
|
354
|
+
|
355
|
+
# 플랫폼별 특화 메서드들
|
356
|
+
def get_commit(self, repo_name: str, commit_sha: str) -> APIResponse:
|
357
|
+
"""커밋 정보 조회"""
|
358
|
+
# 입력 로깅
|
359
|
+
self.io_logger.log_input(
|
360
|
+
"get_commit",
|
361
|
+
data={"repo_name": repo_name, "commit_sha": commit_sha},
|
362
|
+
metadata={"platform": self.platform.value}
|
363
|
+
)
|
364
|
+
|
365
|
+
try:
|
366
|
+
if self.platform == Platform.GITHUB:
|
367
|
+
endpoint = f"/repos/{repo_name}/commits/{commit_sha}"
|
368
|
+
elif self.platform == Platform.GITLAB:
|
369
|
+
# GitLab에서는 프로젝트 ID 필요
|
370
|
+
endpoint = f"/projects/{repo_name.replace('/', '%2F')}/commits/{commit_sha}"
|
371
|
+
else:
|
372
|
+
raise APIError(f"get_commit not implemented for {self.platform.value}")
|
373
|
+
|
374
|
+
response = self.get(endpoint, operation="get_commit")
|
375
|
+
|
376
|
+
# 출력 로깅
|
377
|
+
self.io_logger.log_output(
|
378
|
+
"get_commit",
|
379
|
+
data=response.data,
|
380
|
+
metadata={
|
381
|
+
"platform": self.platform.value,
|
382
|
+
"status_code": response.status_code,
|
383
|
+
"success": response.success,
|
384
|
+
"commit_sha": commit_sha,
|
385
|
+
"files_changed": len(response.data.get("files", [])) if response.data else 0
|
386
|
+
}
|
387
|
+
)
|
388
|
+
|
389
|
+
return response
|
390
|
+
|
391
|
+
except Exception as e:
|
392
|
+
# 오류 로깅
|
393
|
+
self.io_logger.log_error(
|
394
|
+
"get_commit",
|
395
|
+
e,
|
396
|
+
metadata={
|
397
|
+
"platform": self.platform.value,
|
398
|
+
"repo_name": repo_name,
|
399
|
+
"commit_sha": commit_sha
|
400
|
+
}
|
401
|
+
)
|
402
|
+
raise
|
403
|
+
|
404
|
+
def get_repository(self, repo_name: str) -> APIResponse:
|
405
|
+
"""저장소 정보 조회"""
|
406
|
+
if self.platform == Platform.GITHUB:
|
407
|
+
endpoint = f"/repos/{repo_name}"
|
408
|
+
operation = "get_repository"
|
409
|
+
elif self.platform == Platform.GITLAB:
|
410
|
+
endpoint = f"/projects/{repo_name.replace('/', '%2F')}"
|
411
|
+
operation = "get_project"
|
412
|
+
else:
|
413
|
+
raise APIError(f"get_repository not implemented for {self.platform.value}")
|
414
|
+
|
415
|
+
return self.get(endpoint, operation=operation)
|
416
|
+
|
417
|
+
def get_diff(self, repo_name: str, commit_sha: str) -> APIResponse:
|
418
|
+
"""커밋 diff 조회"""
|
419
|
+
if self.platform == Platform.GITHUB:
|
420
|
+
endpoint = f"/repos/{repo_name}/commits/{commit_sha}"
|
421
|
+
elif self.platform == Platform.GITLAB:
|
422
|
+
endpoint = f"/projects/{repo_name.replace('/', '%2F')}/commits/{commit_sha}/diff"
|
423
|
+
else:
|
424
|
+
raise APIError(f"get_diff not implemented for {self.platform.value}")
|
425
|
+
|
426
|
+
return self.get(endpoint, operation="get_diff")
|
427
|
+
|
428
|
+
def close(self):
|
429
|
+
"""클라이언트 정리"""
|
430
|
+
if self.session:
|
431
|
+
self.session.close()
|
432
|
+
if self.cache:
|
433
|
+
self.cache.clear()
|
434
|
+
|
435
|
+
|
436
|
+
class AsyncHTTPAPIClient:
|
437
|
+
"""비동기 HTTP API 클라이언트"""
|
438
|
+
|
439
|
+
def __init__(
|
440
|
+
self,
|
441
|
+
platform: Platform,
|
442
|
+
auth_token: str,
|
443
|
+
enable_cache: bool = True,
|
444
|
+
enable_rate_limiting: bool = True,
|
445
|
+
max_retries: int = 3,
|
446
|
+
timeout: int = 30
|
447
|
+
):
|
448
|
+
self.platform = platform
|
449
|
+
self.auth_token = auth_token
|
450
|
+
self.enable_cache = enable_cache
|
451
|
+
self.enable_rate_limiting = enable_rate_limiting
|
452
|
+
self.max_retries = max_retries
|
453
|
+
self.timeout = timeout
|
454
|
+
|
455
|
+
# 어댑터 초기화
|
456
|
+
self.adapter = AdapterFactory.create_adapter(platform, auth_token)
|
457
|
+
|
458
|
+
# Rate Limiter 초기화
|
459
|
+
self.rate_limiter = RateLimiter() if enable_rate_limiting else None
|
460
|
+
|
461
|
+
# 캐시 초기화
|
462
|
+
self.cache = SimpleCache() if enable_cache else None
|
463
|
+
|
464
|
+
# 로거 설정
|
465
|
+
self.logger = logging.getLogger(f"AsyncHTTPAPIClient.{platform.value}")
|
466
|
+
|
467
|
+
async def request(
|
468
|
+
self,
|
469
|
+
method: HTTPMethod,
|
470
|
+
endpoint: str,
|
471
|
+
params: Optional[Dict[str, Any]] = None,
|
472
|
+
data: Optional[Dict[str, Any]] = None,
|
473
|
+
headers: Optional[Dict[str, str]] = None,
|
474
|
+
timeout: Optional[int] = None,
|
475
|
+
cache_ttl: int = 300,
|
476
|
+
operation: str = "generic"
|
477
|
+
) -> APIResponse:
|
478
|
+
"""비동기 HTTP 요청 실행"""
|
479
|
+
start_time = time.time()
|
480
|
+
|
481
|
+
# 캐시 확인 (GET 요청만)
|
482
|
+
if method == HTTPMethod.GET and self.cache:
|
483
|
+
cache_key = self.adapter.get_cache_key(endpoint, params)
|
484
|
+
cached_response = self.cache.get(cache_key)
|
485
|
+
if cached_response:
|
486
|
+
self.logger.debug(f"Cache hit for {endpoint}")
|
487
|
+
return cached_response
|
488
|
+
|
489
|
+
url = self.adapter.build_url(endpoint)
|
490
|
+
request_headers = self.adapter.get_default_headers()
|
491
|
+
if headers:
|
492
|
+
request_headers.update(headers)
|
493
|
+
|
494
|
+
timeout_config = aiohttp.ClientTimeout(total=timeout or self.timeout)
|
495
|
+
|
496
|
+
try:
|
497
|
+
async with aiohttp.ClientSession(timeout=timeout_config) as session:
|
498
|
+
async with session.request(
|
499
|
+
method=method.value,
|
500
|
+
url=url,
|
501
|
+
params=params,
|
502
|
+
json=data,
|
503
|
+
headers=request_headers
|
504
|
+
) as response:
|
505
|
+
|
506
|
+
# 응답 데이터 파싱
|
507
|
+
try:
|
508
|
+
response_data = await response.json()
|
509
|
+
except:
|
510
|
+
response_text = await response.text()
|
511
|
+
response_data = {"raw_content": response_text}
|
512
|
+
|
513
|
+
response_time = time.time() - start_time
|
514
|
+
|
515
|
+
if response.ok:
|
516
|
+
# 응답 파싱
|
517
|
+
parsed_data = self.adapter.parse_response(response_data, operation)
|
518
|
+
|
519
|
+
api_response = APIResponse.success_response(
|
520
|
+
status_code=response.status,
|
521
|
+
data=parsed_data,
|
522
|
+
headers=dict(response.headers),
|
523
|
+
response_time=response_time
|
524
|
+
)
|
525
|
+
|
526
|
+
# 캐시 저장
|
527
|
+
if (method == HTTPMethod.GET and self.cache and
|
528
|
+
200 <= response.status < 300):
|
529
|
+
cache_key = self.adapter.get_cache_key(endpoint, params)
|
530
|
+
self.cache.set(cache_key, api_response, cache_ttl)
|
531
|
+
|
532
|
+
return api_response
|
533
|
+
else:
|
534
|
+
error_message = self._extract_error_message(response_data, "")
|
535
|
+
raise handle_http_error(response.status, error_message, self.platform.value)
|
536
|
+
|
537
|
+
except asyncio.TimeoutError:
|
538
|
+
raise TimeoutError(
|
539
|
+
f"Request timeout after {timeout or self.timeout}s",
|
540
|
+
timeout=timeout or self.timeout,
|
541
|
+
platform=self.platform.value
|
542
|
+
)
|
543
|
+
except aiohttp.ClientError as e:
|
544
|
+
raise NetworkError(
|
545
|
+
f"Network error: {str(e)}",
|
546
|
+
platform=self.platform.value
|
547
|
+
)
|
548
|
+
|
549
|
+
def _extract_error_message(self, response_data: Dict[str, Any], raw_text: str) -> str:
|
550
|
+
"""에러 메시지 추출 (동기 버전과 동일)"""
|
551
|
+
if isinstance(response_data, dict):
|
552
|
+
if "message" in response_data:
|
553
|
+
return response_data["message"]
|
554
|
+
if "error" in response_data:
|
555
|
+
error = response_data["error"]
|
556
|
+
if isinstance(error, str):
|
557
|
+
return error
|
558
|
+
elif isinstance(error, dict) and "message" in error:
|
559
|
+
return error["message"]
|
560
|
+
for key in ["error_description", "detail", "details"]:
|
561
|
+
if key in response_data:
|
562
|
+
return str(response_data[key])
|
563
|
+
return raw_text[:200] if raw_text else "Unknown error"
|
564
|
+
|
565
|
+
# 편의 메서드들 (동기 버전과 동일한 인터페이스)
|
566
|
+
async def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None, **kwargs) -> APIResponse:
|
567
|
+
"""비동기 GET 요청"""
|
568
|
+
return await self.request(HTTPMethod.GET, endpoint, params=params, **kwargs)
|
569
|
+
|
570
|
+
async def post(self, endpoint: str, data: Optional[Dict[str, Any]] = None, **kwargs) -> APIResponse:
|
571
|
+
"""비동기 POST 요청"""
|
572
|
+
return await self.request(HTTPMethod.POST, endpoint, data=data, **kwargs)
|