financechatbotkit 2.0.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.
Files changed (39) hide show
  1. financechatbotkit-2.0.0.dist-info/METADATA +11 -0
  2. financechatbotkit-2.0.0.dist-info/RECORD +39 -0
  3. financechatbotkit-2.0.0.dist-info/WHEEL +5 -0
  4. financechatbotkit-2.0.0.dist-info/entry_points.txt +2 -0
  5. financechatbotkit-2.0.0.dist-info/top_level.txt +2 -0
  6. orchestrator/__init__.py +29 -0
  7. orchestrator/bond/__init__.py +8 -0
  8. orchestrator/bond/base_reader.py +139 -0
  9. orchestrator/bond/getBondBasiInfo.py +84 -0
  10. orchestrator/bond/getBondWithOptiCallRede.py +83 -0
  11. orchestrator/bond/getEarlExerOpti.py +90 -0
  12. orchestrator/bond/getIssuIssuItemStat.py +85 -0
  13. orchestrator/bond/getOptiExer.py +83 -0
  14. orchestrator/bond/getOptiExerPricAdju.py +84 -0
  15. orchestrator/bond/workflow.py +252 -0
  16. orchestrator/exceptions.py +17 -0
  17. orchestrator/fnguide/__init__.py +21 -0
  18. orchestrator/fnguide/workflow.py +391 -0
  19. orchestrator/mapping/__init__.py +22 -0
  20. orchestrator/mapping/data/__init__.py +1 -0
  21. orchestrator/mapping/data/corp_codes_raw.json +693170 -0
  22. orchestrator/mapping/update_raw_data.py +96 -0
  23. orchestrator/mapping/workflow.py +303 -0
  24. orchestrator/price/__init__.py +15 -0
  25. orchestrator/price/workflow.py +250 -0
  26. telebotkit/__init__.py +51 -0
  27. telebotkit/bot/__init__.py +38 -0
  28. telebotkit/bot/client.py +217 -0
  29. telebotkit/bot/reply.py +36 -0
  30. telebotkit/bot/router.py +125 -0
  31. telebotkit/bot/safety.py +28 -0
  32. telebotkit/bot/telegram.py +41 -0
  33. telebotkit/firestore/__init__.py +45 -0
  34. telebotkit/firestore/client.py +141 -0
  35. telebotkit/firestore/documents.py +164 -0
  36. telebotkit/firestore/fetch.py +228 -0
  37. telebotkit/firestore/locks.py +74 -0
  38. telebotkit/firestore/upload.py +75 -0
  39. telebotkit/sheets.py +219 -0
