financechatbotkit 2.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.
Files changed (49) hide show
  1. financechatbotkit-2.0.0/PKG-INFO +11 -0
  2. financechatbotkit-2.0.0/README.md +52 -0
  3. financechatbotkit-2.0.0/pyproject.toml +30 -0
  4. financechatbotkit-2.0.0/setup.cfg +4 -0
  5. financechatbotkit-2.0.0/src/financechatbotkit.egg-info/PKG-INFO +11 -0
  6. financechatbotkit-2.0.0/src/financechatbotkit.egg-info/SOURCES.txt +47 -0
  7. financechatbotkit-2.0.0/src/financechatbotkit.egg-info/dependency_links.txt +1 -0
  8. financechatbotkit-2.0.0/src/financechatbotkit.egg-info/entry_points.txt +2 -0
  9. financechatbotkit-2.0.0/src/financechatbotkit.egg-info/requires.txt +6 -0
  10. financechatbotkit-2.0.0/src/financechatbotkit.egg-info/top_level.txt +2 -0
  11. financechatbotkit-2.0.0/src/orchestrator/__init__.py +29 -0
  12. financechatbotkit-2.0.0/src/orchestrator/bond/__init__.py +8 -0
  13. financechatbotkit-2.0.0/src/orchestrator/bond/base_reader.py +139 -0
  14. financechatbotkit-2.0.0/src/orchestrator/bond/getBondBasiInfo.py +84 -0
  15. financechatbotkit-2.0.0/src/orchestrator/bond/getBondWithOptiCallRede.py +83 -0
  16. financechatbotkit-2.0.0/src/orchestrator/bond/getEarlExerOpti.py +90 -0
  17. financechatbotkit-2.0.0/src/orchestrator/bond/getIssuIssuItemStat.py +85 -0
  18. financechatbotkit-2.0.0/src/orchestrator/bond/getOptiExer.py +83 -0
  19. financechatbotkit-2.0.0/src/orchestrator/bond/getOptiExerPricAdju.py +84 -0
  20. financechatbotkit-2.0.0/src/orchestrator/bond/workflow.py +252 -0
  21. financechatbotkit-2.0.0/src/orchestrator/exceptions.py +17 -0
  22. financechatbotkit-2.0.0/src/orchestrator/fnguide/__init__.py +21 -0
  23. financechatbotkit-2.0.0/src/orchestrator/fnguide/workflow.py +391 -0
  24. financechatbotkit-2.0.0/src/orchestrator/mapping/__init__.py +22 -0
  25. financechatbotkit-2.0.0/src/orchestrator/mapping/data/__init__.py +1 -0
  26. financechatbotkit-2.0.0/src/orchestrator/mapping/data/corp_codes_raw.json +693170 -0
  27. financechatbotkit-2.0.0/src/orchestrator/mapping/update_raw_data.py +96 -0
  28. financechatbotkit-2.0.0/src/orchestrator/mapping/workflow.py +303 -0
  29. financechatbotkit-2.0.0/src/orchestrator/price/__init__.py +15 -0
  30. financechatbotkit-2.0.0/src/orchestrator/price/workflow.py +250 -0
  31. financechatbotkit-2.0.0/src/telebotkit/__init__.py +51 -0
  32. financechatbotkit-2.0.0/src/telebotkit/bot/__init__.py +38 -0
  33. financechatbotkit-2.0.0/src/telebotkit/bot/client.py +217 -0
  34. financechatbotkit-2.0.0/src/telebotkit/bot/reply.py +36 -0
  35. financechatbotkit-2.0.0/src/telebotkit/bot/router.py +125 -0
  36. financechatbotkit-2.0.0/src/telebotkit/bot/safety.py +28 -0
  37. financechatbotkit-2.0.0/src/telebotkit/bot/telegram.py +41 -0
  38. financechatbotkit-2.0.0/src/telebotkit/firestore/__init__.py +45 -0
  39. financechatbotkit-2.0.0/src/telebotkit/firestore/client.py +141 -0
  40. financechatbotkit-2.0.0/src/telebotkit/firestore/documents.py +164 -0
  41. financechatbotkit-2.0.0/src/telebotkit/firestore/fetch.py +228 -0
  42. financechatbotkit-2.0.0/src/telebotkit/firestore/locks.py +74 -0
  43. financechatbotkit-2.0.0/src/telebotkit/firestore/upload.py +75 -0
  44. financechatbotkit-2.0.0/src/telebotkit/sheets.py +219 -0
  45. financechatbotkit-2.0.0/tests/test_bond_workflow.py +93 -0
  46. financechatbotkit-2.0.0/tests/test_fnguide_workflow.py +132 -0
  47. financechatbotkit-2.0.0/tests/test_price_workflow.py +170 -0
  48. financechatbotkit-2.0.0/tests/test_update_raw_data.py +81 -0
  49. financechatbotkit-2.0.0/tests/test_workflow.py +158 -0
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: financechatbotkit
3
+ Version: 2.0.0
4
+ Summary: FinanceChatbot workflows (orchestrator) and Telegram/Firestore helpers (TeleBotKit)
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: finance-datareader==0.9.110
7
+ Requires-Dist: httpx<1,>=0.27
8
+ Requires-Dist: google-cloud-firestore>=2.16.0
9
+ Requires-Dist: google-api-core>=2.15.0
10
+ Requires-Dist: google-auth>=2.28.0
11
+ Requires-Dist: openpyxl>=3.1.0
@@ -0,0 +1,52 @@
1
+ ## FinanceChatbot
2
+
3
+ Unified finance workflow library (orchestrator) for:
4
+
5
+ - `mapping`
6
+ - `price`
7
+ - `fnguide`
8
+ - `bond`
9
+
10
+ ### Public API
11
+
12
+ Each feature lives at the same level:
13
+
14
+ ```python
15
+ from orchestrator.mapping import MappingWorkflow
16
+ from orchestrator.price import PricePeriodWorkflow
17
+ from orchestrator.fnguide import FnGuideWorkflow
18
+ from orchestrator.bond import MarketBondWorkflow
19
+ ```
20
+
21
+ All workflows return:
22
+
23
+ ```python
24
+ {
25
+ "input": {...},
26
+ "data": {...},
27
+ }
28
+ ```
29
+
30
+ `price` workflows support `price_fields="close"` (default), `price_fields="oclh"`, and `price_fields="oclhv"` to control which price columns are returned.
31
+
32
+ ---
33
+
34
+ ## TeleBotKit
35
+
36
+ `TeleBotKit` is a shared utility library that lives under `src/telebotkit` and is packaged together with this project.
37
+
38
+ It provides helpers for:
39
+
40
+ - Telegram bot command parsing, routing, MarkdownV2 escaping, and reply payload generation (`telebotkit.bot`)
41
+ - Firestore client bootstrap, typed repositories, shared document access, and lease/lock helpers (`telebotkit.firestore`)
42
+ - Excel row parsing and typed JSON payload generation for Firestore imports (`telebotkit.sheets`)
43
+
44
+ ### Example usage
45
+
46
+ ```python
47
+ from telebotkit.bot import Reply, Router
48
+ from telebotkit.firestore import DocumentStore, get_client
49
+ from telebotkit.sheets import build_typed_rows_payload_from_xlsx
50
+ ```
51
+
52
+ See `src/telebotkit/README.md` for full API documentation and examples.
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "financechatbotkit"
7
+ version = "2.0.0"
8
+ description = "FinanceChatbot workflows (orchestrator) and Telegram/Firestore helpers (TeleBotKit)"
9
+ requires-python = ">=3.11"
10
+ dependencies = [
11
+ "finance-datareader==0.9.110",
12
+ "httpx>=0.27,<1",
13
+ "google-cloud-firestore>=2.16.0",
14
+ "google-api-core>=2.15.0",
15
+ "google-auth>=2.28.0",
16
+ "openpyxl>=3.1.0",
17
+ ]
18
+
19
+ [project.scripts]
20
+ orchestrator-update-corp-codes = "orchestrator.mapping.update_raw_data:main"
21
+
22
+ [tool.setuptools]
23
+ package-dir = {"" = "src"}
24
+
25
+ [tool.setuptools.packages.find]
26
+ where = ["src"]
27
+ include = ["orchestrator*", "telebotkit*"]
28
+
29
+ [tool.setuptools.package-data]
30
+ "orchestrator.mapping" = ["data/*.json"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: financechatbotkit
3
+ Version: 2.0.0
4
+ Summary: FinanceChatbot workflows (orchestrator) and Telegram/Firestore helpers (TeleBotKit)
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: finance-datareader==0.9.110
7
+ Requires-Dist: httpx<1,>=0.27
8
+ Requires-Dist: google-cloud-firestore>=2.16.0
9
+ Requires-Dist: google-api-core>=2.15.0
10
+ Requires-Dist: google-auth>=2.28.0
11
+ Requires-Dist: openpyxl>=3.1.0
@@ -0,0 +1,47 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/financechatbotkit.egg-info/PKG-INFO
4
+ src/financechatbotkit.egg-info/SOURCES.txt
5
+ src/financechatbotkit.egg-info/dependency_links.txt
6
+ src/financechatbotkit.egg-info/entry_points.txt
7
+ src/financechatbotkit.egg-info/requires.txt
8
+ src/financechatbotkit.egg-info/top_level.txt
9
+ src/orchestrator/__init__.py
10
+ src/orchestrator/exceptions.py
11
+ src/orchestrator/bond/__init__.py
12
+ src/orchestrator/bond/base_reader.py
13
+ src/orchestrator/bond/getBondBasiInfo.py
14
+ src/orchestrator/bond/getBondWithOptiCallRede.py
15
+ src/orchestrator/bond/getEarlExerOpti.py
16
+ src/orchestrator/bond/getIssuIssuItemStat.py
17
+ src/orchestrator/bond/getOptiExer.py
18
+ src/orchestrator/bond/getOptiExerPricAdju.py
19
+ src/orchestrator/bond/workflow.py
20
+ src/orchestrator/fnguide/__init__.py
21
+ src/orchestrator/fnguide/workflow.py
22
+ src/orchestrator/mapping/__init__.py
23
+ src/orchestrator/mapping/update_raw_data.py
24
+ src/orchestrator/mapping/workflow.py
25
+ src/orchestrator/mapping/data/__init__.py
26
+ src/orchestrator/mapping/data/corp_codes_raw.json
27
+ src/orchestrator/price/__init__.py
28
+ src/orchestrator/price/workflow.py
29
+ src/telebotkit/__init__.py
30
+ src/telebotkit/sheets.py
31
+ src/telebotkit/bot/__init__.py
32
+ src/telebotkit/bot/client.py
33
+ src/telebotkit/bot/reply.py
34
+ src/telebotkit/bot/router.py
35
+ src/telebotkit/bot/safety.py
36
+ src/telebotkit/bot/telegram.py
37
+ src/telebotkit/firestore/__init__.py
38
+ src/telebotkit/firestore/client.py
39
+ src/telebotkit/firestore/documents.py
40
+ src/telebotkit/firestore/fetch.py
41
+ src/telebotkit/firestore/locks.py
42
+ src/telebotkit/firestore/upload.py
43
+ tests/test_bond_workflow.py
44
+ tests/test_fnguide_workflow.py
45
+ tests/test_price_workflow.py
46
+ tests/test_update_raw_data.py
47
+ tests/test_workflow.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ orchestrator-update-corp-codes = orchestrator.mapping.update_raw_data:main
@@ -0,0 +1,6 @@
1
+ finance-datareader==0.9.110
2
+ httpx<1,>=0.27
3
+ google-cloud-firestore>=2.16.0
4
+ google-api-core>=2.15.0
5
+ google-auth>=2.28.0
6
+ openpyxl>=3.1.0
@@ -0,0 +1,2 @@
1
+ orchestrator
2
+ telebotkit
@@ -0,0 +1,29 @@
1
+ """FinanceChatbot workflow library."""
2
+
3
+ from .bond import MarketBondWorkflow, run_market_bond_workflow
4
+ from .exceptions import DownloadError, InvalidInputError, MappingWorkflowError, NotFoundError
5
+ from .fnguide import FnGuideWorkflow, run_fnguide_workflow
6
+ from .mapping import MappingWorkflow, run_mapping_workflow
7
+ from .price import (
8
+ PricePeriodWorkflow,
9
+ PriceSnapshotWorkflow,
10
+ run_price_period_workflow,
11
+ run_price_snapshot_workflow,
12
+ )
13
+
14
+ __all__ = [
15
+ "DownloadError",
16
+ "FnGuideWorkflow",
17
+ "InvalidInputError",
18
+ "MappingWorkflow",
19
+ "MappingWorkflowError",
20
+ "MarketBondWorkflow",
21
+ "NotFoundError",
22
+ "PricePeriodWorkflow",
23
+ "PriceSnapshotWorkflow",
24
+ "run_fnguide_workflow",
25
+ "run_market_bond_workflow",
26
+ "run_mapping_workflow",
27
+ "run_price_period_workflow",
28
+ "run_price_snapshot_workflow",
29
+ ]
@@ -0,0 +1,8 @@
1
+ """Bond workflows for FinanceChatbot."""
2
+
3
+ from .workflow import MarketBondWorkflow, run_market_bond_workflow
4
+
5
+ __all__ = [
6
+ "MarketBondWorkflow",
7
+ "run_market_bond_workflow",
8
+ ]
@@ -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,84 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from .base_reader import get_datagokr_document
6
+
7
+ """
8
+ Read https://www.data.go.kr/data/15059592/openapi.do
9
+ """
10
+
11
+ def get_BondBasiInfo(
12
+ serviceKey: str,
13
+ basDt: str,
14
+ isinCd: str,
15
+ timeout_seconds: float = 60.0,
16
+ ) -> Any | None:
17
+ """ 채권 기초정보 다운로드. """
18
+ if not basDt or not isinCd:
19
+ return None
20
+
21
+ service_url = "1160100/service/GetBondIssuInfoService/getBondBasiInfo"
22
+ params: dict[str, Any] = {
23
+ "serviceKey": serviceKey,
24
+ "basDt": basDt,
25
+ "isinCd": isinCd,
26
+ }
27
+
28
+ return get_datagokr_document(
29
+ serviceUrl=service_url,
30
+ params=params,
31
+ timeout_seconds=timeout_seconds
32
+ )
33
+
34
+
35
+ def parse_bond_basi_info(
36
+ serviceKey: str,
37
+ basDt: str,
38
+ isinCd: str,
39
+ timeout_seconds: float = 60.0,
40
+ ) -> list[dict[str, Any]] | None:
41
+ """ Parse get_BondBasiInfo results. """
42
+ raw = get_BondBasiInfo(
43
+ serviceKey=serviceKey,
44
+ basDt=basDt,
45
+ isinCd=isinCd,
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
+ for it in items:
77
+ out.append(
78
+ {
79
+ "발행잔액": it.get("bondBal", ""),
80
+ }
81
+ )
82
+
83
+ return out
84
+
@@ -0,0 +1,83 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from .base_reader import get_datagokr_document
6
+
7
+ """
8
+ Read https://www.data.go.kr/data/15059595/openapi.do
9
+ """
10
+
11
+ def get_BondWithOptiCallRede(
12
+ serviceKey: str,
13
+ isinCd: str,
14
+ timeout_seconds: float = 60.0,
15
+ ) -> Any | None:
16
+ """ 옵션 행사내역 다운로드. """
17
+ if not isinCd:
18
+ return None
19
+
20
+ service_url = "1160100/service/GetBondRedeInfoService/getBondWithOptiCallRede"
21
+ params: dict[str, Any] = {
22
+ "serviceKey": serviceKey,
23
+ "isinCd": isinCd,
24
+ }
25
+
26
+ return get_datagokr_document(
27
+ serviceUrl=service_url,
28
+ params=params,
29
+ timeout_seconds=timeout_seconds
30
+ )
31
+
32
+
33
+ def parse_bond_with_opti_call_rede(
34
+ serviceKey: str,
35
+ isinCd: str,
36
+ timeout_seconds: float = 60.0,
37
+ ) -> list[dict[str, Any]] | None:
38
+ """ Parse get_BondWithOptiCallRede results. """
39
+ raw = get_BondWithOptiCallRede(
40
+ serviceKey=serviceKey,
41
+ isinCd=isinCd,
42
+ timeout_seconds=timeout_seconds,
43
+ )
44
+ if raw is None:
45
+ return None
46
+
47
+ try:
48
+ body = raw["response"]["body"]
49
+ items_obj = body.get("items")
50
+ except Exception:
51
+ return None
52
+
53
+ def extract_items(items: Any) -> list[dict[str, Any]]:
54
+ if items is None:
55
+ return []
56
+ if isinstance(items, dict) and "item" in items:
57
+ inner = items.get("item")
58
+ if inner is None:
59
+ return []
60
+ if isinstance(inner, list):
61
+ return [i for i in inner if isinstance(i, dict)]
62
+ return [inner] if isinstance(inner, dict) else []
63
+ if isinstance(items, list):
64
+ return [i for i in items if isinstance(i, dict)]
65
+ return [items] if isinstance(items, dict) else []
66
+
67
+ items = extract_items(items_obj)
68
+ if not items:
69
+ return []
70
+
71
+ out: list[dict[str, Any]] = []
72
+ for it in items:
73
+ out.append(
74
+ {
75
+ "옵션분류": it.get("optnTcdNm", ""),
76
+ "행사일자": it.get("opbdClrdDt", ""),
77
+ "행사원금": it.get("opbdPamtPayAmt", ""),
78
+ "행사이자": it.get("opbdIntPayAmt", ""),
79
+ }
80
+ )
81
+
82
+ return out
83
+
@@ -0,0 +1,90 @@
1
+ from __future__ import annotations
2
+ from typing import Any
3
+ from .base_reader import get_datagokr_document
4
+
5
+
6
+ """
7
+ Read https://www.data.go.kr/data/15059595/openapi.do
8
+ """
9
+
10
+
11
+ def get_EarlExerOpti(
12
+ serviceKey: str,
13
+ basDt: str,
14
+ isinCd: str,
15
+ timeout_seconds: float = 60.0,
16
+ ) -> Any | None:
17
+ """
18
+ 옵션 행사 일정 다운로드. 콜옵션은 대부분의 경우 누락되니 주의.
19
+ basDt가 없는 경우 수만 개에 달하기 때문에 사실상 사용 불가능.
20
+ """
21
+ if not basDt or not isinCd:
22
+ return None
23
+
24
+ service_url = "1160100/service/GetBondRedeInfoService/getEarlExerOpti"
25
+ params: dict[str, Any] = {
26
+ "serviceKey": serviceKey,
27
+ "basDt": basDt,
28
+ "isinCd": isinCd,
29
+ }
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,
40
+ isinCd: str,
41
+ timeout_seconds: float = 60.0,
42
+ ) -> list[dict[str, Any]] | None:
43
+ """ Parse get_EarlExerOpti results. """
44
+ raw = get_EarlExerOpti(
45
+ serviceKey=serviceKey,
46
+ basDt=basDt,
47
+ isinCd=isinCd,
48
+ timeout_seconds=timeout_seconds
49
+ )
50
+ if raw is None:
51
+ return None
52
+
53
+ try:
54
+ body = raw["response"]["body"]
55
+ items_obj = body.get("items")
56
+ except Exception:
57
+ return None
58
+
59
+ def extract_items(items: Any) -> list[dict[str, Any]]:
60
+ if items is None:
61
+ return []
62
+ if isinstance(items, dict) and "item" in items:
63
+ inner = items.get("item")
64
+ if inner is None:
65
+ return []
66
+ if isinstance(inner, list):
67
+ return [i for i in inner if isinstance(i, dict)]
68
+ return [inner] if isinstance(inner, dict) else []
69
+ if isinstance(items, list):
70
+ return [i for i in items if isinstance(i, dict)]
71
+ return [items] if isinstance(items, dict) else []
72
+
73
+ items = extract_items(items_obj)
74
+ if not items:
75
+ return []
76
+
77
+ out: list[dict[str, Any]] = []
78
+ for it in items:
79
+ out.append(
80
+ {
81
+ "옵션분류": it.get("optnTcdNm", ""),
82
+ "행사일정": [
83
+ it.get("옵션청구시작일", ""),
84
+ it.get("옵션청구종료일", ""),
85
+ it.get("지급일", ""),
86
+ ],
87
+ }
88
+ )
89
+
90
+ return out