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/sheets.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""General-purpose spreadsheet -> JSON helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from openpyxl import load_workbook
|
|
11
|
+
|
|
12
|
+
_ONE_MIB = 1_048_576
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def read_rows_from_xlsx(path: str | Path, *, sheet_name: str | None = None) -> list[list[str]]:
|
|
16
|
+
"""Read rows from an XLSX worksheet using openpyxl."""
|
|
17
|
+
|
|
18
|
+
workbook = load_workbook(filename=Path(path), read_only=True, data_only=True)
|
|
19
|
+
try:
|
|
20
|
+
worksheet = workbook[sheet_name] if sheet_name else workbook[workbook.sheetnames[0]]
|
|
21
|
+
rows: list[list[str]] = []
|
|
22
|
+
for row in worksheet.iter_rows(values_only=True):
|
|
23
|
+
values = ["" if value is None else str(value) for value in row]
|
|
24
|
+
if any(value.strip() for value in values):
|
|
25
|
+
rows.append(values)
|
|
26
|
+
return rows
|
|
27
|
+
finally:
|
|
28
|
+
workbook.close()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def build_typed_rows_payload(
|
|
32
|
+
rows: list[list[str]],
|
|
33
|
+
*,
|
|
34
|
+
source_file: str = "",
|
|
35
|
+
document_type: str = "typed_rows",
|
|
36
|
+
records_field: str = "records",
|
|
37
|
+
metadata_field: str = "metadata",
|
|
38
|
+
key_field_name: str = "key",
|
|
39
|
+
) -> dict[str, Any]:
|
|
40
|
+
"""Build a typed JSON payload from a simple row sheet."""
|
|
41
|
+
|
|
42
|
+
if not rows:
|
|
43
|
+
raise ValueError("Excel sheet is empty.")
|
|
44
|
+
header = rows[0]
|
|
45
|
+
if len(header) < 1:
|
|
46
|
+
raise ValueError("Excel sheet must contain at least a key column.")
|
|
47
|
+
|
|
48
|
+
dynamic_fields: list[tuple[int, str]] = []
|
|
49
|
+
seen_fields: set[str] = set()
|
|
50
|
+
for idx, raw_header in enumerate(header[1:], start=1):
|
|
51
|
+
field_name = str(raw_header or "").strip()
|
|
52
|
+
if not field_name:
|
|
53
|
+
continue
|
|
54
|
+
if field_name in seen_fields:
|
|
55
|
+
raise ValueError(f"Duplicate field header: {field_name}")
|
|
56
|
+
seen_fields.add(field_name)
|
|
57
|
+
dynamic_fields.append((idx, field_name))
|
|
58
|
+
|
|
59
|
+
records: dict[str, dict[str, Any]] = {}
|
|
60
|
+
|
|
61
|
+
for row_index, row in enumerate(rows[1:], start=2):
|
|
62
|
+
record_key = str(row[0] if len(row) >= 1 else "").strip()
|
|
63
|
+
if not record_key and not any(str(value).strip() for value in row[1:]):
|
|
64
|
+
continue
|
|
65
|
+
if not record_key:
|
|
66
|
+
raise ValueError(f"Row {row_index}: key value is required.")
|
|
67
|
+
if record_key in records:
|
|
68
|
+
raise ValueError(f"Row {row_index}: duplicate key `{record_key}`.")
|
|
69
|
+
|
|
70
|
+
entry: dict[str, Any] = {
|
|
71
|
+
key_field_name: record_key,
|
|
72
|
+
}
|
|
73
|
+
for field_index, field_name in dynamic_fields:
|
|
74
|
+
raw_value = str(row[field_index] if field_index < len(row) else "").strip()
|
|
75
|
+
if not raw_value:
|
|
76
|
+
continue
|
|
77
|
+
entry[field_name] = parse_strict_json_cell(
|
|
78
|
+
raw_value,
|
|
79
|
+
row_index=row_index,
|
|
80
|
+
field_name=field_name,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
records[record_key] = entry
|
|
84
|
+
|
|
85
|
+
payload: dict[str, Any] = {
|
|
86
|
+
"document_type": document_type,
|
|
87
|
+
"schema_version": 1,
|
|
88
|
+
"generated_at": datetime.now(UTC).isoformat(),
|
|
89
|
+
"source_file": source_file,
|
|
90
|
+
metadata_field: {
|
|
91
|
+
"record_count": len(records),
|
|
92
|
+
"field_names": [field_name for _, field_name in dynamic_fields],
|
|
93
|
+
},
|
|
94
|
+
records_field: records,
|
|
95
|
+
}
|
|
96
|
+
payload[metadata_field]["size_bytes"] = typed_rows_size_bytes(payload)
|
|
97
|
+
return payload
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def build_typed_rows_payload_from_xlsx(
|
|
101
|
+
path: str | Path,
|
|
102
|
+
*,
|
|
103
|
+
document_type: str = "typed_rows",
|
|
104
|
+
records_field: str = "records",
|
|
105
|
+
metadata_field: str = "metadata",
|
|
106
|
+
key_field_name: str = "key",
|
|
107
|
+
sheet_name: str | None = None,
|
|
108
|
+
) -> dict[str, Any]:
|
|
109
|
+
source = Path(path)
|
|
110
|
+
rows = read_rows_from_xlsx(source, sheet_name=sheet_name)
|
|
111
|
+
return build_typed_rows_payload(
|
|
112
|
+
rows,
|
|
113
|
+
source_file=str(source),
|
|
114
|
+
document_type=document_type,
|
|
115
|
+
records_field=records_field,
|
|
116
|
+
metadata_field=metadata_field,
|
|
117
|
+
key_field_name=key_field_name,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def typed_rows_size_bytes(payload: dict[str, Any]) -> int:
|
|
122
|
+
return len(json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8"))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def validate_typed_rows_payload(
|
|
126
|
+
payload: dict[str, Any],
|
|
127
|
+
*,
|
|
128
|
+
document_type: str = "typed_rows",
|
|
129
|
+
records_field: str = "records",
|
|
130
|
+
metadata_field: str = "metadata",
|
|
131
|
+
key_field_name: str = "key",
|
|
132
|
+
) -> dict[str, Any]:
|
|
133
|
+
if not isinstance(payload, dict):
|
|
134
|
+
raise ValueError("Typed rows JSON must be an object.")
|
|
135
|
+
if payload.get("document_type") != document_type:
|
|
136
|
+
raise ValueError("Typed rows JSON has unexpected document_type.")
|
|
137
|
+
if not isinstance(payload.get(records_field), dict):
|
|
138
|
+
raise ValueError(f"Typed rows JSON must contain a `{records_field}` object.")
|
|
139
|
+
if not isinstance(payload.get(metadata_field), dict):
|
|
140
|
+
raise ValueError(f"Typed rows JSON must contain a `{metadata_field}` object.")
|
|
141
|
+
|
|
142
|
+
for record_key, entry in payload[records_field].items():
|
|
143
|
+
if not isinstance(entry, dict):
|
|
144
|
+
raise ValueError(f"`{records_field}.{record_key}` must be an object.")
|
|
145
|
+
if str(entry.get(key_field_name) or "") != str(record_key):
|
|
146
|
+
raise ValueError(
|
|
147
|
+
f"`{records_field}.{record_key}.{key_field_name}` must match the record key."
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
size_bytes = typed_rows_size_bytes(payload)
|
|
151
|
+
if size_bytes >= _ONE_MIB:
|
|
152
|
+
raise ValueError(
|
|
153
|
+
f"Typed rows JSON is too large for a single Firestore document: {size_bytes} bytes"
|
|
154
|
+
)
|
|
155
|
+
payload.setdefault(metadata_field, {})["size_bytes"] = size_bytes
|
|
156
|
+
return payload
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def save_typed_rows_payload_json(
|
|
160
|
+
*,
|
|
161
|
+
payload: dict[str, Any],
|
|
162
|
+
output_path: str,
|
|
163
|
+
document_type: str = "typed_rows",
|
|
164
|
+
records_field: str = "records",
|
|
165
|
+
metadata_field: str = "metadata",
|
|
166
|
+
key_field_name: str = "key",
|
|
167
|
+
) -> str:
|
|
168
|
+
validated = validate_typed_rows_payload(
|
|
169
|
+
payload,
|
|
170
|
+
document_type=document_type,
|
|
171
|
+
records_field=records_field,
|
|
172
|
+
metadata_field=metadata_field,
|
|
173
|
+
key_field_name=key_field_name,
|
|
174
|
+
)
|
|
175
|
+
target = Path(output_path)
|
|
176
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
177
|
+
target.write_text(json.dumps(validated, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
178
|
+
return str(target)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def load_typed_rows_json(
|
|
182
|
+
json_path: str | Path,
|
|
183
|
+
*,
|
|
184
|
+
document_type: str = "typed_rows",
|
|
185
|
+
records_field: str = "records",
|
|
186
|
+
metadata_field: str = "metadata",
|
|
187
|
+
key_field_name: str = "key",
|
|
188
|
+
) -> dict[str, Any]:
|
|
189
|
+
source = Path(json_path)
|
|
190
|
+
payload = json.loads(source.read_text(encoding="utf-8"))
|
|
191
|
+
return validate_typed_rows_payload(
|
|
192
|
+
payload,
|
|
193
|
+
document_type=document_type,
|
|
194
|
+
records_field=records_field,
|
|
195
|
+
metadata_field=metadata_field,
|
|
196
|
+
key_field_name=key_field_name,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def parse_strict_json_cell(raw_value: str, *, row_index: int, field_name: str) -> Any:
|
|
201
|
+
try:
|
|
202
|
+
return json.loads(raw_value)
|
|
203
|
+
except json.JSONDecodeError as exc:
|
|
204
|
+
raise ValueError(
|
|
205
|
+
f"Row {row_index} field `{field_name}` must be valid JSON. "
|
|
206
|
+
f"Use strict JSON such as 1, 0.75, true, null, \"text\", [..], or {{..}}."
|
|
207
|
+
) from exc
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
__all__ = [
|
|
211
|
+
"build_typed_rows_payload",
|
|
212
|
+
"build_typed_rows_payload_from_xlsx",
|
|
213
|
+
"load_typed_rows_json",
|
|
214
|
+
"parse_strict_json_cell",
|
|
215
|
+
"read_rows_from_xlsx",
|
|
216
|
+
"save_typed_rows_payload_json",
|
|
217
|
+
"typed_rows_size_bytes",
|
|
218
|
+
"validate_typed_rows_payload",
|
|
219
|
+
]
|