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
telebotkit/__init__.py ADDED
@@ -0,0 +1,51 @@
1
+ """TeleBotKit public package."""
2
+
3
+ from telebotkit.bot import (
4
+ CommandMatch,
5
+ Reply,
6
+ Router,
7
+ SECTION_DIVIDER,
8
+ escape_markdown,
9
+ format_krw_amount,
10
+ safe_call,
11
+ )
12
+ from telebotkit.firestore import (
13
+ DocumentStore,
14
+ LeaseStore,
15
+ SharedDocumentStore,
16
+ get_client,
17
+ upload_typed_rows_json,
18
+ upload_typed_rows_payload,
19
+ )
20
+ from telebotkit.sheets import (
21
+ build_typed_rows_payload,
22
+ build_typed_rows_payload_from_xlsx,
23
+ load_typed_rows_json,
24
+ read_rows_from_xlsx,
25
+ save_typed_rows_payload_json,
26
+ validate_typed_rows_payload,
27
+ )
28
+
29
+ __all__ = [
30
+ "CommandMatch",
31
+ "DocumentStore",
32
+ "LeaseStore",
33
+ "Reply",
34
+ "Router",
35
+ "SECTION_DIVIDER",
36
+ "SharedDocumentStore",
37
+ "build_typed_rows_payload",
38
+ "build_typed_rows_payload_from_xlsx",
39
+ "bot",
40
+ "escape_markdown",
41
+ "firestore",
42
+ "format_krw_amount",
43
+ "get_client",
44
+ "load_typed_rows_json",
45
+ "read_rows_from_xlsx",
46
+ "safe_call",
47
+ "save_typed_rows_payload_json",
48
+ "upload_typed_rows_json",
49
+ "upload_typed_rows_payload",
50
+ "validate_typed_rows_payload",
51
+ ]
@@ -0,0 +1,38 @@
1
+ """Bot-facing TeleBotKit helpers."""
2
+
3
+ from telebotkit.bot.client import (
4
+ SendResult,
5
+ TelegramApiError,
6
+ TelegramBot,
7
+ TelegramSendError,
8
+ )
9
+ from telebotkit.bot.reply import DEFAULT_PARSE_MODE, Reply
10
+ from telebotkit.bot.router import (
11
+ CommandHandler,
12
+ CommandMatch,
13
+ CommandRoute,
14
+ Router,
15
+ )
16
+ from telebotkit.bot.safety import safe_call
17
+ from telebotkit.bot.telegram import (
18
+ SECTION_DIVIDER,
19
+ escape_markdown,
20
+ format_krw_amount,
21
+ )
22
+
23
+ __all__ = [
24
+ "CommandHandler",
25
+ "CommandMatch",
26
+ "CommandRoute",
27
+ "DEFAULT_PARSE_MODE",
28
+ "Reply",
29
+ "Router",
30
+ "SendResult",
31
+ "SECTION_DIVIDER",
32
+ "TelegramApiError",
33
+ "TelegramBot",
34
+ "TelegramSendError",
35
+ "escape_markdown",
36
+ "format_krw_amount",
37
+ "safe_call",
38
+ ]
@@ -0,0 +1,217 @@
1
+ """Telegram Bot API sending helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from collections.abc import Callable
7
+ from dataclasses import dataclass
8
+ from typing import Any
9
+
10
+ import httpx
11
+
12
+ from telebotkit.bot.reply import DEFAULT_PARSE_MODE, Reply
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class TelegramSendError(RuntimeError):
18
+ """Raised when a Telegram send request fails."""
19
+
20
+
21
+ class TelegramApiError(TelegramSendError):
22
+ """Raised when Telegram responds with an error payload or status code."""
23
+
24
+ def __init__(self, *, status_code: int, detail: str) -> None:
25
+ self.status_code = status_code
26
+ self.detail = detail
27
+ super().__init__(f"Telegram API error ({status_code}): {detail}")
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class SendResult:
32
+ """Normalized result of a Telegram send request."""
33
+
34
+ ok: bool
35
+ chat_id: int
36
+ payload: dict[str, Any]
37
+ status_code: int
38
+ data: dict[str, Any]
39
+
40
+
41
+ class TelegramBot:
42
+ """Small async client for Telegram Bot API `sendMessage`."""
43
+
44
+ def __init__(
45
+ self,
46
+ *,
47
+ token: str,
48
+ timeout: float = 10.0,
49
+ base_url: str | None = None,
50
+ client_factory: Callable[[], httpx.AsyncClient] | None = None,
51
+ ) -> None:
52
+ self._token = token
53
+ self._timeout = timeout
54
+ self._base_url = (base_url or f"https://api.telegram.org/bot{token}").rstrip("/")
55
+ self._client_factory = client_factory or self._default_client_factory
56
+
57
+ async def send(self, *, chat_id: int, reply: Reply) -> SendResult:
58
+ payload = reply.to_payload(chat_id)
59
+ response = await self._post("sendMessage", payload)
60
+ data = self._parse_response_data(response)
61
+ return SendResult(
62
+ ok=True,
63
+ chat_id=chat_id,
64
+ payload=payload,
65
+ status_code=response.status_code,
66
+ data=data,
67
+ )
68
+
69
+ async def send_text(
70
+ self,
71
+ *,
72
+ chat_id: int,
73
+ text: str,
74
+ parse_mode: str | None = None,
75
+ ) -> SendResult:
76
+ reply = Reply(text=text, parse_mode=parse_mode or DEFAULT_PARSE_MODE)
77
+ return await self.send(chat_id=chat_id, reply=reply)
78
+
79
+ async def send_plain_text(self, *, chat_id: int, text: str) -> SendResult:
80
+ return await self.send(chat_id=chat_id, reply=Reply.plain(text))
81
+
82
+ async def send_markdown(self, *, chat_id: int, text: str) -> SendResult:
83
+ return await self.send(chat_id=chat_id, reply=Reply.md(text))
84
+
85
+ async def send_payload(self, payload: dict[str, Any]) -> SendResult:
86
+ chat_id = int(payload["chat_id"])
87
+ reply = Reply(
88
+ text=str(payload["text"]),
89
+ parse_mode=payload.get("parse_mode"),
90
+ )
91
+ return await self.send(chat_id=chat_id, reply=reply)
92
+
93
+ async def send_safe(
94
+ self,
95
+ *,
96
+ chat_id: int,
97
+ reply: Reply,
98
+ logger: logging.Logger | None = None,
99
+ success_log: str | None = None,
100
+ success_args: tuple[object, ...] = (),
101
+ failure_log: str | None = None,
102
+ failure_args: tuple[object, ...] = (),
103
+ ) -> SendResult | None:
104
+ active_logger = logger or globals()["logger"]
105
+ try:
106
+ result = await self.send(chat_id=chat_id, reply=reply)
107
+ except Exception:
108
+ if failure_log:
109
+ active_logger.exception(failure_log, *failure_args)
110
+ else:
111
+ active_logger.exception("Failed to send Telegram message to chat_id=%s", chat_id)
112
+ return None
113
+
114
+ if success_log:
115
+ active_logger.info(success_log, *success_args)
116
+ return result
117
+
118
+ async def send_plain_text_safe(
119
+ self,
120
+ *,
121
+ chat_id: int,
122
+ text: str,
123
+ logger: logging.Logger | None = None,
124
+ success_log: str | None = None,
125
+ success_args: tuple[object, ...] = (),
126
+ failure_log: str | None = None,
127
+ failure_args: tuple[object, ...] = (),
128
+ ) -> SendResult | None:
129
+ return await self.send_safe(
130
+ chat_id=chat_id,
131
+ reply=Reply.plain(text),
132
+ logger=logger,
133
+ success_log=success_log,
134
+ success_args=success_args,
135
+ failure_log=failure_log,
136
+ failure_args=failure_args,
137
+ )
138
+
139
+ async def send_markdown_safe(
140
+ self,
141
+ *,
142
+ chat_id: int,
143
+ text: str,
144
+ logger: logging.Logger | None = None,
145
+ success_log: str | None = None,
146
+ success_args: tuple[object, ...] = (),
147
+ failure_log: str | None = None,
148
+ failure_args: tuple[object, ...] = (),
149
+ ) -> SendResult | None:
150
+ return await self.send_safe(
151
+ chat_id=chat_id,
152
+ reply=Reply.md(text),
153
+ logger=logger,
154
+ success_log=success_log,
155
+ success_args=success_args,
156
+ failure_log=failure_log,
157
+ failure_args=failure_args,
158
+ )
159
+
160
+ async def set_webhook(
161
+ self,
162
+ *,
163
+ url: str,
164
+ secret_token: str | None = None,
165
+ ) -> dict[str, Any]:
166
+ payload: dict[str, Any] = {"url": url}
167
+ if secret_token:
168
+ payload["secret_token"] = secret_token
169
+ response = await self._post("setWebhook", payload)
170
+ return self._parse_response_data(response)
171
+
172
+ async def delete_webhook(
173
+ self,
174
+ *,
175
+ drop_pending_updates: bool = False,
176
+ ) -> dict[str, Any]:
177
+ response = await self._post(
178
+ "deleteWebhook",
179
+ {"drop_pending_updates": drop_pending_updates},
180
+ )
181
+ return self._parse_response_data(response)
182
+
183
+ async def _post(self, method: str, payload: dict[str, Any] | None) -> httpx.Response:
184
+ async with self._client_factory() as client:
185
+ response = await client.post(f"{self._base_url}/{method}", json=payload)
186
+ if response.is_error:
187
+ raise TelegramApiError(
188
+ status_code=response.status_code,
189
+ detail=response.text,
190
+ )
191
+ return response
192
+
193
+ def _default_client_factory(self) -> httpx.AsyncClient:
194
+ return httpx.AsyncClient(timeout=self._timeout)
195
+
196
+ @staticmethod
197
+ def _parse_response_data(response: httpx.Response) -> dict[str, Any]:
198
+ try:
199
+ data = response.json()
200
+ except ValueError as exc:
201
+ raise TelegramSendError("Telegram response was not valid JSON.") from exc
202
+ if not isinstance(data, dict):
203
+ raise TelegramSendError("Telegram response payload must be an object.")
204
+ if data.get("ok") is False:
205
+ raise TelegramApiError(
206
+ status_code=response.status_code,
207
+ detail=str(data.get("description") or "Telegram API returned ok=false."),
208
+ )
209
+ return data
210
+
211
+
212
+ __all__ = [
213
+ "SendResult",
214
+ "TelegramApiError",
215
+ "TelegramBot",
216
+ "TelegramSendError",
217
+ ]
@@ -0,0 +1,36 @@
1
+ """Telegram reply payload helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+ DEFAULT_PARSE_MODE = "MarkdownV2"
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class Reply:
13
+ """Simple outbound Telegram reply."""
14
+
15
+ text: str
16
+ parse_mode: str | None = DEFAULT_PARSE_MODE
17
+
18
+ @classmethod
19
+ def md(cls, text: str) -> "Reply":
20
+ return cls(text=text, parse_mode=DEFAULT_PARSE_MODE)
21
+
22
+ @classmethod
23
+ def plain(cls, text: str) -> "Reply":
24
+ return cls(text=text, parse_mode=None)
25
+
26
+ def to_payload(self, chat_id: int) -> dict[str, Any]:
27
+ payload: dict[str, Any] = {
28
+ "chat_id": chat_id,
29
+ "text": self.text,
30
+ }
31
+ if self.parse_mode:
32
+ payload["parse_mode"] = self.parse_mode
33
+ return payload
34
+
35
+
36
+ __all__ = ["DEFAULT_PARSE_MODE", "Reply"]
@@ -0,0 +1,125 @@
1
+ """Command parsing and routing helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Callable, Generic, TypeVar
7
+
8
+ RequestT = TypeVar("RequestT")
9
+ ServicesT = TypeVar("ServicesT")
10
+ ResponseT = TypeVar("ResponseT")
11
+ CommandHandler = Callable[[RequestT, ServicesT], ResponseT]
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class CommandMatch:
16
+ """Parsed slash-command payload."""
17
+
18
+ name: str
19
+ args: str
20
+ invoked_name: str
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class CommandRoute(Generic[RequestT, ServicesT, ResponseT]):
25
+ """Registered command route."""
26
+
27
+ name: str
28
+ aliases: tuple[str, ...]
29
+ handler: CommandHandler[RequestT, ServicesT, ResponseT]
30
+
31
+
32
+ class Router(Generic[RequestT, ServicesT, ResponseT]):
33
+ """Register slash commands once and reuse for parse + dispatch."""
34
+
35
+ def __init__(self) -> None:
36
+ self._routes_by_command: dict[str, CommandRoute[RequestT, ServicesT, ResponseT]] = {}
37
+
38
+ def register(
39
+ self,
40
+ name: str,
41
+ handler: CommandHandler[RequestT, ServicesT, ResponseT],
42
+ *,
43
+ aliases: tuple[str, ...] = (),
44
+ ) -> None:
45
+ route = CommandRoute(
46
+ name=self._normalize_name(name),
47
+ aliases=tuple(self._normalize_name(alias) for alias in aliases),
48
+ handler=handler,
49
+ )
50
+ for command_name in (route.name, *route.aliases):
51
+ self._routes_by_command[command_name] = route
52
+
53
+ def add(
54
+ self,
55
+ name: str,
56
+ handler: CommandHandler[RequestT, ServicesT, ResponseT],
57
+ *,
58
+ aliases: tuple[str, ...] = (),
59
+ ) -> None:
60
+ self.register(name, handler, aliases=aliases)
61
+
62
+ def command(
63
+ self,
64
+ name: str,
65
+ *,
66
+ aliases: tuple[str, ...] = (),
67
+ ) -> Callable[
68
+ [CommandHandler[RequestT, ServicesT, ResponseT]],
69
+ CommandHandler[RequestT, ServicesT, ResponseT],
70
+ ]:
71
+ def _decorator(
72
+ handler: CommandHandler[RequestT, ServicesT, ResponseT],
73
+ ) -> CommandHandler[RequestT, ServicesT, ResponseT]:
74
+ self.register(name, handler, aliases=aliases)
75
+ return handler
76
+
77
+ return _decorator
78
+
79
+ def route(
80
+ self,
81
+ name: str,
82
+ *,
83
+ aliases: tuple[str, ...] = (),
84
+ ) -> Callable[
85
+ [CommandHandler[RequestT, ServicesT, ResponseT]],
86
+ CommandHandler[RequestT, ServicesT, ResponseT],
87
+ ]:
88
+ return self.command(name, aliases=aliases)
89
+
90
+ def parse(self, text: str) -> CommandMatch | None:
91
+ stripped = (text or "").strip()
92
+ if not stripped or not stripped.startswith("/"):
93
+ return None
94
+
95
+ parts = stripped.split(maxsplit=1)
96
+ command_text = parts[0][1:].lower()
97
+ invoked_name = self._normalize_name(command_text.split("@", 1)[0])
98
+ route = self._routes_by_command.get(invoked_name)
99
+ if route is None:
100
+ return None
101
+
102
+ args = parts[1].strip() if len(parts) > 1 else ""
103
+ return CommandMatch(name=route.name, args=args, invoked_name=invoked_name)
104
+
105
+ def dispatch(
106
+ self,
107
+ command_name: str,
108
+ request: RequestT,
109
+ services: ServicesT,
110
+ ) -> ResponseT | None:
111
+ route = self._routes_by_command.get(self._normalize_name(command_name))
112
+ if route is None:
113
+ return None
114
+ return route.handler(request, services)
115
+
116
+ @staticmethod
117
+ def _normalize_name(name: str) -> str:
118
+ return str(name or "").strip().lower()
119
+
120
+ __all__ = [
121
+ "CommandHandler",
122
+ "CommandMatch",
123
+ "CommandRoute",
124
+ "Router",
125
+ ]
@@ -0,0 +1,28 @@
1
+ """Small helpers for safe handler execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from collections.abc import Callable
7
+ from typing import TypeVar
8
+
9
+ ResultT = TypeVar("ResultT")
10
+
11
+
12
+ def safe_call(
13
+ *,
14
+ action: Callable[[], ResultT],
15
+ default: ResultT,
16
+ logger: logging.Logger,
17
+ log_message: str,
18
+ log_args: tuple[object, ...] = (),
19
+ ) -> ResultT:
20
+ """Run *action* and fall back with consistent exception logging."""
21
+
22
+ try:
23
+ return action()
24
+ except Exception:
25
+ logger.exception(log_message, *log_args)
26
+ return default
27
+
28
+ __all__ = ["safe_call"]
@@ -0,0 +1,41 @@
1
+ """Telegram formatting helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ SECTION_DIVIDER = "─" * 20
6
+ TELEGRAM_MARKDOWN_V2_SPECIAL_CHARS = "_*[]()~`>#+-=|{}.!"
7
+
8
+
9
+ def escape_markdown(text: str) -> str:
10
+ """Escape special Telegram MarkdownV2 characters."""
11
+
12
+ escaped = str(text)
13
+ for char in TELEGRAM_MARKDOWN_V2_SPECIAL_CHARS:
14
+ escaped = escaped.replace(char, f"\\{char}")
15
+ return escaped
16
+
17
+
18
+ def format_krw_amount(amount: int) -> str:
19
+ """Format a KRW amount into a compact Korean-readable string."""
20
+
21
+ if amount < 0:
22
+ return f"-{format_krw_amount(-amount)}"
23
+
24
+ x_1t = 1_000_000_000_000
25
+ x_100m = 100_000_000
26
+ x_10k = 10_000
27
+
28
+ if amount >= x_1t:
29
+ return f"{amount / x_1t:,.2f}조"
30
+ if amount >= x_100m:
31
+ return f"{amount / x_100m:,.2f}억"
32
+ if amount >= x_10k:
33
+ return f"{amount / x_10k:,.2f}만"
34
+ return f"{amount:,}"
35
+
36
+ __all__ = [
37
+ "SECTION_DIVIDER",
38
+ "TELEGRAM_MARKDOWN_V2_SPECIAL_CHARS",
39
+ "escape_markdown",
40
+ "format_krw_amount",
41
+ ]
@@ -0,0 +1,45 @@
1
+ """Firestore helpers for TeleBotKit."""
2
+
3
+ from telebotkit.firestore.client import (
4
+ FirestoreClientProvider,
5
+ FirestoreClientFactory,
6
+ FirestoreQuotaError,
7
+ get_client,
8
+ load_service_account_info_from_env,
9
+ raise_if_quota_error,
10
+ reset_client,
11
+ )
12
+ from telebotkit.firestore.documents import (
13
+ DocumentStore,
14
+ SharedDocumentStore,
15
+ )
16
+ from telebotkit.firestore.fetch import (
17
+ FetchResult,
18
+ clear_document_cache,
19
+ fetch_document,
20
+ invalidate_document_cache,
21
+ )
22
+ from telebotkit.firestore.locks import LeaseStore
23
+ from telebotkit.firestore.upload import (
24
+ upload_typed_rows_json,
25
+ upload_typed_rows_payload,
26
+ )
27
+
28
+ __all__ = [
29
+ "DocumentStore",
30
+ "FetchResult",
31
+ "FirestoreClientProvider",
32
+ "FirestoreClientFactory",
33
+ "FirestoreQuotaError",
34
+ "LeaseStore",
35
+ "SharedDocumentStore",
36
+ "clear_document_cache",
37
+ "fetch_document",
38
+ "get_client",
39
+ "invalidate_document_cache",
40
+ "load_service_account_info_from_env",
41
+ "raise_if_quota_error",
42
+ "reset_client",
43
+ "upload_typed_rows_json",
44
+ "upload_typed_rows_payload",
45
+ ]