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.
- financechatbotkit-2.0.0/PKG-INFO +11 -0
- financechatbotkit-2.0.0/README.md +52 -0
- financechatbotkit-2.0.0/pyproject.toml +30 -0
- financechatbotkit-2.0.0/setup.cfg +4 -0
- financechatbotkit-2.0.0/src/financechatbotkit.egg-info/PKG-INFO +11 -0
- financechatbotkit-2.0.0/src/financechatbotkit.egg-info/SOURCES.txt +47 -0
- financechatbotkit-2.0.0/src/financechatbotkit.egg-info/dependency_links.txt +1 -0
- financechatbotkit-2.0.0/src/financechatbotkit.egg-info/entry_points.txt +2 -0
- financechatbotkit-2.0.0/src/financechatbotkit.egg-info/requires.txt +6 -0
- financechatbotkit-2.0.0/src/financechatbotkit.egg-info/top_level.txt +2 -0
- financechatbotkit-2.0.0/src/orchestrator/__init__.py +29 -0
- financechatbotkit-2.0.0/src/orchestrator/bond/__init__.py +8 -0
- financechatbotkit-2.0.0/src/orchestrator/bond/base_reader.py +139 -0
- financechatbotkit-2.0.0/src/orchestrator/bond/getBondBasiInfo.py +84 -0
- financechatbotkit-2.0.0/src/orchestrator/bond/getBondWithOptiCallRede.py +83 -0
- financechatbotkit-2.0.0/src/orchestrator/bond/getEarlExerOpti.py +90 -0
- financechatbotkit-2.0.0/src/orchestrator/bond/getIssuIssuItemStat.py +85 -0
- financechatbotkit-2.0.0/src/orchestrator/bond/getOptiExer.py +83 -0
- financechatbotkit-2.0.0/src/orchestrator/bond/getOptiExerPricAdju.py +84 -0
- financechatbotkit-2.0.0/src/orchestrator/bond/workflow.py +252 -0
- financechatbotkit-2.0.0/src/orchestrator/exceptions.py +17 -0
- financechatbotkit-2.0.0/src/orchestrator/fnguide/__init__.py +21 -0
- financechatbotkit-2.0.0/src/orchestrator/fnguide/workflow.py +391 -0
- financechatbotkit-2.0.0/src/orchestrator/mapping/__init__.py +22 -0
- financechatbotkit-2.0.0/src/orchestrator/mapping/data/__init__.py +1 -0
- financechatbotkit-2.0.0/src/orchestrator/mapping/data/corp_codes_raw.json +693170 -0
- financechatbotkit-2.0.0/src/orchestrator/mapping/update_raw_data.py +96 -0
- financechatbotkit-2.0.0/src/orchestrator/mapping/workflow.py +303 -0
- financechatbotkit-2.0.0/src/orchestrator/price/__init__.py +15 -0
- financechatbotkit-2.0.0/src/orchestrator/price/workflow.py +250 -0
- financechatbotkit-2.0.0/src/telebotkit/__init__.py +51 -0
- financechatbotkit-2.0.0/src/telebotkit/bot/__init__.py +38 -0
- financechatbotkit-2.0.0/src/telebotkit/bot/client.py +217 -0
- financechatbotkit-2.0.0/src/telebotkit/bot/reply.py +36 -0
- financechatbotkit-2.0.0/src/telebotkit/bot/router.py +125 -0
- financechatbotkit-2.0.0/src/telebotkit/bot/safety.py +28 -0
- financechatbotkit-2.0.0/src/telebotkit/bot/telegram.py +41 -0
- financechatbotkit-2.0.0/src/telebotkit/firestore/__init__.py +45 -0
- financechatbotkit-2.0.0/src/telebotkit/firestore/client.py +141 -0
- financechatbotkit-2.0.0/src/telebotkit/firestore/documents.py +164 -0
- financechatbotkit-2.0.0/src/telebotkit/firestore/fetch.py +228 -0
- financechatbotkit-2.0.0/src/telebotkit/firestore/locks.py +74 -0
- financechatbotkit-2.0.0/src/telebotkit/firestore/upload.py +75 -0
- financechatbotkit-2.0.0/src/telebotkit/sheets.py +219 -0
- financechatbotkit-2.0.0/tests/test_bond_workflow.py +93 -0
- financechatbotkit-2.0.0/tests/test_fnguide_workflow.py +132 -0
- financechatbotkit-2.0.0/tests/test_price_workflow.py +170 -0
- financechatbotkit-2.0.0/tests/test_update_raw_data.py +81 -0
- 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,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 @@
|
|
|
1
|
+
|
|
@@ -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,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
|