opendart-fss 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.
- opendart_fss/__init__.py +77 -0
- opendart_fss/_version.py +34 -0
- opendart_fss/api/__init__.py +19 -0
- opendart_fss/api/base.py +72 -0
- opendart_fss/api/disclosure.py +103 -0
- opendart_fss/api/financial.py +206 -0
- opendart_fss/api/major_event.py +1051 -0
- opendart_fss/api/registration.py +183 -0
- opendart_fss/api/report.py +821 -0
- opendart_fss/api/shareholder.py +51 -0
- opendart_fss/client.py +96 -0
- opendart_fss/constants.py +88 -0
- opendart_fss/exceptions.py +90 -0
- opendart_fss/models/__init__.py +124 -0
- opendart_fss/models/base.py +10 -0
- opendart_fss/models/disclosure.py +106 -0
- opendart_fss/models/financial.py +85 -0
- opendart_fss/models/major_event.py +863 -0
- opendart_fss/models/registration.py +186 -0
- opendart_fss/models/report.py +691 -0
- opendart_fss/models/shareholder.py +54 -0
- opendart_fss/verification/__init__.py +50 -0
- opendart_fss/verification/config.py +450 -0
- opendart_fss/verification/rate_limiter.py +92 -0
- opendart_fss/verification/reporter.py +255 -0
- opendart_fss/verification/runner.py +326 -0
- opendart_fss-0.1.0.dist-info/METADATA +308 -0
- opendart_fss-0.1.0.dist-info/RECORD +30 -0
- opendart_fss-0.1.0.dist-info/WHEEL +4 -0
- opendart_fss-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""검증 결과 리포트 생성."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
from opendart_fss.verification.config import (
|
|
9
|
+
CATEGORY_DESCRIPTIONS,
|
|
10
|
+
VerificationResult,
|
|
11
|
+
VerificationStatus,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class VerificationReport:
|
|
17
|
+
"""검증 리포트."""
|
|
18
|
+
|
|
19
|
+
timestamp: str
|
|
20
|
+
duration_seconds: float
|
|
21
|
+
total_endpoints: int
|
|
22
|
+
success_count: int
|
|
23
|
+
no_data_count: int
|
|
24
|
+
failed_count: int
|
|
25
|
+
skipped_count: int
|
|
26
|
+
results: list[VerificationResult] = field(default_factory=list)
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def success_rate(self) -> float:
|
|
30
|
+
"""성공률 (SUCCESS + NO_DATA)."""
|
|
31
|
+
if self.total_endpoints == 0:
|
|
32
|
+
return 0.0
|
|
33
|
+
return ((self.success_count + self.no_data_count) / self.total_endpoints) * 100
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def from_results(
|
|
37
|
+
cls,
|
|
38
|
+
results: list[VerificationResult],
|
|
39
|
+
duration_seconds: float,
|
|
40
|
+
) -> "VerificationReport":
|
|
41
|
+
"""검증 결과로부터 리포트 생성.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
results: 검증 결과 목록
|
|
45
|
+
duration_seconds: 총 소요 시간 (초)
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
검증 리포트
|
|
49
|
+
"""
|
|
50
|
+
success_count = sum(
|
|
51
|
+
1 for r in results if r.status == VerificationStatus.SUCCESS
|
|
52
|
+
)
|
|
53
|
+
no_data_count = sum(
|
|
54
|
+
1 for r in results if r.status == VerificationStatus.NO_DATA
|
|
55
|
+
)
|
|
56
|
+
failed_count = sum(1 for r in results if r.status == VerificationStatus.FAILED)
|
|
57
|
+
skipped_count = sum(
|
|
58
|
+
1 for r in results if r.status == VerificationStatus.SKIPPED
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return cls(
|
|
62
|
+
timestamp=datetime.now().isoformat(timespec="seconds"),
|
|
63
|
+
duration_seconds=duration_seconds,
|
|
64
|
+
total_endpoints=len(results),
|
|
65
|
+
success_count=success_count,
|
|
66
|
+
no_data_count=no_data_count,
|
|
67
|
+
failed_count=failed_count,
|
|
68
|
+
skipped_count=skipped_count,
|
|
69
|
+
results=results,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def generate_report(
|
|
74
|
+
results: list[VerificationResult],
|
|
75
|
+
duration_seconds: float,
|
|
76
|
+
format: Literal["console", "json", "markdown"] = "console",
|
|
77
|
+
) -> str:
|
|
78
|
+
"""검증 결과 리포트 생성.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
results: 검증 결과 목록
|
|
82
|
+
duration_seconds: 총 소요 시간 (초)
|
|
83
|
+
format: 출력 형식 (console/json/markdown)
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
포맷팅된 리포트 문자열
|
|
87
|
+
"""
|
|
88
|
+
report = VerificationReport.from_results(results, duration_seconds)
|
|
89
|
+
|
|
90
|
+
if format == "json":
|
|
91
|
+
return _generate_json(report)
|
|
92
|
+
elif format == "markdown":
|
|
93
|
+
return _generate_markdown(report)
|
|
94
|
+
else:
|
|
95
|
+
return _generate_console(report)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _generate_console(report: VerificationReport) -> str:
|
|
99
|
+
"""콘솔 출력 형식 리포트 생성."""
|
|
100
|
+
lines = [
|
|
101
|
+
"=" * 60,
|
|
102
|
+
"OpenDART API Endpoint Verification Report",
|
|
103
|
+
"=" * 60,
|
|
104
|
+
f"Timestamp: {report.timestamp}",
|
|
105
|
+
f"Duration: {report.duration_seconds:.1f}s",
|
|
106
|
+
"",
|
|
107
|
+
"Summary:",
|
|
108
|
+
f" Total Endpoints: {report.total_endpoints}",
|
|
109
|
+
f" Successful: {report.success_count} "
|
|
110
|
+
f"({report.success_count / report.total_endpoints * 100:.1f}%)"
|
|
111
|
+
if report.total_endpoints > 0
|
|
112
|
+
else " Successful: 0",
|
|
113
|
+
f" No Data: {report.no_data_count} "
|
|
114
|
+
f"({report.no_data_count / report.total_endpoints * 100:.1f}%)"
|
|
115
|
+
if report.total_endpoints > 0
|
|
116
|
+
else " No Data: 0",
|
|
117
|
+
f" Failed: {report.failed_count} "
|
|
118
|
+
f"({report.failed_count / report.total_endpoints * 100:.1f}%)"
|
|
119
|
+
if report.total_endpoints > 0
|
|
120
|
+
else " Failed: 0",
|
|
121
|
+
f" Skipped: {report.skipped_count} "
|
|
122
|
+
f"({report.skipped_count / report.total_endpoints * 100:.1f}%)"
|
|
123
|
+
if report.total_endpoints > 0
|
|
124
|
+
else " Skipped: 0",
|
|
125
|
+
"",
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
# 카테고리별 결과 출력
|
|
129
|
+
current_category = None
|
|
130
|
+
for result in report.results:
|
|
131
|
+
if result.category != current_category:
|
|
132
|
+
current_category = result.category
|
|
133
|
+
desc = CATEGORY_DESCRIPTIONS.get(current_category, "")
|
|
134
|
+
lines.append(f"{current_category} - {desc}:")
|
|
135
|
+
|
|
136
|
+
status_icon = _get_status_icon(result.status)
|
|
137
|
+
time_str = (
|
|
138
|
+
f"({result.response_time_ms:.0f}ms)" if result.response_time_ms else ""
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if result.error_message:
|
|
142
|
+
lines.append(f" [{status_icon}] {result.endpoint_name} {time_str}")
|
|
143
|
+
lines.append(f" {result.error_message}")
|
|
144
|
+
else:
|
|
145
|
+
lines.append(f" [{status_icon}] {result.endpoint_name} {time_str}")
|
|
146
|
+
|
|
147
|
+
lines.append("")
|
|
148
|
+
lines.append("=" * 60)
|
|
149
|
+
|
|
150
|
+
return "\n".join(lines)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _generate_json(report: VerificationReport) -> str:
|
|
154
|
+
"""JSON 형식 리포트 생성."""
|
|
155
|
+
data = {
|
|
156
|
+
"timestamp": report.timestamp,
|
|
157
|
+
"duration_seconds": report.duration_seconds,
|
|
158
|
+
"summary": {
|
|
159
|
+
"total": report.total_endpoints,
|
|
160
|
+
"success": report.success_count,
|
|
161
|
+
"no_data": report.no_data_count,
|
|
162
|
+
"failed": report.failed_count,
|
|
163
|
+
"skipped": report.skipped_count,
|
|
164
|
+
"success_rate": report.success_rate,
|
|
165
|
+
},
|
|
166
|
+
"results": [
|
|
167
|
+
{
|
|
168
|
+
"id": r.endpoint_id,
|
|
169
|
+
"name": r.endpoint_name,
|
|
170
|
+
"category": r.category,
|
|
171
|
+
"status": r.status.value,
|
|
172
|
+
"response_time_ms": r.response_time_ms,
|
|
173
|
+
"error_message": r.error_message,
|
|
174
|
+
"response_data": r.response_data,
|
|
175
|
+
}
|
|
176
|
+
for r in report.results
|
|
177
|
+
],
|
|
178
|
+
}
|
|
179
|
+
return json.dumps(data, indent=2, ensure_ascii=False)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _generate_markdown(report: VerificationReport) -> str:
|
|
183
|
+
"""마크다운 형식 리포트 생성."""
|
|
184
|
+
lines = [
|
|
185
|
+
"# OpenDART API Endpoint Verification Report",
|
|
186
|
+
"",
|
|
187
|
+
f"- **Timestamp:** {report.timestamp}",
|
|
188
|
+
f"- **Duration:** {report.duration_seconds:.1f}s",
|
|
189
|
+
"",
|
|
190
|
+
"## Summary",
|
|
191
|
+
"",
|
|
192
|
+
"| Metric | Count | Percentage |",
|
|
193
|
+
"|--------|-------|------------|",
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
if report.total_endpoints > 0:
|
|
197
|
+
lines.extend(
|
|
198
|
+
[
|
|
199
|
+
f"| Total | {report.total_endpoints} | 100% |",
|
|
200
|
+
f"| Success | {report.success_count} | "
|
|
201
|
+
f"{report.success_count / report.total_endpoints * 100:.1f}% |",
|
|
202
|
+
f"| No Data | {report.no_data_count} | "
|
|
203
|
+
f"{report.no_data_count / report.total_endpoints * 100:.1f}% |",
|
|
204
|
+
f"| Failed | {report.failed_count} | "
|
|
205
|
+
f"{report.failed_count / report.total_endpoints * 100:.1f}% |",
|
|
206
|
+
f"| Skipped | {report.skipped_count} | "
|
|
207
|
+
f"{report.skipped_count / report.total_endpoints * 100:.1f}% |",
|
|
208
|
+
]
|
|
209
|
+
)
|
|
210
|
+
else:
|
|
211
|
+
lines.append("| Total | 0 | 0% |")
|
|
212
|
+
|
|
213
|
+
lines.extend(["", "## Details", ""])
|
|
214
|
+
|
|
215
|
+
# 카테고리별 결과
|
|
216
|
+
current_category = None
|
|
217
|
+
for result in report.results:
|
|
218
|
+
if result.category != current_category:
|
|
219
|
+
current_category = result.category
|
|
220
|
+
desc = CATEGORY_DESCRIPTIONS.get(current_category, "")
|
|
221
|
+
lines.extend(["", f"### {current_category} - {desc}", ""])
|
|
222
|
+
lines.append("| Status | Endpoint | Time (ms) | Notes |")
|
|
223
|
+
lines.append("|--------|----------|-----------|-------|")
|
|
224
|
+
|
|
225
|
+
status_badge = _get_status_badge(result.status)
|
|
226
|
+
time_str = f"{result.response_time_ms:.0f}" if result.response_time_ms else "-"
|
|
227
|
+
notes = result.error_message or "-"
|
|
228
|
+
|
|
229
|
+
lines.append(
|
|
230
|
+
f"| {status_badge} | {result.endpoint_name} | {time_str} | {notes} |"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
return "\n".join(lines)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _get_status_icon(status: VerificationStatus) -> str:
|
|
237
|
+
"""상태에 따른 아이콘 반환."""
|
|
238
|
+
icons = {
|
|
239
|
+
VerificationStatus.SUCCESS: "OK",
|
|
240
|
+
VerificationStatus.NO_DATA: "NO_DATA",
|
|
241
|
+
VerificationStatus.FAILED: "FAIL",
|
|
242
|
+
VerificationStatus.SKIPPED: "SKIP",
|
|
243
|
+
}
|
|
244
|
+
return icons.get(status, "?")
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _get_status_badge(status: VerificationStatus) -> str:
|
|
248
|
+
"""상태에 따른 마크다운 배지 반환."""
|
|
249
|
+
badges = {
|
|
250
|
+
VerificationStatus.SUCCESS: "SUCCESS",
|
|
251
|
+
VerificationStatus.NO_DATA: "NO_DATA",
|
|
252
|
+
VerificationStatus.FAILED: "FAILED",
|
|
253
|
+
VerificationStatus.SKIPPED: "SKIPPED",
|
|
254
|
+
}
|
|
255
|
+
return badges.get(status, "UNKNOWN")
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"""엔드포인트 검증 실행기."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from opendart_fss.client import OpenDartClient
|
|
7
|
+
from opendart_fss.exceptions import (
|
|
8
|
+
APIError,
|
|
9
|
+
NotFoundError,
|
|
10
|
+
RateLimitError,
|
|
11
|
+
ValidationError,
|
|
12
|
+
)
|
|
13
|
+
from opendart_fss.verification.config import (
|
|
14
|
+
DEFAULT_TEST_DATA,
|
|
15
|
+
ENDPOINT_CONFIGS,
|
|
16
|
+
EndpointConfig,
|
|
17
|
+
VerificationResult,
|
|
18
|
+
VerificationStatus,
|
|
19
|
+
)
|
|
20
|
+
from opendart_fss.verification.rate_limiter import AdaptiveRateLimiter
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class EndpointVerifier:
|
|
24
|
+
"""OpenDART API 엔드포인트 검증기.
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
```python
|
|
28
|
+
async with EndpointVerifier() as verifier:
|
|
29
|
+
# 모든 엔드포인트 검증
|
|
30
|
+
results = await verifier.verify_all()
|
|
31
|
+
|
|
32
|
+
# 특정 카테고리만 검증
|
|
33
|
+
results = await verifier.verify_category("DS001")
|
|
34
|
+
|
|
35
|
+
# 특정 엔드포인트만 검증
|
|
36
|
+
result = await verifier.verify_endpoint("DS001-01")
|
|
37
|
+
```
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
api_key: str | None = None,
|
|
43
|
+
test_data: dict[str, str] | None = None,
|
|
44
|
+
max_retries: int = 3,
|
|
45
|
+
) -> None:
|
|
46
|
+
"""검증기 초기화.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
api_key: OpenDART API 키 (환경변수에서 자동 로드 가능)
|
|
50
|
+
test_data: 테스트 데이터 (기본값 사용 가능)
|
|
51
|
+
max_retries: Rate limit 시 최대 재시도 횟수
|
|
52
|
+
"""
|
|
53
|
+
self._api_key = api_key
|
|
54
|
+
self._test_data = {**DEFAULT_TEST_DATA, **(test_data or {})}
|
|
55
|
+
self._max_retries = max_retries
|
|
56
|
+
self._rate_limiter = AdaptiveRateLimiter()
|
|
57
|
+
self._client: OpenDartClient | None = None
|
|
58
|
+
self._rcept_no: str | None = None # 공시 검색에서 획득
|
|
59
|
+
|
|
60
|
+
async def __aenter__(self) -> "EndpointVerifier":
|
|
61
|
+
"""컨텍스트 매니저 진입."""
|
|
62
|
+
self._client = OpenDartClient(api_key=self._api_key)
|
|
63
|
+
await self._client.__aenter__()
|
|
64
|
+
return self
|
|
65
|
+
|
|
66
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
67
|
+
"""컨텍스트 매니저 종료."""
|
|
68
|
+
if self._client:
|
|
69
|
+
await self._client.__aexit__(*args)
|
|
70
|
+
|
|
71
|
+
async def verify_all(
|
|
72
|
+
self,
|
|
73
|
+
category: str | None = None,
|
|
74
|
+
) -> list[VerificationResult]:
|
|
75
|
+
"""모든 엔드포인트 검증.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
category: 특정 카테고리만 검증 (예: "DS001")
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
검증 결과 목록
|
|
82
|
+
"""
|
|
83
|
+
configs = ENDPOINT_CONFIGS
|
|
84
|
+
if category:
|
|
85
|
+
configs = [c for c in configs if c.category == category]
|
|
86
|
+
|
|
87
|
+
results: list[VerificationResult] = []
|
|
88
|
+
for config in configs:
|
|
89
|
+
result = await self._verify_with_retry(config)
|
|
90
|
+
results.append(result)
|
|
91
|
+
|
|
92
|
+
return results
|
|
93
|
+
|
|
94
|
+
async def verify_category(self, category: str) -> list[VerificationResult]:
|
|
95
|
+
"""특정 카테고리의 모든 엔드포인트 검증.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
category: 카테고리 ID (예: "DS001")
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
검증 결과 목록
|
|
102
|
+
"""
|
|
103
|
+
return await self.verify_all(category=category)
|
|
104
|
+
|
|
105
|
+
async def verify_endpoint(self, endpoint_id: str) -> VerificationResult:
|
|
106
|
+
"""특정 엔드포인트 검증.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
endpoint_id: 엔드포인트 ID (예: "DS001-01")
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
검증 결과
|
|
113
|
+
"""
|
|
114
|
+
config = next(
|
|
115
|
+
(c for c in ENDPOINT_CONFIGS if c.id == endpoint_id),
|
|
116
|
+
None,
|
|
117
|
+
)
|
|
118
|
+
if not config:
|
|
119
|
+
return VerificationResult(
|
|
120
|
+
endpoint_id=endpoint_id,
|
|
121
|
+
endpoint_name="unknown",
|
|
122
|
+
category="unknown",
|
|
123
|
+
status=VerificationStatus.FAILED,
|
|
124
|
+
error_message=f"Unknown endpoint: {endpoint_id}",
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
return await self._verify_with_retry(config)
|
|
128
|
+
|
|
129
|
+
async def _verify_with_retry(
|
|
130
|
+
self,
|
|
131
|
+
config: EndpointConfig,
|
|
132
|
+
) -> VerificationResult:
|
|
133
|
+
"""재시도 로직을 포함한 엔드포인트 검증.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
config: 엔드포인트 설정
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
검증 결과
|
|
140
|
+
"""
|
|
141
|
+
for attempt in range(self._max_retries + 1):
|
|
142
|
+
await self._rate_limiter.wait()
|
|
143
|
+
|
|
144
|
+
result = await self._verify_single(config)
|
|
145
|
+
|
|
146
|
+
if result.status == VerificationStatus.FAILED:
|
|
147
|
+
if "rate limit" in (result.error_message or "").lower():
|
|
148
|
+
self._rate_limiter.on_rate_limit()
|
|
149
|
+
if attempt < self._max_retries:
|
|
150
|
+
continue
|
|
151
|
+
result.error_message = (
|
|
152
|
+
f"{result.error_message} (after {attempt + 1} retries)"
|
|
153
|
+
)
|
|
154
|
+
else:
|
|
155
|
+
self._rate_limiter.on_success()
|
|
156
|
+
|
|
157
|
+
return result
|
|
158
|
+
|
|
159
|
+
return result
|
|
160
|
+
|
|
161
|
+
async def _verify_single(self, config: EndpointConfig) -> VerificationResult:
|
|
162
|
+
"""단일 엔드포인트 검증.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
config: 엔드포인트 설정
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
검증 결과
|
|
169
|
+
"""
|
|
170
|
+
if not self._client:
|
|
171
|
+
return VerificationResult(
|
|
172
|
+
endpoint_id=config.id,
|
|
173
|
+
endpoint_name=config.name,
|
|
174
|
+
category=config.category,
|
|
175
|
+
status=VerificationStatus.FAILED,
|
|
176
|
+
error_message="Client not initialized. Use async with.",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# rcept_no가 필요한데 없으면 스킵
|
|
180
|
+
if config.requires_rcept_no and not self._rcept_no:
|
|
181
|
+
return VerificationResult(
|
|
182
|
+
endpoint_id=config.id,
|
|
183
|
+
endpoint_name=config.name,
|
|
184
|
+
category=config.category,
|
|
185
|
+
status=VerificationStatus.SKIPPED,
|
|
186
|
+
error_message="Requires rcept_no from disclosure.search",
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# 파라미터 치환
|
|
190
|
+
params = self._resolve_params(config.params)
|
|
191
|
+
|
|
192
|
+
start_time = time.perf_counter()
|
|
193
|
+
try:
|
|
194
|
+
# 서비스와 메서드 가져오기
|
|
195
|
+
service = getattr(self._client, config.service)
|
|
196
|
+
method = getattr(service, config.method)
|
|
197
|
+
|
|
198
|
+
# API 호출
|
|
199
|
+
result = await method(**params)
|
|
200
|
+
elapsed_ms = (time.perf_counter() - start_time) * 1000
|
|
201
|
+
|
|
202
|
+
# 결과 분석
|
|
203
|
+
status = self._analyze_result(result)
|
|
204
|
+
|
|
205
|
+
# 공시 검색 결과에서 rcept_no 저장
|
|
206
|
+
if config.method == "search" and result:
|
|
207
|
+
self._rcept_no = result[0].rcept_no
|
|
208
|
+
|
|
209
|
+
return VerificationResult(
|
|
210
|
+
endpoint_id=config.id,
|
|
211
|
+
endpoint_name=config.name,
|
|
212
|
+
category=config.category,
|
|
213
|
+
status=status,
|
|
214
|
+
response_time_ms=elapsed_ms,
|
|
215
|
+
response_data=self._summarize_result(result),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
except NotFoundError as e:
|
|
219
|
+
elapsed_ms = (time.perf_counter() - start_time) * 1000
|
|
220
|
+
return VerificationResult(
|
|
221
|
+
endpoint_id=config.id,
|
|
222
|
+
endpoint_name=config.name,
|
|
223
|
+
category=config.category,
|
|
224
|
+
status=VerificationStatus.NO_DATA,
|
|
225
|
+
response_time_ms=elapsed_ms,
|
|
226
|
+
error_message=str(e),
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
except RateLimitError as e:
|
|
230
|
+
elapsed_ms = (time.perf_counter() - start_time) * 1000
|
|
231
|
+
return VerificationResult(
|
|
232
|
+
endpoint_id=config.id,
|
|
233
|
+
endpoint_name=config.name,
|
|
234
|
+
category=config.category,
|
|
235
|
+
status=VerificationStatus.FAILED,
|
|
236
|
+
response_time_ms=elapsed_ms,
|
|
237
|
+
error_message=f"Rate limit: {e}",
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
except ValidationError as e:
|
|
241
|
+
elapsed_ms = (time.perf_counter() - start_time) * 1000
|
|
242
|
+
return VerificationResult(
|
|
243
|
+
endpoint_id=config.id,
|
|
244
|
+
endpoint_name=config.name,
|
|
245
|
+
category=config.category,
|
|
246
|
+
status=VerificationStatus.FAILED,
|
|
247
|
+
response_time_ms=elapsed_ms,
|
|
248
|
+
error_message=f"Validation error: {e}",
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
except APIError as e:
|
|
252
|
+
elapsed_ms = (time.perf_counter() - start_time) * 1000
|
|
253
|
+
return VerificationResult(
|
|
254
|
+
endpoint_id=config.id,
|
|
255
|
+
endpoint_name=config.name,
|
|
256
|
+
category=config.category,
|
|
257
|
+
status=VerificationStatus.FAILED,
|
|
258
|
+
response_time_ms=elapsed_ms,
|
|
259
|
+
error_message=f"API error ({e.status}): {e}",
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
except Exception as e:
|
|
263
|
+
elapsed_ms = (time.perf_counter() - start_time) * 1000
|
|
264
|
+
return VerificationResult(
|
|
265
|
+
endpoint_id=config.id,
|
|
266
|
+
endpoint_name=config.name,
|
|
267
|
+
category=config.category,
|
|
268
|
+
status=VerificationStatus.FAILED,
|
|
269
|
+
response_time_ms=elapsed_ms,
|
|
270
|
+
error_message=str(e),
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
def _resolve_params(self, params: dict[str, str]) -> dict[str, Any]:
|
|
274
|
+
"""파라미터 템플릿을 실제 값으로 치환.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
params: 템플릿 파라미터
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
치환된 파라미터
|
|
281
|
+
"""
|
|
282
|
+
resolved = {}
|
|
283
|
+
for key, value in params.items():
|
|
284
|
+
if value.startswith("{") and value.endswith("}"):
|
|
285
|
+
param_name = value[1:-1]
|
|
286
|
+
if param_name == "rcept_no":
|
|
287
|
+
resolved[key] = self._rcept_no
|
|
288
|
+
else:
|
|
289
|
+
resolved[key] = self._test_data.get(param_name, value)
|
|
290
|
+
else:
|
|
291
|
+
resolved[key] = value
|
|
292
|
+
return resolved
|
|
293
|
+
|
|
294
|
+
def _analyze_result(self, result: Any) -> VerificationStatus:
|
|
295
|
+
"""결과를 분석하여 상태 반환.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
result: API 응답
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
검증 상태
|
|
302
|
+
"""
|
|
303
|
+
if result is None:
|
|
304
|
+
return VerificationStatus.NO_DATA
|
|
305
|
+
if isinstance(result, (list, tuple)):
|
|
306
|
+
return VerificationStatus.SUCCESS if result else VerificationStatus.NO_DATA
|
|
307
|
+
if isinstance(result, bytes):
|
|
308
|
+
return VerificationStatus.SUCCESS if result else VerificationStatus.NO_DATA
|
|
309
|
+
return VerificationStatus.SUCCESS
|
|
310
|
+
|
|
311
|
+
def _summarize_result(self, result: Any) -> dict[str, Any]:
|
|
312
|
+
"""결과 요약.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
result: API 응답
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
요약된 결과
|
|
319
|
+
"""
|
|
320
|
+
if result is None:
|
|
321
|
+
return {"type": "null"}
|
|
322
|
+
if isinstance(result, bytes):
|
|
323
|
+
return {"type": "bytes", "size": len(result)}
|
|
324
|
+
if isinstance(result, (list, tuple)):
|
|
325
|
+
return {"type": "list", "count": len(result)}
|
|
326
|
+
return {"type": type(result).__name__}
|