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.
- financechatbotkit-2.0.0.dist-info/METADATA +11 -0
- financechatbotkit-2.0.0.dist-info/RECORD +39 -0
- financechatbotkit-2.0.0.dist-info/WHEEL +5 -0
- financechatbotkit-2.0.0.dist-info/entry_points.txt +2 -0
- financechatbotkit-2.0.0.dist-info/top_level.txt +2 -0
- orchestrator/__init__.py +29 -0
- orchestrator/bond/__init__.py +8 -0
- orchestrator/bond/base_reader.py +139 -0
- orchestrator/bond/getBondBasiInfo.py +84 -0
- orchestrator/bond/getBondWithOptiCallRede.py +83 -0
- orchestrator/bond/getEarlExerOpti.py +90 -0
- orchestrator/bond/getIssuIssuItemStat.py +85 -0
- orchestrator/bond/getOptiExer.py +83 -0
- orchestrator/bond/getOptiExerPricAdju.py +84 -0
- orchestrator/bond/workflow.py +252 -0
- orchestrator/exceptions.py +17 -0
- orchestrator/fnguide/__init__.py +21 -0
- orchestrator/fnguide/workflow.py +391 -0
- orchestrator/mapping/__init__.py +22 -0
- orchestrator/mapping/data/__init__.py +1 -0
- orchestrator/mapping/data/corp_codes_raw.json +693170 -0
- orchestrator/mapping/update_raw_data.py +96 -0
- orchestrator/mapping/workflow.py +303 -0
- orchestrator/price/__init__.py +15 -0
- orchestrator/price/workflow.py +250 -0
- telebotkit/__init__.py +51 -0
- telebotkit/bot/__init__.py +38 -0
- telebotkit/bot/client.py +217 -0
- telebotkit/bot/reply.py +36 -0
- telebotkit/bot/router.py +125 -0
- telebotkit/bot/safety.py +28 -0
- telebotkit/bot/telegram.py +41 -0
- telebotkit/firestore/__init__.py +45 -0
- telebotkit/firestore/client.py +141 -0
- telebotkit/firestore/documents.py +164 -0
- telebotkit/firestore/fetch.py +228 -0
- telebotkit/firestore/locks.py +74 -0
- telebotkit/firestore/upload.py +75 -0
- 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
|
+
]
|
telebotkit/bot/client.py
ADDED
|
@@ -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
|
+
]
|
telebotkit/bot/reply.py
ADDED
|
@@ -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"]
|
telebotkit/bot/router.py
ADDED
|
@@ -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
|
+
]
|
telebotkit/bot/safety.py
ADDED
|
@@ -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
|
+
]
|