datagokr-reader 1.0.0__tar.gz

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,25 @@
1
+ Metadata-Version: 2.4
2
+ Name: datagokr_reader
3
+ Version: 1.0.0
4
+ Summary: data.go.kr의 채권 관련 API를 사용하기 편하게 만든 라이브러리입니다.
5
+ Author: WONWOO LEE
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://example.com
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: httpx>=0.27
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=8.0; extra == "dev"
13
+
14
+ ## DatagokrReader
15
+
16
+ data.go.kr의 채권 관련 API를 사용하기 편하게 만든 라이브러리입니다.
17
+
18
+ ### Example
19
+
20
+ ```python
21
+ from datagokr_reader import run_bond_workflow
22
+
23
+ # 띄어쓰기 및 대소문자 무관, 채권 관련 주요 데이터 모두 정리
24
+ result = run_bond_workflow(datagokr_api_key, "공공데이터포털1EB")
25
+ ```
@@ -0,0 +1,12 @@
1
+ ## DatagokrReader
2
+
3
+ data.go.kr의 채권 관련 API를 사용하기 편하게 만든 라이브러리입니다.
4
+
5
+ ### Example
6
+
7
+ ```python
8
+ from datagokr_reader import run_bond_workflow
9
+
10
+ # 띄어쓰기 및 대소문자 무관, 채권 관련 주요 데이터 모두 정리
11
+ result = run_bond_workflow(datagokr_api_key, "공공데이터포털1EB")
12
+ ```
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "datagokr_reader"
7
+ version = "1.0.0"
8
+ description = " data.go.kr의 채권 관련 API를 사용하기 편하게 만든 라이브러리입니다."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [{ name = "WONWOO LEE" }]
13
+ dependencies = [
14
+ "httpx>=0.27",
15
+ ]
16
+
17
+ [project.optional-dependencies]
18
+ dev = [
19
+ "pytest>=8.0",
20
+ ]
21
+
22
+ [project.urls]
23
+ Repository = "https://example.com"
24
+
25
+ [project.scripts]
26
+ datagokr-reader = "datagokr_reader.cli:main"
27
+
28
+ [tool.setuptools]
29
+ package-dir = {"" = "src"}
30
+
31
+ [tool.setuptools.packages.find]
32
+ where = ["src"]
33
+
34
+ [tool.pytest.ini_options]
35
+ testpaths = ["tests"]
36
+ addopts = "-q"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,27 @@
1
+ # datagokr_reader 구조:
2
+ #
3
+ # src/datagokr_reader/
4
+ # ├── __init__.py
5
+ # ├── getOptiExer.py # 실제 주식 행사내역 다운로드
6
+ # ├── getOptiExerPricAdju.py # 주식 행사가 조정내역 다운로드
7
+ # ├── getEarlExerOpti.py # 옵션 행사 일정 다운로드
8
+ # ├── getBondBasiInfo.py # 채권 기본 정보 다운로드, Not reliable
9
+ # ├── getBondWithOptiCallRede.py # 옵션 행사내역 다운로드
10
+ # ├── getIssuIssuItemStat.py # 발행인에 따른 채권 조회
11
+
12
+ from .getOptiExer import get_OptiExer, parse_opti_exer
13
+ from .getOptiExerPricAdju import get_OptiExerPricAdju, parse_opti_exer_pric_adju
14
+ from .getEarlExerOpti import get_EarlExerOpti, parse_earl_exer_opti
15
+ from .getBondBasiInfo import get_BondBasiInfo, parse_bond_basi_info
16
+ from .getBondWithOptiCallRede import get_BondWithOptiCallRede, parse_bond_with_opti_call_rede
17
+ from .getIssuIssuItemStat import get_IssuIssuItemStat, parse_issu_issu_item_stat
18
+ from .workflow import run_bond_workflow
19
+ __all__ = [
20
+ "get_OptiExer", "parse_opti_exer",
21
+ "get_OptiExerPricAdju", "parse_opti_exer_pric_adju",
22
+ "get_EarlExerOpti", "parse_earl_exer_opti",
23
+ "get_BondBasiInfo", "parse_bond_basi_info",
24
+ "get_BondWithOptiCallRede", "parse_bond_with_opti_call_rede",
25
+ "get_IssuIssuItemStat", "parse_issu_issu_item_stat",
26
+ "run_bond_workflow",
27
+ ]
@@ -0,0 +1,139 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import re
6
+ import httpx
7
+
8
+
9
+ def get_datagokr_data(
10
+ serviceUrl: str, params: dict[str, Any], timeout_seconds: float = 60.0
11
+ ) -> Any | None:
12
+ """ Datagokr API를 호출해서 응답 JSON을 반환한다. """
13
+ baseUrl = "http://apis.data.go.kr"
14
+ url = f"{baseUrl}/{serviceUrl}"
15
+ try:
16
+ resp = httpx.get(url, params=params, timeout=timeout_seconds)
17
+ resp.raise_for_status()
18
+ return resp.json()
19
+ except Exception:
20
+ return None
21
+
22
+ def get_datagokr_document(
23
+ serviceUrl: str, params: dict[str, Any], timeout_seconds: float = 60.0,
24
+ numOfRows: int = 100, resultType: str = "json",
25
+ ) -> Any | None:
26
+ """
27
+ Datagokr API를 반복 호출해서 응답 JSON을 반환한다.
28
+ 1. `pageNo = 1`부터 `pageNo = totalCount / numOfRows` 페이지까지 반복 호출한다.
29
+ 2. 각 페이지의 items를 모두 모아서 반환한다.
30
+ 반환 형식은 기본 형식과 동일하며, items는 {"item": [...]} 구조와 단순 리스트 구조를 모두 지원한다.
31
+ ```json
32
+ {
33
+ "response": {
34
+ "body": {
35
+ "pageNo": 1,
36
+ "numOfRows": 100,
37
+ "totalCount": ...,
38
+ "items": # contents
39
+ }
40
+ }
41
+ }
42
+ ```
43
+ """
44
+ # 공통 파라미터 설정 (기본값이 있으면 덮어쓰지 않음)
45
+ base_params: dict[str, Any] = dict(params)
46
+ base_params.setdefault("numOfRows", numOfRows)
47
+ base_params.setdefault("resultType", resultType)
48
+
49
+ # 1페이지 호출
50
+ first_params = dict(base_params)
51
+ first_params["pageNo"] = 1
52
+ first_doc = get_datagokr_data(serviceUrl, first_params, timeout_seconds)
53
+ if first_doc is None:
54
+ return None
55
+ try:
56
+ body = first_doc["response"]["body"]
57
+ except Exception:
58
+ return first_doc
59
+
60
+ # 전체 개수 확인
61
+ total_count_raw = body.get("totalCount", 0)
62
+ try:
63
+ total_count = int(total_count_raw)
64
+ except Exception:
65
+ total_count = 0
66
+ if total_count <= 0:
67
+ return first_doc
68
+
69
+ # items 추출
70
+ def extract_items(items_obj: Any) -> list[Any]:
71
+ # 1) items가 None인 경우 -> []
72
+ if items_obj is None:
73
+ return []
74
+ # 2) "items": {"item": [...]} OR "items": {"item": {...}} 인 경우
75
+ # - inner가 리스트면 그대로 리스트 반환
76
+ # - inner가 단일 객체면 리스트로 감싸서 반환
77
+ if isinstance(items_obj, dict) and "item" in items_obj:
78
+ inner = items_obj.get("item")
79
+ if inner is None:
80
+ return []
81
+ if isinstance(inner, list):
82
+ return inner
83
+ return [inner]
84
+ # 3) "items": [...] 인 경우 -> 그대로 리스트 반환
85
+ if isinstance(items_obj, list):
86
+ return items_obj
87
+ # 4) 그 외에는 리스트로 감싸서 반환
88
+ return [items_obj]
89
+
90
+ items_obj = body.get("items")
91
+ all_items: list[Any] = extract_items(items_obj)
92
+
93
+ # 총 페이지 수 계산
94
+ total_pages = (total_count + numOfRows - 1) // numOfRows
95
+
96
+ # 2페이지부터 마지막 페이지까지 반복 호출
97
+ for page_no in range(2, total_pages + 1):
98
+ page_params = dict(base_params)
99
+ page_params["pageNo"] = page_no
100
+ page_doc = get_datagokr_data(serviceUrl, page_params, timeout_seconds)
101
+ if page_doc is None:
102
+ break
103
+ try:
104
+ page_body = page_doc["response"]["body"]
105
+ page_items_obj = page_body.get("items")
106
+ except Exception:
107
+ break
108
+ all_items.extend(extract_items(page_items_obj))
109
+
110
+ # 첫 응답 객체에 모든 items를 합쳐서 세팅
111
+ if isinstance(items_obj, dict) and "item" in items_obj:
112
+ items_obj["item"] = all_items
113
+ body["items"] = items_obj
114
+ else:
115
+ body["items"] = all_items
116
+
117
+ # 메타 정보 정리 (pageNo는 1, numOfRows는 입력값, totalCount는 실제 아이템 개수)
118
+ body["pageNo"] = 1
119
+ body["numOfRows"] = numOfRows
120
+ body["totalCount"] = len(all_items)
121
+
122
+ return first_doc
123
+
124
+
125
+ def normalize_name(name: str) -> str:
126
+ name_no_space = name.replace(" ", "")
127
+ name_no_space = re.sub(r"[A-Za-z]+", lambda m: m.group(0).lower(), name_no_space)
128
+ paren_index = name_no_space.find("(")
129
+ if paren_index != -1:
130
+ return name_no_space[:paren_index]
131
+ return name_no_space
132
+
133
+ def issuer_name_from_bond_name(bond_name: str) -> str:
134
+ """ 채권명에서 발행사명을 추출. (숫자를 기준으로 절삭) """
135
+ normalized = normalize_name(bond_name)
136
+ m = re.search(r"\d", normalized)
137
+ if not m:
138
+ return normalized
139
+ return normalized[: m.start()]
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+
7
+ from datagokr_reader.workflow import run_bond_workflow
8
+
9
+
10
+ def main(argv: list[str] | None = None) -> int:
11
+ parser = argparse.ArgumentParser(prog="datagokr-reader")
12
+ parser.add_argument("--api-key", required=True, help="data.go.kr serviceKey")
13
+ parser.add_argument("--bond-name", required=True, help="채권명 (띄어쓰기/대소문자 무관)")
14
+ parser.add_argument(
15
+ "--pretty",
16
+ action="store_true",
17
+ help="Pretty JSON 출력",
18
+ )
19
+ args = parser.parse_args(argv)
20
+
21
+ result = run_bond_workflow(args.api_key, args.bond_name)
22
+ if args.pretty:
23
+ print(json.dumps(result, ensure_ascii=False, indent=2))
24
+ else:
25
+ print(json.dumps(result, ensure_ascii=False))
26
+ return 0
27
+
28
+
29
+ if __name__ == "__main__":
30
+ raise SystemExit(main(sys.argv[1:]))
31
+
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from .base_reader import get_datagokr_document
6
+
7
+ """
8
+ BondBasiInfo : (bondIsurNm|crno)
9
+ -> bondBal(발행잔액) +
10
+ "basDt": 채권의 기준일
11
+ "crno": 기업의 법인등록번호
12
+ "bondIsurNm": 채권의 발행사명
13
+ "isinCdNm": 채권의 ISIN 종목명
14
+ "bondIssuDt": 채권의 발행일
15
+ "bondExprDt": 채권의 만기일
16
+ "bondIssuAmt": 채권의 발행액
17
+ "bondPymtAmt": 채권의 납입액
18
+ "issuDptyNm": 주간사명
19
+ """
20
+ def get_BondBasiInfo(serviceKey: str, basDt: str, bondIsurNm: str, crno: str, timeout_seconds: float = 60.0) -> Any | None:
21
+ """ 채권 기본 정보 다운로드. """
22
+ service_url = "1160100/service/GetBondIssuInfoService/getBondBasiInfo"
23
+ if not bondIsurNm and not crno:
24
+ raise ValueError("bondIsurNm 또는 crno가 없습니다.")
25
+
26
+ params: dict[str, Any] = {
27
+ "serviceKey": serviceKey
28
+ }
29
+ if basDt:
30
+ params["basDt"] = basDt
31
+ if bondIsurNm:
32
+ params["bondIsurNm"] = bondIsurNm
33
+ if crno:
34
+ params["crno"] = crno
35
+
36
+ return get_datagokr_document(
37
+ serviceUrl=service_url,
38
+ params=params,
39
+ timeout_seconds=timeout_seconds
40
+ )
41
+
42
+
43
+ def parse_bond_basi_info(serviceKey: str, basDt: str, bondIsurNm: str, crno: str, timeout_seconds: float = 60.0) -> dict[str, Any] | None:
44
+ """ Parse get_BondBasiInfo results. """
45
+ raw = get_BondBasiInfo(serviceKey=serviceKey, basDt=basDt, bondIsurNm=bondIsurNm, crno=crno, timeout_seconds=timeout_seconds)
46
+ if raw is None:
47
+ return None
48
+
49
+ try:
50
+ body = raw["response"]["body"]
51
+ items_obj = body.get("items")
52
+ except Exception:
53
+ return None
54
+
55
+ def extract_items(items: Any) -> list[dict[str, Any]]:
56
+ if items is None:
57
+ return []
58
+ if isinstance(items, dict) and "item" in items:
59
+ inner = items.get("item")
60
+ if inner is None:
61
+ return []
62
+ if isinstance(inner, list):
63
+ return [i for i in inner if isinstance(i, dict)]
64
+ return [inner] if isinstance(inner, dict) else []
65
+ if isinstance(items, list):
66
+ return [i for i in items if isinstance(i, dict)]
67
+ return [items] if isinstance(items, dict) else []
68
+
69
+ items = extract_items(items_obj)
70
+ if not items:
71
+ return {}
72
+
73
+ result: dict[str, Any] = {}
74
+ for it in items:
75
+ isin = it.get("isinCd")
76
+ if not isin:
77
+ continue
78
+
79
+ meta = result.setdefault(
80
+ isin,
81
+ {
82
+ "metadata": {
83
+ "basDt": it.get("basDt"),
84
+ "crno": it.get("crno"),
85
+ "bondIsurNm": it.get("bondIsurNm"),
86
+ "isinCd": it.get("isinCd"),
87
+ "isinCdNm": it.get("isinCdNm"),
88
+ "bondIssuDt": it.get("bondIssuDt"),
89
+ "bondIssuAmt": it.get("bondIssuAmt"),
90
+ "bondExprDt": it.get("bondExprDt"),
91
+ "bondPymtAmt": it.get("bondPymtAmt"),
92
+ # "bondSrfcInrt": it.get("bondSrfcInrt"), # 표면이자율(추후 복원 가능하도록 주석 처리)
93
+ "issuDptyNm": it.get("issuDptyNm"),
94
+ },
95
+ "items": [],
96
+ },
97
+ )
98
+
99
+ meta["items"].append(
100
+ {
101
+ "bondBal": it.get("bondBal"),
102
+ }
103
+ )
104
+
105
+ return result
106
+
107
+
@@ -0,0 +1,111 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from .base_reader import get_datagokr_document
6
+
7
+
8
+ def get_BondWithOptiCallRede(
9
+ serviceKey: str,
10
+ crno: str,
11
+ opbdIsurNm: str,
12
+ timeout_seconds: float = 60.0,
13
+ numOfRows: int = 100,
14
+ resultType: str = "json",
15
+ ) -> Any | None:
16
+ """ 옵션 행사내역 다운로드. """
17
+ service_url = "1160100/service/GetBondRedeInfoService/getBondWithOptiCallRede"
18
+ if not crno and not opbdIsurNm:
19
+ raise ValueError("crno 또는 opbdIsurNm가 없습니다.")
20
+
21
+ params: dict[str, Any] = {
22
+ "serviceKey": serviceKey,
23
+ }
24
+ if crno:
25
+ params["crno"] = crno
26
+ if opbdIsurNm:
27
+ params["opbdIsurNm"] = opbdIsurNm
28
+
29
+ return get_datagokr_document(
30
+ serviceUrl=service_url,
31
+ params=params,
32
+ timeout_seconds=timeout_seconds,
33
+ numOfRows=numOfRows,
34
+ resultType=resultType,
35
+ )
36
+
37
+
38
+ def parse_bond_with_opti_call_rede(
39
+ serviceKey: str,
40
+ crno: str,
41
+ opbdIsurNm: str,
42
+ timeout_seconds: float = 60.0,
43
+ raw: Any | None = None,
44
+ isinCdKey: str | None = None,
45
+ ) -> list[dict[str, Any]] | None:
46
+ """ Parse get_BondWithOptiCallRede results. """
47
+ if raw is None:
48
+ raw = get_BondWithOptiCallRede(
49
+ serviceKey=serviceKey,
50
+ crno=crno,
51
+ opbdIsurNm=opbdIsurNm,
52
+ timeout_seconds=timeout_seconds,
53
+ )
54
+ if raw is None:
55
+ return None
56
+
57
+ try:
58
+ body = raw["response"]["body"]
59
+ items_obj = body.get("items")
60
+ except Exception:
61
+ return None
62
+
63
+ def extract_items(items: Any) -> list[dict[str, Any]]:
64
+ if items is None:
65
+ return []
66
+ if isinstance(items, dict) and "item" in items:
67
+ inner = items.get("item")
68
+ if inner is None:
69
+ return []
70
+ if isinstance(inner, list):
71
+ return [i for i in inner if isinstance(i, dict)]
72
+ return [inner] if isinstance(inner, dict) else []
73
+ if isinstance(items, list):
74
+ return [i for i in items if isinstance(i, dict)]
75
+ return [items] if isinstance(items, dict) else []
76
+
77
+ items = extract_items(items_obj)
78
+ if not items:
79
+ return []
80
+
81
+ # 아래 항목들은 it에 존재하지만, 현재 라이브러리 목적상 파싱하지 않음
82
+ # "isinCd": it.get("isinCd"), # ISIN 종목번호, BasiInfo에서 이미 있음(추후 복원 가능하도록 주석 처리)
83
+ # "crno": it.get("crno"), # 기업의 법인등록번호, BasiInfo에서 이미 있음(추후 복원 가능하도록 주석 처리)
84
+ # "isinCdNm": it.get("isinCdNm"), # ISIN 종목명, BasiInfo에서 이미 있음(추후 복원 가능하도록 주석 처리)
85
+ # "opbdIsurNm": it.get("opbdIsurNm"), # 채권의 발행사명, BasiInfo에서 이미 있음(추후 복원 가능하도록 주석 처리)
86
+ # "opbdIssuDt": it.get("opbdIssuDt"), # 채권의 발행일, BasiInfo에서 이미 있음(추후 복원 가능하도록 주석 처리)
87
+ # "opbdExprDt": it.get("opbdExprDt"), # 채권의 만기일, BasiInfo에서 이미 있음(추후 복원 가능하도록 주석 처리)
88
+ # "opbdIssuAmt": it.get("opbdIssuAmt"), # 최초발행액, BasiInfo에서 이미 있음(추후 복원 가능하도록 주석 처리)
89
+ # "bondIssuAmt": it.get("bondIssuAmt"), # 발행잔액, BasiInfo에서 이미 있음(추후 복원 가능하도록 주석 처리)
90
+
91
+ out: list[dict[str, Any]] = []
92
+ for it in items:
93
+ isin = it.get("isinCd")
94
+ if isinCdKey:
95
+ if not isin or isin != isinCdKey:
96
+ continue
97
+ else:
98
+ if not isin:
99
+ continue
100
+ out.append(
101
+ {
102
+ "isinCd": isin,
103
+ "optnTcdNm": it.get("optnTcdNm"),
104
+ "opbdClrdDt": it.get("opbdClrdDt"),
105
+ "opbdPamtPayAmt": it.get("opbdPamtPayAmt"),
106
+ }
107
+ )
108
+
109
+ return out
110
+
111
+
@@ -0,0 +1,100 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from .base_reader import get_datagokr_document
6
+
7
+ def get_EarlExerOpti(
8
+ serviceKey: str,
9
+ basDt: str | None,
10
+ bondIsurNm: str,
11
+ crno: str,
12
+ timeout_seconds: float = 60.0,
13
+ ) -> Any | None:
14
+ """ 옵션 행사 일정 다운로드.
15
+ 사모 사채는 콜옵션이 공시되지 않는 경우가 있으니 주의.
16
+ """
17
+ service_url = "1160100/service/GetBondRedeInfoService/getEarlExerOpti"
18
+ if not bondIsurNm and not crno:
19
+ raise ValueError("bondIsurNm 또는 crno가 없습니다.")
20
+
21
+ params: dict[str, Any] = {
22
+ "serviceKey": serviceKey
23
+ }
24
+ if basDt:
25
+ params["basDt"] = basDt
26
+ if bondIsurNm:
27
+ params["bondIsurNm"] = bondIsurNm
28
+ if crno:
29
+ params["crno"] = crno
30
+ return get_datagokr_document(
31
+ serviceUrl=service_url,
32
+ params=params,
33
+ timeout_seconds=timeout_seconds
34
+ )
35
+
36
+
37
+ def parse_earl_exer_opti(
38
+ serviceKey: str,
39
+ basDt: str | None,
40
+ bondIsurNm: str,
41
+ crno: str,
42
+ timeout_seconds: float = 60.0,
43
+ isinCdKey: str | None = None,
44
+ ) -> list[dict[str, Any]] | None:
45
+ """ Parse get_EarlExerOpti results. """
46
+ raw = get_EarlExerOpti(serviceKey=serviceKey, basDt=basDt, bondIsurNm=bondIsurNm, crno=crno, timeout_seconds=timeout_seconds)
47
+ if raw is None:
48
+ return None
49
+
50
+ try:
51
+ body = raw["response"]["body"]
52
+ items_obj = body.get("items")
53
+ except Exception:
54
+ return None
55
+
56
+ def extract_items(items: Any) -> list[dict[str, Any]]:
57
+ if items is None:
58
+ return []
59
+ if isinstance(items, dict) and "item" in items:
60
+ inner = items.get("item")
61
+ if inner is None:
62
+ return []
63
+ if isinstance(inner, list):
64
+ return [i for i in inner if isinstance(i, dict)]
65
+ return [inner] if isinstance(inner, dict) else []
66
+ if isinstance(items, list):
67
+ return [i for i in items if isinstance(i, dict)]
68
+ return [items] if isinstance(items, dict) else []
69
+
70
+ items = extract_items(items_obj)
71
+ if not items:
72
+ return []
73
+
74
+ # 아래 항목들은 it에 존재하지만, 현재 라이브러리 목적상 파싱하지 않음
75
+ # "isinCd": it.get("isinCd"), # ISIN 종목번호, BasiInfo에서 이미 있음(추후 복원 가능하도록 주석 처리)
76
+ # "basDt": it.get("basDt"), # 채권의 기준일, BasiInfo에서 이미 있음(추후 복원 가능하도록 주석 처리)
77
+ # "crno": it.get("crno"), # 기업의 법인등록번호, BasiInfo에서 이미 있음(추후 복원 가능하도록 주석 처리)
78
+ # "bondIsurNm": it.get("bondIsurNm"), # 채권의 발행사명, BasiInfo에서 이미 있음(추후 복원 가능하도록 주석 처리)
79
+ # "isinCdNm": it.get("isinCdNm"), # ISIN 종목명, BasiInfo에서 이미 있음(추후 복원 가능하도록 주석 처리)
80
+
81
+ out: list[dict[str, Any]] = []
82
+ for it in items:
83
+ isin = it.get("isinCd")
84
+ if not isin:
85
+ continue
86
+ if isinCdKey and isin != isinCdKey:
87
+ continue
88
+ out.append(
89
+ {
90
+ "isinCd": isin,
91
+ "type": it.get("optnTcdNm"),
92
+ "dates": [
93
+ it.get("optnExertSttgDt"),
94
+ it.get("optnExertEdDt"),
95
+ it.get("clrdDt"),
96
+ ],
97
+ }
98
+ )
99
+
100
+ return out
@@ -0,0 +1,75 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from .base_reader import get_datagokr_document, normalize_name
6
+
7
+ def get_IssuIssuItemStat(serviceKey: str, bondIsurNm: str, timeout_seconds: float = 60.0) -> Any | None:
8
+ """ 발행인에 따른 채권 조회. """
9
+ service_url = "1160100/service/GetBondTradInfoService/getIssuIssuItemStat"
10
+
11
+ params: dict[str, Any] = {
12
+ "serviceKey": serviceKey,
13
+ "bondIsurNm": bondIsurNm
14
+ }
15
+ return get_datagokr_document(
16
+ serviceUrl=service_url,
17
+ params=params,
18
+ timeout_seconds=timeout_seconds
19
+ )
20
+
21
+
22
+ def parse_issu_issu_item_stat(
23
+ serviceKey: str,
24
+ bondIsurNm: str,
25
+ bondNm: str,
26
+ timeout_seconds: float = 60.0,
27
+ raw: Any | None = None,
28
+ ) -> dict[str, Any] | None:
29
+ """ Parse get_IssuIssuItemStat results. """
30
+ if raw is None:
31
+ raw = get_IssuIssuItemStat(serviceKey=serviceKey, bondIsurNm=bondIsurNm, timeout_seconds=timeout_seconds)
32
+ if raw is None:
33
+ return None
34
+
35
+ try:
36
+ body = raw["response"]["body"]
37
+ items_obj = body.get("items")
38
+ except Exception:
39
+ return None
40
+
41
+ def extract_items(items: Any) -> list[dict[str, Any]]:
42
+ if items is None:
43
+ return []
44
+ if isinstance(items, dict) and "item" in items:
45
+ inner = items.get("item")
46
+ if inner is None:
47
+ return []
48
+ if isinstance(inner, list):
49
+ return [i for i in inner if isinstance(i, dict)]
50
+ return [inner] if isinstance(inner, dict) else []
51
+ if isinstance(items, list):
52
+ return [i for i in items if isinstance(i, dict)]
53
+ return [items] if isinstance(items, dict) else []
54
+
55
+ items = extract_items(items_obj)
56
+ if not items:
57
+ return None
58
+ for it in items:
59
+ normalized_isinCdNm = normalize_name(it.get("isinCdNm"))
60
+ normalized_bondNm = normalize_name(bondNm)
61
+ if normalized_isinCdNm == normalized_bondNm:
62
+ result = {
63
+ "basDt": it.get("basDt"),
64
+ "crno": it.get("crno"),
65
+ "isinCd": it.get("isinCd"),
66
+
67
+ "bondExprDt": it.get("bondExprDt"),
68
+ "bondIssuDt": it.get("bondIssuDt"),
69
+ "bondIssuAmt": it.get("bondIssuAmt"),
70
+
71
+ "isinCdNm": it.get("isinCdNm"),
72
+ "bondPymtAmt": it.get("bondPymtAmt"),
73
+ }
74
+ return result
75
+ return None
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from .base_reader import get_datagokr_document
6
+
7
+
8
+ def get_OptiExer(serviceKey: str, crno: str, timeout_seconds: float = 60.0) -> str | None:
9
+ """ 옵션 실제 행사내역 다운로드. """
10
+ service_url = "1160100/service/GetBondRedeInfoService/getOptiExer"
11
+ if not crno:
12
+ raise ValueError("crno가 없습니다.")
13
+
14
+ params: dict[str, Any] = {
15
+ "serviceKey": serviceKey,
16
+ "crno": crno,
17
+ }
18
+ return get_datagokr_document(
19
+ serviceUrl=service_url,
20
+ params=params,
21
+ timeout_seconds=timeout_seconds
22
+ )
23
+
24
+
25
+ def parse_opti_exer(
26
+ serviceKey: str,
27
+ crno: str,
28
+ timeout_seconds: float = 60.0,
29
+ raw: Any | None = None,
30
+ isinCdKey: str | None = None,
31
+ ) -> list[dict[str, Any]] | None:
32
+ """ Parse get_OptiExer results. """
33
+ if raw is None:
34
+ raw = get_OptiExer(serviceKey=serviceKey, crno=crno, timeout_seconds=timeout_seconds)
35
+ if raw is None:
36
+ return None
37
+
38
+ try:
39
+ body = raw["response"]["body"]
40
+ items_obj = body.get("items")
41
+ except Exception:
42
+ return None
43
+
44
+ def extract_items(items: Any) -> list[dict[str, Any]]:
45
+ if items is None:
46
+ return []
47
+ if isinstance(items, dict) and "item" in items:
48
+ inner = items.get("item")
49
+ if inner is None:
50
+ return []
51
+ if isinstance(inner, list):
52
+ return [i for i in inner if isinstance(i, dict)]
53
+ return [inner] if isinstance(inner, dict) else []
54
+ if isinstance(items, list):
55
+ return [i for i in items if isinstance(i, dict)]
56
+ return [items] if isinstance(items, dict) else []
57
+
58
+ items = extract_items(items_obj)
59
+ if not items:
60
+ return []
61
+
62
+ # 아래 항목들은 it에 존재하지만, 현재 라이브러리 목적상 파싱하지 않음
63
+ # "basDt": it.get("basDt"), # 채권의 기준일, BasiInfo에서 이미 있음(추후 복원 가능하도록 주석 처리)
64
+ # "crno": it.get("crno"), # 기업의 법인등록번호, BasiInfo에서 이미 있음(추후 복원 가능하도록 주석 처리)
65
+ # "isinCdNm": it.get("isinCdNm"), # ISIN 종목명, BasiInfo에서 이미 있음(추후 복원 가능하도록 주석 처리)
66
+ # "scrsIsurNm": it.get("scrsIsurNm"), # 채권의 발행사명, BasiInfo에서 이미 있음(추후 복원 가능하도록 주석 처리)
67
+ # "trgtStckIsinCd": it.get("trgtStckIsinCd"), # 대상 주식의 ISIN 종목번호, 현재 프로젝트에서 사용하지 않음(추후 복원 가능하도록 주석 처리)
68
+ # "trgtStckIsinCdNm": it.get("trgtStckIsinCdNm"), # 대상 주식의 ISIN 종목명, 현재 프로젝트에서 사용하지 않음(추후 복원 가능하도록 주석 처리)
69
+
70
+ out: list[dict[str, Any]] = []
71
+ for it in items:
72
+ isin = it.get("isinCd")
73
+ if not isin:
74
+ continue
75
+ if isinCdKey and isin != isinCdKey:
76
+ continue
77
+ out.append(
78
+ {
79
+ "isinCd": isin,
80
+ "optnExertPrc": it.get("optnExertPrc"),
81
+ "rgtOccrBasDt": it.get("rgtOccrBasDt"),
82
+ "exertPric": it.get("exertPric"),
83
+ "exertAmt": it.get("exertAmt"),
84
+ }
85
+ )
86
+
87
+ # items 에 "rgtOccrBasDt": "00010101" 하나만 있으면 items 를 None 으로 처리
88
+ out = [x for x in out if (x.get("rgtOccrBasDt") != "00010101")]
89
+ return out
@@ -0,0 +1,101 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from .base_reader import get_datagokr_document
6
+
7
+
8
+ def get_OptiExerPricAdju(
9
+ serviceKey: str,
10
+ crno: str,
11
+ timeout_seconds: float = 60.0,
12
+ numOfRows: int = 100,
13
+ resultType: str = "json",
14
+ ) -> Any | None:
15
+ """ 행사가 변동내역 및 현재 행사가 다운로드. """
16
+ service_url = "1160100/service/GetBondRedeInfoService/getOptiExerPricAdju"
17
+ if not crno:
18
+ raise ValueError("crno가 없습니다.")
19
+
20
+ params: dict[str, Any] = {
21
+ "serviceKey": serviceKey,
22
+ "crno": crno,
23
+ }
24
+
25
+ return get_datagokr_document(
26
+ serviceUrl=service_url,
27
+ params=params,
28
+ timeout_seconds=timeout_seconds,
29
+ numOfRows=numOfRows,
30
+ resultType=resultType,
31
+ )
32
+
33
+
34
+ def parse_opti_exer_pric_adju(
35
+ serviceKey: str,
36
+ crno: str,
37
+ timeout_seconds: float = 60.0,
38
+ raw: Any | None = None,
39
+ isinCdKey: str | None = None,
40
+ ) -> list[dict[str, Any]] | None:
41
+ """ Parse get_OptiExerPricAdju results. """
42
+ if raw is None:
43
+ raw = get_OptiExerPricAdju(
44
+ serviceKey=serviceKey,
45
+ crno=crno,
46
+ timeout_seconds=timeout_seconds,
47
+ )
48
+ if raw is None:
49
+ return None
50
+
51
+ try:
52
+ body = raw["response"]["body"]
53
+ items_obj = body.get("items")
54
+ except Exception:
55
+ return None
56
+
57
+ def extract_items(items: Any) -> list[dict[str, Any]]:
58
+ if items is None:
59
+ return []
60
+ if isinstance(items, dict) and "item" in items:
61
+ inner = items.get("item")
62
+ if inner is None:
63
+ return []
64
+ if isinstance(inner, list):
65
+ return [i for i in inner if isinstance(i, dict)]
66
+ return [inner] if isinstance(inner, dict) else []
67
+ if isinstance(items, list):
68
+ return [i for i in items if isinstance(i, dict)]
69
+ return [items] if isinstance(items, dict) else []
70
+
71
+ items = extract_items(items_obj)
72
+ if not items:
73
+ return []
74
+
75
+ out: list[dict[str, Any]] = []
76
+ # "trgtStckIsinCd": it.get("trgtStckIsinCd"), # 대상 주식의 ISIN 종목번호, 현재 프로젝트에서 사용하지 않음(추후 복원 가능하도록 주석 처리)
77
+ # "trgtStckIsinCdNm": it.get("trgtStckIsinCdNm"), # 대상 주식의 ISIN 종목명, 현재 프로젝트에서 사용하지 않음(추후 복원 가능하도록 주석 처리)
78
+ for it in items:
79
+ isin = it.get("isinCd")
80
+ if not isin:
81
+ continue
82
+ if isinCdKey and isin != isinCdKey:
83
+ continue
84
+ out.append(
85
+ {
86
+ "isinCd": isin,
87
+ "basDt": it.get("basDt"),
88
+ "crno": it.get("crno"),
89
+ "isinCdNm": it.get("isinCdNm"),
90
+ "scrsIsurNm": it.get("scrsIsurNm"),
91
+ "optnExertPrc": it.get("optnExertPrc"),
92
+ "rgtExertPricAdjDt": it.get("rgtExertPricAdjDt"),
93
+ "rgtBchgExertPrc": it.get("rgtBchgExertPrc"),
94
+ "rgtAchgExertPrc": it.get("rgtAchgExertPrc"),
95
+ }
96
+ )
97
+
98
+ # items 에 "rgtExertPricAdjDt": "00010101" 하나만 있으면 items 를 None 으로 처리
99
+ out = [x for x in out if (x.get("rgtExertPricAdjDt") != "00010101")]
100
+ return out
101
+
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ from datagokr_reader import (
4
+ parse_bond_with_opti_call_rede,
5
+ parse_earl_exer_opti,
6
+ parse_issu_issu_item_stat,
7
+ parse_opti_exer,
8
+ parse_opti_exer_pric_adju,
9
+ )
10
+ from datagokr_reader.base_reader import issuer_name_from_bond_name, normalize_name
11
+
12
+
13
+ def run_bond_workflow(datagokr_api_key: str, bond_name: str):
14
+ """
15
+ 채권명 to {"함수명": 결과, ...} dict
16
+ """
17
+ bond_name_normalized = normalize_name(bond_name)
18
+ issuer_name = issuer_name_from_bond_name(bond_name)
19
+
20
+ issu = parse_issu_issu_item_stat(
21
+ serviceKey=datagokr_api_key,
22
+ bondIsurNm=issuer_name,
23
+ bondNm=bond_name_normalized,
24
+ )
25
+
26
+ crno = issu.get("crno") if isinstance(issu, dict) else ""
27
+ isinCdKey = issu.get("isinCd") if isinstance(issu, dict) else None
28
+ basDt = issu.get("basDt") if isinstance(issu, dict) else None
29
+
30
+ bond_with = parse_bond_with_opti_call_rede(
31
+ serviceKey=datagokr_api_key,
32
+ crno=crno,
33
+ opbdIsurNm=issuer_name,
34
+ isinCdKey=isinCdKey,
35
+ )
36
+ opti_exer = parse_opti_exer(
37
+ serviceKey=datagokr_api_key,
38
+ crno=crno,
39
+ isinCdKey=isinCdKey,
40
+ )
41
+ opti_adju = parse_opti_exer_pric_adju(
42
+ serviceKey=datagokr_api_key,
43
+ crno=crno,
44
+ isinCdKey=isinCdKey,
45
+ )
46
+ earl = parse_earl_exer_opti(
47
+ serviceKey=datagokr_api_key,
48
+ basDt=basDt,
49
+ bondIsurNm=issuer_name,
50
+ crno=crno,
51
+ isinCdKey=isinCdKey,
52
+ )
53
+
54
+ return {
55
+ "parse_issu_issu_item_stat": issu,
56
+ "parse_bond_with_opti_call_rede": bond_with,
57
+ "parse_opti_exer": opti_exer,
58
+ "parse_opti_exer_pric_adju": opti_adju,
59
+ "parse_earl_exer_opti": earl,
60
+ }
61
+
@@ -0,0 +1,25 @@
1
+ Metadata-Version: 2.4
2
+ Name: datagokr_reader
3
+ Version: 1.0.0
4
+ Summary: data.go.kr의 채권 관련 API를 사용하기 편하게 만든 라이브러리입니다.
5
+ Author: WONWOO LEE
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://example.com
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: httpx>=0.27
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=8.0; extra == "dev"
13
+
14
+ ## DatagokrReader
15
+
16
+ data.go.kr의 채권 관련 API를 사용하기 편하게 만든 라이브러리입니다.
17
+
18
+ ### Example
19
+
20
+ ```python
21
+ from datagokr_reader import run_bond_workflow
22
+
23
+ # 띄어쓰기 및 대소문자 무관, 채권 관련 주요 데이터 모두 정리
24
+ result = run_bond_workflow(datagokr_api_key, "공공데이터포털1EB")
25
+ ```
@@ -0,0 +1,18 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/datagokr_reader/__init__.py
4
+ src/datagokr_reader/base_reader.py
5
+ src/datagokr_reader/cli.py
6
+ src/datagokr_reader/getBondBasiInfo.py
7
+ src/datagokr_reader/getBondWithOptiCallRede.py
8
+ src/datagokr_reader/getEarlExerOpti.py
9
+ src/datagokr_reader/getIssuIssuItemStat.py
10
+ src/datagokr_reader/getOptiExer.py
11
+ src/datagokr_reader/getOptiExerPricAdju.py
12
+ src/datagokr_reader/workflow.py
13
+ src/datagokr_reader.egg-info/PKG-INFO
14
+ src/datagokr_reader.egg-info/SOURCES.txt
15
+ src/datagokr_reader.egg-info/dependency_links.txt
16
+ src/datagokr_reader.egg-info/entry_points.txt
17
+ src/datagokr_reader.egg-info/requires.txt
18
+ src/datagokr_reader.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ datagokr-reader = datagokr_reader.cli:main
@@ -0,0 +1,4 @@
1
+ httpx>=0.27
2
+
3
+ [dev]
4
+ pytest>=8.0
@@ -0,0 +1 @@
1
+ datagokr_reader