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.
@@ -0,0 +1,54 @@
1
+ """DS004 지분공시 종합정보 모델."""
2
+
3
+ import msgspec
4
+
5
+
6
+ class MajorStock(msgspec.Struct, kw_only=True):
7
+ """대량보유 상황보고."""
8
+
9
+ rcept_no: str # 접수번호
10
+ rcept_dt: str | None = None # 접수일자
11
+ corp_code: str | None = None # 고유번호
12
+ corp_name: str | None = None # 회사명
13
+ report_tp: str | None = None # 보고구분
14
+ repror: str | None = None # 보고자
15
+ stkqy: str | None = None # 보유주식등의수
16
+ stkqy_irds: str | None = None # 보유주식등의수증감
17
+ stkrt: str | None = None # 보유비율
18
+ stkrt_irds: str | None = None # 보유비율증감
19
+ ctr_stkqy: str | None = None # 주요체결등의수
20
+ ctr_stkrt: str | None = None # 주요체결등비율
21
+ report_resn: str | None = None # 보고사유
22
+
23
+
24
+ class MajorStockListResponse(msgspec.Struct, kw_only=True):
25
+ """대량보유 상황보고 응답."""
26
+
27
+ status: str
28
+ message: str
29
+ items: list[MajorStock] = msgspec.field(default_factory=list, name="list")
30
+
31
+
32
+ class ExecutiveStock(msgspec.Struct, kw_only=True):
33
+ """임원/주요주주 소유보고."""
34
+
35
+ rcept_no: str # 접수번호
36
+ rcept_dt: str | None = None # 접수일자
37
+ corp_code: str | None = None # 고유번호
38
+ corp_name: str | None = None # 회사명
39
+ repror: str | None = None # 보고자
40
+ isu_exctv_rgist_at: str | None = None # 발행회사관계(임원등기여부)
41
+ isu_exctv_ofcps: str | None = None # 발행회사관계(직위)
42
+ isu_main_shrholdr: str | None = None # 발행회사관계(주요주주)
43
+ sp_stock_lmp_cnt: str | None = None # 특정증권등소유수(주권)
44
+ sp_stock_lmp_irds_cnt: str | None = None # 특정증권등소유수증감(주권)
45
+ sp_stock_lmp_rate: str | None = None # 특정증권등소유비율(주권)
46
+ sp_stock_lmp_irds_rate: str | None = None # 특정증권등소유비율증감(주권)
47
+
48
+
49
+ class ExecutiveStockListResponse(msgspec.Struct, kw_only=True):
50
+ """임원/주요주주 소유보고 응답."""
51
+
52
+ status: str
53
+ message: str
54
+ items: list[ExecutiveStock] = msgspec.field(default_factory=list, name="list")
@@ -0,0 +1,50 @@
1
+ """OpenDART API 엔드포인트 검증 모듈.
2
+
3
+ 이 모듈은 OpenDART API의 모든 엔드포인트를 실제 호출하여 검증합니다.
4
+
5
+ Example:
6
+ ```python
7
+ import asyncio
8
+ from opendart_fss.verification import EndpointVerifier, generate_report
9
+
10
+ async def main():
11
+ verifier = EndpointVerifier()
12
+ results = await verifier.verify_all()
13
+ report = generate_report(results, format="console")
14
+ print(report)
15
+
16
+ asyncio.run(main())
17
+ ```
18
+ """
19
+
20
+ from opendart_fss.verification.config import (
21
+ DEFAULT_TEST_DATA,
22
+ ENDPOINT_CONFIGS,
23
+ FALLBACK_CORP_CODES,
24
+ EndpointConfig,
25
+ VerificationResult,
26
+ VerificationStatus,
27
+ )
28
+ from opendart_fss.verification.rate_limiter import AdaptiveRateLimiter
29
+ from opendart_fss.verification.reporter import (
30
+ VerificationReport,
31
+ generate_report,
32
+ )
33
+ from opendart_fss.verification.runner import EndpointVerifier
34
+
35
+ __all__ = [
36
+ # Config
37
+ "EndpointConfig",
38
+ "VerificationResult",
39
+ "VerificationStatus",
40
+ "ENDPOINT_CONFIGS",
41
+ "DEFAULT_TEST_DATA",
42
+ "FALLBACK_CORP_CODES",
43
+ # Rate Limiter
44
+ "AdaptiveRateLimiter",
45
+ # Runner
46
+ "EndpointVerifier",
47
+ # Reporter
48
+ "VerificationReport",
49
+ "generate_report",
50
+ ]
@@ -0,0 +1,450 @@
1
+ """엔드포인트 검증 설정."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import StrEnum
5
+ from typing import Any
6
+
7
+
8
+ class VerificationStatus(StrEnum):
9
+ """검증 상태."""
10
+
11
+ SUCCESS = "SUCCESS"
12
+ NO_DATA = "NO_DATA"
13
+ FAILED = "FAILED"
14
+ SKIPPED = "SKIPPED"
15
+
16
+
17
+ @dataclass
18
+ class VerificationResult:
19
+ """검증 결과."""
20
+
21
+ endpoint_id: str
22
+ endpoint_name: str
23
+ category: str
24
+ status: VerificationStatus
25
+ response_time_ms: float = 0.0
26
+ error_message: str | None = None
27
+ response_data: Any = None
28
+
29
+
30
+ @dataclass
31
+ class EndpointConfig:
32
+ """엔드포인트 설정."""
33
+
34
+ id: str
35
+ name: str
36
+ category: str
37
+ service: str
38
+ method: str
39
+ params: dict[str, str] = field(default_factory=dict)
40
+ requires_rcept_no: bool = False
41
+ description: str = ""
42
+
43
+
44
+ # 기본 테스트 데이터
45
+ DEFAULT_TEST_DATA = {
46
+ "corp_code": "00126380", # 삼성전자
47
+ "bsns_year": "2023",
48
+ "reprt_code": "11011", # 사업보고서
49
+ "bgn_de": "20230101",
50
+ "end_de": "20231231",
51
+ "fs_div": "CFS", # 연결재무제표
52
+ "sj_div": "BS", # 재무상태표
53
+ }
54
+
55
+ # 대체 기업 코드 (삼성전자에 데이터가 없을 경우)
56
+ FALLBACK_CORP_CODES = [
57
+ "00164779", # SK하이닉스
58
+ "00401731", # 현대자동차
59
+ "00155246", # LG전자
60
+ ]
61
+
62
+ # 모든 엔드포인트 설정 (28개)
63
+ ENDPOINT_CONFIGS: list[EndpointConfig] = [
64
+ # DS001 - 공시정보 (4개)
65
+ EndpointConfig(
66
+ id="DS001-01",
67
+ name="disclosure.search",
68
+ category="DS001",
69
+ service="disclosure",
70
+ method="search",
71
+ params={
72
+ "corp_code": "{corp_code}",
73
+ "bgn_de": "{bgn_de}",
74
+ "end_de": "{end_de}",
75
+ },
76
+ description="공시검색",
77
+ ),
78
+ EndpointConfig(
79
+ id="DS001-02",
80
+ name="disclosure.get_company",
81
+ category="DS001",
82
+ service="disclosure",
83
+ method="get_company",
84
+ params={"corp_code": "{corp_code}"},
85
+ description="기업개황 조회",
86
+ ),
87
+ EndpointConfig(
88
+ id="DS001-03",
89
+ name="disclosure.download_document",
90
+ category="DS001",
91
+ service="disclosure",
92
+ method="download_document",
93
+ params={"rcept_no": "{rcept_no}"},
94
+ requires_rcept_no=True,
95
+ description="공시서류 원본 다운로드",
96
+ ),
97
+ EndpointConfig(
98
+ id="DS001-04",
99
+ name="disclosure.download_corp_codes",
100
+ category="DS001",
101
+ service="disclosure",
102
+ method="download_corp_codes",
103
+ params={},
104
+ description="고유번호 전체 다운로드",
105
+ ),
106
+ # DS002 - 정기보고서 주요정보 (8개)
107
+ EndpointConfig(
108
+ id="DS002-01",
109
+ name="report.get_stock_changes",
110
+ category="DS002",
111
+ service="report",
112
+ method="get_stock_changes",
113
+ params={
114
+ "corp_code": "{corp_code}",
115
+ "bsns_year": "{bsns_year}",
116
+ "reprt_code": "{reprt_code}",
117
+ },
118
+ description="증자(감자) 현황 조회",
119
+ ),
120
+ EndpointConfig(
121
+ id="DS002-02",
122
+ name="report.get_dividends",
123
+ category="DS002",
124
+ service="report",
125
+ method="get_dividends",
126
+ params={
127
+ "corp_code": "{corp_code}",
128
+ "bsns_year": "{bsns_year}",
129
+ "reprt_code": "{reprt_code}",
130
+ },
131
+ description="배당에 관한 사항 조회",
132
+ ),
133
+ EndpointConfig(
134
+ id="DS002-03",
135
+ name="report.get_treasury_stock",
136
+ category="DS002",
137
+ service="report",
138
+ method="get_treasury_stock",
139
+ params={
140
+ "corp_code": "{corp_code}",
141
+ "bsns_year": "{bsns_year}",
142
+ "reprt_code": "{reprt_code}",
143
+ },
144
+ description="자기주식 현황 조회",
145
+ ),
146
+ EndpointConfig(
147
+ id="DS002-04",
148
+ name="report.get_largest_shareholders",
149
+ category="DS002",
150
+ service="report",
151
+ method="get_largest_shareholders",
152
+ params={
153
+ "corp_code": "{corp_code}",
154
+ "bsns_year": "{bsns_year}",
155
+ "reprt_code": "{reprt_code}",
156
+ },
157
+ description="최대주주 현황 조회",
158
+ ),
159
+ EndpointConfig(
160
+ id="DS002-05",
161
+ name="report.get_executives",
162
+ category="DS002",
163
+ service="report",
164
+ method="get_executives",
165
+ params={
166
+ "corp_code": "{corp_code}",
167
+ "bsns_year": "{bsns_year}",
168
+ "reprt_code": "{reprt_code}",
169
+ },
170
+ description="임원 현황 조회",
171
+ ),
172
+ EndpointConfig(
173
+ id="DS002-06",
174
+ name="report.get_employees",
175
+ category="DS002",
176
+ service="report",
177
+ method="get_employees",
178
+ params={
179
+ "corp_code": "{corp_code}",
180
+ "bsns_year": "{bsns_year}",
181
+ "reprt_code": "{reprt_code}",
182
+ },
183
+ description="직원 현황 조회",
184
+ ),
185
+ EndpointConfig(
186
+ id="DS002-07",
187
+ name="report.get_individual_compensation",
188
+ category="DS002",
189
+ service="report",
190
+ method="get_individual_compensation",
191
+ params={
192
+ "corp_code": "{corp_code}",
193
+ "bsns_year": "{bsns_year}",
194
+ "reprt_code": "{reprt_code}",
195
+ },
196
+ description="개인별 보수 현황 조회",
197
+ ),
198
+ EndpointConfig(
199
+ id="DS002-08",
200
+ name="report.get_director_compensation",
201
+ category="DS002",
202
+ service="report",
203
+ method="get_director_compensation",
204
+ params={
205
+ "corp_code": "{corp_code}",
206
+ "bsns_year": "{bsns_year}",
207
+ "reprt_code": "{reprt_code}",
208
+ },
209
+ description="이사/감사 보수 현황 조회",
210
+ ),
211
+ # DS003 - 정기보고서 재무정보 (6개)
212
+ EndpointConfig(
213
+ id="DS003-01",
214
+ name="financial.get_single_account",
215
+ category="DS003",
216
+ service="financial",
217
+ method="get_single_account",
218
+ params={
219
+ "corp_code": "{corp_code}",
220
+ "bsns_year": "{bsns_year}",
221
+ "reprt_code": "{reprt_code}",
222
+ "fs_div": "{fs_div}",
223
+ },
224
+ description="단일회사 주요계정 조회",
225
+ ),
226
+ EndpointConfig(
227
+ id="DS003-02",
228
+ name="financial.get_multi_account",
229
+ category="DS003",
230
+ service="financial",
231
+ method="get_multi_account",
232
+ params={
233
+ "corp_code": "{corp_code}",
234
+ "bsns_year": "{bsns_year}",
235
+ "reprt_code": "{reprt_code}",
236
+ "fs_div": "{fs_div}",
237
+ },
238
+ description="다중회사 주요계정 조회",
239
+ ),
240
+ EndpointConfig(
241
+ id="DS003-03",
242
+ name="financial.get_full_statements",
243
+ category="DS003",
244
+ service="financial",
245
+ method="get_full_statements",
246
+ params={
247
+ "corp_code": "{corp_code}",
248
+ "bsns_year": "{bsns_year}",
249
+ "reprt_code": "{reprt_code}",
250
+ "fs_div": "{fs_div}",
251
+ },
252
+ description="단일회사 전체 재무제표 조회",
253
+ ),
254
+ EndpointConfig(
255
+ id="DS003-04",
256
+ name="financial.download_xbrl",
257
+ category="DS003",
258
+ service="financial",
259
+ method="download_xbrl",
260
+ params={
261
+ "rcept_no": "{rcept_no}",
262
+ "reprt_code": "{reprt_code}",
263
+ },
264
+ requires_rcept_no=True,
265
+ description="XBRL 원본파일 다운로드",
266
+ ),
267
+ EndpointConfig(
268
+ id="DS003-05",
269
+ name="financial.get_xbrl_taxonomy",
270
+ category="DS003",
271
+ service="financial",
272
+ method="get_xbrl_taxonomy",
273
+ params={"sj_div": "{sj_div}"},
274
+ description="XBRL 택사노미 조회",
275
+ ),
276
+ EndpointConfig(
277
+ id="DS003-06",
278
+ name="financial.get_indicators",
279
+ category="DS003",
280
+ service="financial",
281
+ method="get_indicators",
282
+ params={
283
+ "corp_code": "{corp_code}",
284
+ "bsns_year": "{bsns_year}",
285
+ "reprt_code": "{reprt_code}",
286
+ },
287
+ description="재무지표 조회",
288
+ ),
289
+ # DS004 - 지분공시 종합정보 (2개)
290
+ EndpointConfig(
291
+ id="DS004-01",
292
+ name="shareholder.get_major_stock",
293
+ category="DS004",
294
+ service="shareholder",
295
+ method="get_major_stock",
296
+ params={"corp_code": "{corp_code}"},
297
+ description="대량보유 상황보고 조회",
298
+ ),
299
+ EndpointConfig(
300
+ id="DS004-02",
301
+ name="shareholder.get_executive_stock",
302
+ category="DS004",
303
+ service="shareholder",
304
+ method="get_executive_stock",
305
+ params={"corp_code": "{corp_code}"},
306
+ description="임원/주요주주 소유보고 조회",
307
+ ),
308
+ # DS005 - 주요사항보고서 주요정보 (6개)
309
+ EndpointConfig(
310
+ id="DS005-01",
311
+ name="major_event.get_paid_capital_increase",
312
+ category="DS005",
313
+ service="major_event",
314
+ method="get_paid_capital_increase",
315
+ params={
316
+ "corp_code": "{corp_code}",
317
+ "bgn_de": "{bgn_de}",
318
+ "end_de": "{end_de}",
319
+ },
320
+ description="유상증자 결정 조회",
321
+ ),
322
+ EndpointConfig(
323
+ id="DS005-02",
324
+ name="major_event.get_bonus_issue",
325
+ category="DS005",
326
+ service="major_event",
327
+ method="get_bonus_issue",
328
+ params={
329
+ "corp_code": "{corp_code}",
330
+ "bgn_de": "{bgn_de}",
331
+ "end_de": "{end_de}",
332
+ },
333
+ description="무상증자 결정 조회",
334
+ ),
335
+ EndpointConfig(
336
+ id="DS005-03",
337
+ name="major_event.get_capital_reduction",
338
+ category="DS005",
339
+ service="major_event",
340
+ method="get_capital_reduction",
341
+ params={
342
+ "corp_code": "{corp_code}",
343
+ "bgn_de": "{bgn_de}",
344
+ "end_de": "{end_de}",
345
+ },
346
+ description="감자 결정 조회",
347
+ ),
348
+ EndpointConfig(
349
+ id="DS005-04",
350
+ name="major_event.get_convertible_bond",
351
+ category="DS005",
352
+ service="major_event",
353
+ method="get_convertible_bond",
354
+ params={
355
+ "corp_code": "{corp_code}",
356
+ "bgn_de": "{bgn_de}",
357
+ "end_de": "{end_de}",
358
+ },
359
+ description="전환사채권 발행결정 조회",
360
+ ),
361
+ EndpointConfig(
362
+ id="DS005-05",
363
+ name="major_event.get_merger_decision",
364
+ category="DS005",
365
+ service="major_event",
366
+ method="get_merger_decision",
367
+ params={
368
+ "corp_code": "{corp_code}",
369
+ "bgn_de": "{bgn_de}",
370
+ "end_de": "{end_de}",
371
+ },
372
+ description="합병 결정 조회",
373
+ ),
374
+ EndpointConfig(
375
+ id="DS005-06",
376
+ name="major_event.get_split_decision",
377
+ category="DS005",
378
+ service="major_event",
379
+ method="get_split_decision",
380
+ params={
381
+ "corp_code": "{corp_code}",
382
+ "bgn_de": "{bgn_de}",
383
+ "end_de": "{end_de}",
384
+ },
385
+ description="분할 결정 조회",
386
+ ),
387
+ # DS006 - 증권신고서 주요정보 (4개)
388
+ EndpointConfig(
389
+ id="DS006-01",
390
+ name="registration.get_equity_securities",
391
+ category="DS006",
392
+ service="registration",
393
+ method="get_equity_securities",
394
+ params={
395
+ "corp_code": "{corp_code}",
396
+ "bgn_de": "{bgn_de}",
397
+ "end_de": "{end_de}",
398
+ },
399
+ description="지분증권 발행 조회",
400
+ ),
401
+ EndpointConfig(
402
+ id="DS006-02",
403
+ name="registration.get_debt_securities",
404
+ category="DS006",
405
+ service="registration",
406
+ method="get_debt_securities",
407
+ params={
408
+ "corp_code": "{corp_code}",
409
+ "bgn_de": "{bgn_de}",
410
+ "end_de": "{end_de}",
411
+ },
412
+ description="채무증권 발행 조회",
413
+ ),
414
+ EndpointConfig(
415
+ id="DS006-03",
416
+ name="registration.get_merger_registration",
417
+ category="DS006",
418
+ service="registration",
419
+ method="get_merger_registration",
420
+ params={
421
+ "corp_code": "{corp_code}",
422
+ "bgn_de": "{bgn_de}",
423
+ "end_de": "{end_de}",
424
+ },
425
+ description="합병 신고 조회",
426
+ ),
427
+ EndpointConfig(
428
+ id="DS006-04",
429
+ name="registration.get_split_registration",
430
+ category="DS006",
431
+ service="registration",
432
+ method="get_split_registration",
433
+ params={
434
+ "corp_code": "{corp_code}",
435
+ "bgn_de": "{bgn_de}",
436
+ "end_de": "{end_de}",
437
+ },
438
+ description="분할 신고 조회",
439
+ ),
440
+ ]
441
+
442
+ # 카테고리 설명
443
+ CATEGORY_DESCRIPTIONS = {
444
+ "DS001": "공시정보",
445
+ "DS002": "정기보고서 주요정보",
446
+ "DS003": "정기보고서 재무정보",
447
+ "DS004": "지분공시 종합정보",
448
+ "DS005": "주요사항보고서 주요정보",
449
+ "DS006": "증권신고서 주요정보",
450
+ }
@@ -0,0 +1,92 @@
1
+ """적응형 Rate Limiter."""
2
+
3
+ import asyncio
4
+ import time
5
+
6
+
7
+ class AdaptiveRateLimiter:
8
+ """지수 백오프를 사용하는 적응형 Rate Limiter.
9
+
10
+ Rate limit 오류 발생 시 지수적으로 대기 시간을 증가시키고,
11
+ 성공 시 점진적으로 감소시킵니다.
12
+
13
+ Example:
14
+ ```python
15
+ limiter = AdaptiveRateLimiter()
16
+
17
+ async def make_request():
18
+ await limiter.wait()
19
+ try:
20
+ result = await api_call()
21
+ limiter.on_success()
22
+ return result
23
+ except RateLimitError:
24
+ limiter.on_rate_limit()
25
+ raise
26
+ ```
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ min_delay: float = 0.5,
32
+ max_delay: float = 16.0,
33
+ backoff_factor: float = 2.0,
34
+ recovery_factor: float = 0.9,
35
+ ) -> None:
36
+ """Rate Limiter 초기화.
37
+
38
+ Args:
39
+ min_delay: 최소 요청 간격 (초)
40
+ max_delay: 최대 요청 간격 (초)
41
+ backoff_factor: Rate limit 시 지연 증가 배수
42
+ recovery_factor: 성공 시 지연 감소 배수
43
+ """
44
+ self.min_delay = min_delay
45
+ self.max_delay = max_delay
46
+ self.backoff_factor = backoff_factor
47
+ self.recovery_factor = recovery_factor
48
+
49
+ self._current_delay = min_delay
50
+ self._last_request_time: float = 0.0
51
+ self._rate_limit_count = 0
52
+
53
+ @property
54
+ def current_delay(self) -> float:
55
+ """현재 요청 간격."""
56
+ return self._current_delay
57
+
58
+ @property
59
+ def rate_limit_count(self) -> int:
60
+ """Rate limit 발생 횟수."""
61
+ return self._rate_limit_count
62
+
63
+ async def wait(self) -> None:
64
+ """다음 요청 전 필요한 시간만큼 대기."""
65
+ now = time.monotonic()
66
+ elapsed = now - self._last_request_time
67
+
68
+ if elapsed < self._current_delay:
69
+ await asyncio.sleep(self._current_delay - elapsed)
70
+
71
+ self._last_request_time = time.monotonic()
72
+
73
+ def on_success(self) -> None:
74
+ """요청 성공 시 호출. 대기 시간 점진적 감소."""
75
+ self._current_delay = max(
76
+ self.min_delay,
77
+ self._current_delay * self.recovery_factor,
78
+ )
79
+
80
+ def on_rate_limit(self) -> None:
81
+ """Rate limit 발생 시 호출. 대기 시간 지수적 증가."""
82
+ self._rate_limit_count += 1
83
+ self._current_delay = min(
84
+ self.max_delay,
85
+ self._current_delay * self.backoff_factor,
86
+ )
87
+
88
+ def reset(self) -> None:
89
+ """Rate limiter 상태 초기화."""
90
+ self._current_delay = self.min_delay
91
+ self._last_request_time = 0.0
92
+ self._rate_limit_count = 0