@@ -0,0 +1,84 @@
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_OptiExerPricAdju(
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/getOptiExerPricAdju"
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_opti_exer_pric_adju(
34
+ serviceKey: str,
35
+ isinCd: str,
36
+ timeout_seconds: float = 60.0,
37
+ ) -> list[dict[str, Any]] | None:
38
+ """ Parse get_OptiExerPricAdju results. """
39
+ raw = get_OptiExerPricAdju(
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("optnExertPrc", ""),
76
+ "변경일": it.get("rgtExertPricAdjDt"),
77
+ "이전행사가": it.get("rgtBchgExertPrc"),
78
+ "이후행사가": it.get("rgtAchgExertPrc"),
79
+ }
80
+ )
81
+
82
+ # items 에 "rgtExertPricAdjDt": "00010101" 하나만 있으면 items 를 None 으로 처리
83
+ out = [x for x in out if (x.get("rgtExertPricAdjDt") != "00010101")]
84
+ return out
@@ -0,0 +1,252 @@
1
+ """Data.go.kr bond workflows included directly in FinanceChatbot."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from ..exceptions import InvalidInputError
10
+
11
+
12
+ def _empty_market_bond_payload(issue: dict[str, Any] | None = None) -> dict[str, Any]:
13
+ return {
14
+ "issue": issue,
15
+ "optionRedemptions": [],
16
+ "optionExercises": [],
17
+ "priceAdjustments": [],
18
+ "optionSchedules": [],
19
+ "bondBasicInfo": [],
20
+ }
21
+
22
+
23
+ def _compact_dict(value: dict[str, Any]) -> dict[str, Any]:
24
+ return {
25
+ key: item
26
+ for key, item in value.items()
27
+ if item is not None and item != "" and item != [] and item != {} and item != "-"
28
+ }
29
+
30
+
31
+ def _coerce_list(value: Any) -> list[dict[str, Any]]:
32
+ if not isinstance(value, list):
33
+ return []
34
+ return [item for item in value if isinstance(item, dict)]
35
+
36
+
37
+ def _normalize_items(items: Any, normalizer) -> list[dict[str, Any]]:
38
+ normalized_items: list[dict[str, Any]] = []
39
+ for item in _coerce_list(items):
40
+ normalized = normalizer(item)
41
+ if normalized:
42
+ normalized_items.append(normalized)
43
+ return normalized_items
44
+
45
+
46
+ def _normalize_issue(value: Any) -> dict[str, Any] | None:
47
+ if not isinstance(value, dict):
48
+ return None
49
+ normalized = _compact_dict(
50
+ {
51
+ "isinCdNm": value.get("isinCdNm"),
52
+ "isinCd": value.get("isinCd"),
53
+ "basDt": value.get("basDt") or value.get("기준일자"),
54
+ "bondIssuDt": value.get("bondIssuDt") or value.get("발행일"),
55
+ "bondExprDt": value.get("bondExprDt") or value.get("만기일"),
56
+ "bondIssuAmt": value.get("bondIssuAmt") or value.get("최초발행권면"),
57
+ "bondPymtAmt": value.get("bondPymtAmt") or value.get("최초납입금액"),
58
+ }
59
+ )
60
+ return normalized or None
61
+
62
+
63
+ def _normalize_redemption_item(item: dict[str, Any]) -> dict[str, Any] | None:
64
+ normalized = _compact_dict(
65
+ {
66
+ "optnTcdNm": item.get("optnTcdNm") or item.get("옵션분류"),
67
+ "opbdClrdDt": item.get("opbdClrdDt") or item.get("행사일자"),
68
+ "opbdPamtPayAmt": item.get("opbdPamtPayAmt") or item.get("행사원금"),
69
+ "opbdIntPayAmt": item.get("opbdIntPayAmt") or item.get("행사이자"),
70
+ "bondIssuAmt": item.get("bondIssuAmt") or item.get("발행잔액"),
71
+ }
72
+ )
73
+ return normalized or None
74
+
75
+
76
+ def _normalize_exercise_item(item: dict[str, Any]) -> dict[str, Any] | None:
77
+ normalized = _compact_dict(
78
+ {
79
+ "rgtOccrBasDt": item.get("rgtOccrBasDt") or item.get("행사일"),
80
+ "optnExertPrc": item.get("optnExertPrc") or item.get("현재행사단가"),
81
+ "exertPric": item.get("exertPric") or item.get("행사단가"),
82
+ "exertAmt": item.get("exertAmt") or item.get("행사금액"),
83
+ }
84
+ )
85
+ if normalized.get("rgtOccrBasDt") in {"00000000", "00010101"}:
86
+ return None
87
+ return normalized or None
88
+
89
+
90
+ def _normalize_adjustment_item(item: dict[str, Any]) -> dict[str, Any] | None:
91
+ normalized = _compact_dict(
92
+ {
93
+ "optnExertPrc": item.get("optnExertPrc") or item.get("현재행사가"),
94
+ "rgtExertPricAdjDt": item.get("rgtExertPricAdjDt") or item.get("변경일"),
95
+ "rgtBchgExertPrc": item.get("rgtBchgExertPrc") or item.get("이전행사가"),
96
+ "rgtAchgExertPrc": item.get("rgtAchgExertPrc") or item.get("이후행사가"),
97
+ }
98
+ )
99
+ if normalized.get("rgtExertPricAdjDt") in {"00000000", "00010101"}:
100
+ return None
101
+ return normalized or None
102
+
103
+
104
+ def _normalize_schedule_item(item: dict[str, Any]) -> dict[str, Any] | None:
105
+ dates = item.get("dates") or item.get("행사일정") or []
106
+ normalized = _compact_dict(
107
+ {
108
+ "type": item.get("type") or item.get("옵션분류"),
109
+ "dates": dates if isinstance(dates, list) else [],
110
+ }
111
+ )
112
+ return normalized or None
113
+
114
+
115
+ def _normalize_basic_info_item(item: dict[str, Any]) -> dict[str, Any] | None:
116
+ normalized = _compact_dict(
117
+ {
118
+ "bondBal": item.get("bondBal") or item.get("발행잔액"),
119
+ }
120
+ )
121
+ return normalized or None
122
+
123
+
124
+ def _orchestrate_bond_api_flow(
125
+ *,
126
+ api_key: str,
127
+ query: str,
128
+ parse_issue,
129
+ parse_redemptions,
130
+ parse_exercises,
131
+ parse_adjustments,
132
+ parse_schedules,
133
+ parse_basic_info,
134
+ issuer_name_from_bond_name,
135
+ normalize_name,
136
+ ) -> dict[str, Any]:
137
+ normalized_query = normalize_name(query)
138
+ issuer_name = issuer_name_from_bond_name(query)
139
+
140
+ issue = parse_issue(
141
+ serviceKey=api_key,
142
+ bondIsurNm=issuer_name,
143
+ bondNm=normalized_query,
144
+ )
145
+ if not isinstance(issue, dict):
146
+ return _empty_market_bond_payload()
147
+
148
+ isin_cd = str(issue.get("isinCd") or "").strip()
149
+ bas_dt = str(issue.get("기준일자") or issue.get("basDt") or "").strip()
150
+ if not isin_cd or not bas_dt:
151
+ return _empty_market_bond_payload(_normalize_issue(issue))
152
+
153
+ return {
154
+ "issue": _normalize_issue(issue),
155
+ "optionRedemptions": _normalize_items(
156
+ parse_redemptions(serviceKey=api_key, isinCd=isin_cd),
157
+ _normalize_redemption_item,
158
+ ),
159
+ "optionExercises": _normalize_items(
160
+ parse_exercises(serviceKey=api_key, isinCd=isin_cd),
161
+ _normalize_exercise_item,
162
+ ),
163
+ "priceAdjustments": _normalize_items(
164
+ parse_adjustments(serviceKey=api_key, isinCd=isin_cd),
165
+ _normalize_adjustment_item,
166
+ ),
167
+ "optionSchedules": _normalize_items(
168
+ parse_schedules(serviceKey=api_key, basDt=bas_dt, isinCd=isin_cd),
169
+ _normalize_schedule_item,
170
+ ),
171
+ "bondBasicInfo": _normalize_items(
172
+ parse_basic_info(serviceKey=api_key, basDt=bas_dt, isinCd=isin_cd),
173
+ _normalize_basic_info_item,
174
+ ),
175
+ }
176
+
177
+
178
+ def _run_bond_api_flow(api_key: str, query: str) -> dict[str, Any]:
179
+ from .getBondBasiInfo import parse_bond_basi_info # noqa: PLC0415
180
+ from .getBondWithOptiCallRede import parse_bond_with_opti_call_rede # noqa: PLC0415
181
+ from .getEarlExerOpti import parse_earl_exer_opti # noqa: PLC0415
182
+ from .getIssuIssuItemStat import parse_issu_issu_item_stat # noqa: PLC0415
183
+ from .getOptiExer import parse_opti_exer # noqa: PLC0415
184
+ from .getOptiExerPricAdju import parse_opti_exer_pric_adju # noqa: PLC0415
185
+ from .base_reader import issuer_name_from_bond_name, normalize_name # noqa: PLC0415
186
+
187
+ return _orchestrate_bond_api_flow(
188
+ api_key=api_key,
189
+ query=query,
190
+ parse_issue=parse_issu_issu_item_stat,
191
+ parse_redemptions=parse_bond_with_opti_call_rede,
192
+ parse_exercises=parse_opti_exer,
193
+ parse_adjustments=parse_opti_exer_pric_adju,
194
+ parse_schedules=parse_earl_exer_opti,
195
+ parse_basic_info=parse_bond_basi_info,
196
+ issuer_name_from_bond_name=issuer_name_from_bond_name,
197
+ normalize_name=normalize_name,
198
+ )
199
+
200
+
201
+ class MarketBondWorkflow:
202
+ """Return normalized Data.go.kr bond payloads."""
203
+
204
+ def __init__(self, *, workflow_runner=None) -> None:
205
+ self._workflow_runner = workflow_runner or _run_bond_api_flow
206
+
207
+ def run(
208
+ self,
209
+ *,
210
+ query: str,
211
+ api_key: str,
212
+ save_path: str | None = None,
213
+ ) -> dict[str, Any]:
214
+ normalized_query = str(query or "").strip()
215
+ normalized_api_key = str(api_key or "").strip()
216
+ if not normalized_query:
217
+ raise InvalidInputError("query is required.")
218
+ if not normalized_api_key:
219
+ raise InvalidInputError("api_key is required.")
220
+
221
+ payload = self._workflow_runner(normalized_api_key, normalized_query)
222
+ if not isinstance(payload, dict):
223
+ raise InvalidInputError("bond workflow payload must be a JSON object.")
224
+
225
+ if save_path:
226
+ path = Path(save_path)
227
+ path.parent.mkdir(parents=True, exist_ok=True)
228
+ path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
229
+
230
+ return {
231
+ "input": {
232
+ "query": normalized_query,
233
+ "save_path": save_path,
234
+ },
235
+ "data": payload,
236
+ }
237
+
238
+
239
+ _DEFAULT_WORKFLOW = MarketBondWorkflow()
240
+
241
+
242
+ def run_market_bond_workflow(
243
+ *,
244
+ query: str,
245
+ api_key: str,
246
+ save_path: str | None = None,
247
+ ) -> dict[str, Any]:
248
+ return _DEFAULT_WORKFLOW.run(
249
+ query=query,
250
+ api_key=api_key,
251
+ save_path=save_path,
252
+ )
@@ -0,0 +1,17 @@
1
+ """Exceptions for the mapping workflow library."""
2
+
3
+
4
+ class MappingWorkflowError(Exception):
5
+ """Base exception for mapping workflow failures."""
6
+
7
+
8
+ class InvalidInputError(MappingWorkflowError):
9
+ """Raised when the workflow input contract is violated."""
10
+
11
+
12
+ class NotFoundError(MappingWorkflowError):
13
+ """Raised when no mapping matches the requested lookup."""
14
+
15
+
16
+ class DownloadError(MappingWorkflowError):
17
+ """Raised when latest OpenDart corp-code data cannot be downloaded."""
@@ -0,0 +1,21 @@
1
+ """FnGuide workflows for FinanceChatbot."""
2
+
3
+ from .workflow import (
4
+ FnGuideWorkflow,
5
+ build_finance_ratio_params,
6
+ build_finance_statement_params,
7
+ fetch_fnguide_page,
8
+ parse_fnguide_finance_ratio_html,
9
+ parse_fnguide_finance_statement_html,
10
+ run_fnguide_workflow,
11
+ )
12
+
13
+ __all__ = [
14
+ "FnGuideWorkflow",
15
+ "build_finance_ratio_params",
16
+ "build_finance_statement_params",
17
+ "fetch_fnguide_page",
18
+ "parse_fnguide_finance_ratio_html",
19
+ "parse_fnguide_finance_statement_html",
20
+ "run_fnguide_workflow",
21
+ ]