trd-utils 0.0.1__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.

Potentially problematic release.


This version of trd-utils might be problematic. Click here for more details.

@@ -0,0 +1,436 @@
1
+ """BxUltra exchange subclass"""
2
+
3
+ from decimal import Decimal
4
+ import json
5
+ import logging
6
+ import uuid
7
+
8
+ import httpx
9
+
10
+ import time
11
+ from pathlib import Path
12
+
13
+ from .common_utils import do_ultra_ss
14
+ from .bx_types import (
15
+ AssetsInfoResponse,
16
+ ContractsListResponse,
17
+ CopyTraderTradePositionsResponse,
18
+ HintListResponse,
19
+ HomePageResponse,
20
+ HotSearchResponse,
21
+ QuotationRankResponse,
22
+ SearchCopyTraderCondition,
23
+ SearchCopyTradersResponse,
24
+ UserFavoriteQuotationResponse,
25
+ ZenDeskABStatusResponse,
26
+ ZoneModuleListResponse,
27
+ )
28
+ from ..cipher import AESCipher
29
+
30
+ PLATFORM_ID_ANDROID = "10"
31
+ PLATFORM_ID_WEB = "30"
32
+ PLATFORM_ID_TG = "100"
33
+
34
+ ANDROID_DEVICE_BRAND = "SM-N976N"
35
+ WEB_DEVICE_BRAND = "Windows 10_Chrome_127.0.0.0"
36
+ EDGE_DEVICE_BRAND = "Windows 10_Edge_131.0.0.0"
37
+
38
+ ANDROID_APP_VERSION = "4.28.3"
39
+ WEB_APP_VERSION = "4.78.12"
40
+ TG_APP_VERSION = "5.0.15"
41
+
42
+ logger = logging.getLogger(__name__)
43
+
44
+
45
+ class BXUltraClient:
46
+ ###########################################################
47
+ # region client parameters
48
+ we_api_base_host: str = "\u0061pi-\u0061pp.w\u0065-\u0061pi.com"
49
+ we_api_base_url: str = "https://\u0061pi-\u0061pp.w\u0065-\u0061pi.com/\u0061pi"
50
+
51
+ original_base_host: str = "https://\u0062ing\u0078.co\u006d"
52
+
53
+ qq_os_base_host: str = "https://\u0061pi-\u0061pp.\u0071\u0071-os.com"
54
+ qq_os_base_url: str = "https://\u0061pi-\u0061pp.\u0071\u0071-os.com/\u0061pi"
55
+
56
+ device_id: str = None
57
+ trace_id: str = None
58
+ app_version: str = "4.28.3"
59
+ platform_id: str = "10"
60
+ install_channel: str = "officialAPK"
61
+ channel_header: str = "officialAPK"
62
+ origin_header: str = "https://\u0062ing\u0078.co\u006d"
63
+ authorization_token: str = None
64
+ app_id: str = "30004"
65
+ main_app_id: str = "10009"
66
+ trade_env: str = "real"
67
+ timezone: str = "3"
68
+ os_version: str = "7.1.2"
69
+ device_brand: str = "SM-N976N"
70
+ platform_lang: str = "en"
71
+ sys_lang: str = "en"
72
+ user_agent: str = "okhttp/4.12.0"
73
+ x_requested_with: str = None
74
+ httpx_client: httpx.AsyncClient = None
75
+ account_name: str = "default"
76
+
77
+ _fav_letter: str = "^"
78
+
79
+ # endregion
80
+ ###########################################################
81
+ # region client constructor
82
+ def __init__(
83
+ self,
84
+ account_name: str = "default",
85
+ platform_id: str = PLATFORM_ID_ANDROID,
86
+ device_brand: str = ANDROID_DEVICE_BRAND,
87
+ app_version: str = ANDROID_APP_VERSION,
88
+ http_verify: bool = True,
89
+ fav_letter: str = "^",
90
+ ):
91
+ self.httpx_client = httpx.AsyncClient(
92
+ verify=http_verify, http2=True, http1=False
93
+ )
94
+ self.account_name = account_name
95
+ self.platform_id = platform_id
96
+ self.device_brand = device_brand
97
+ self.app_version = app_version
98
+ self._fav_letter = fav_letter
99
+
100
+ self.read_from_session_file(f"{self.account_name}.bx")
101
+
102
+ # endregion
103
+ ###########################################################
104
+ # region api/coin/v1
105
+ async def get_zone_module_info(
106
+ self, only_one_position: int = 0, biz_type: int = 10
107
+ ) -> ZoneModuleListResponse:
108
+ """
109
+ Fetches and returns zone module info from the API.
110
+ Available zones are: All, Forex, Indices, MEME, Elon-inspired,
111
+ Innovation, AI Agent, BTC Ecosystem, TON Ecosystem, Commodities,
112
+ GameFi, Fan Tokens , Layer1 & Layer2, SOL Ecosystem, RWA, LST, DePin, AI
113
+ """
114
+ params = {
115
+ "bizType": f"{biz_type}",
116
+ }
117
+ headers = self.get_headers(params)
118
+ headers["Only_one_position"] = f"{only_one_position}"
119
+ response = await self.httpx_client.get(
120
+ f"{self.we_api_base_url}/coin/v1/zone/module-info",
121
+ headers=headers,
122
+ params=params,
123
+ )
124
+ return ZoneModuleListResponse.deserialize(response.json(parse_float=Decimal))
125
+
126
+ async def get_user_favorite_quotation(
127
+ self, only_one_position: int = 0, biz_type: int = 1
128
+ ):
129
+ params = {
130
+ "bizType": f"{biz_type}",
131
+ }
132
+ headers = self.get_headers(params)
133
+ headers["Only_one_position"] = f"{only_one_position}"
134
+ response = await self.httpx_client.get(
135
+ f"{self.we_api_base_url}/coin/v1/user/favorite/quotation",
136
+ headers=headers,
137
+ params=params,
138
+ )
139
+ return UserFavoriteQuotationResponse.deserialize(
140
+ response.json(parse_float=Decimal)
141
+ )
142
+
143
+ async def get_quotation_rank(self, only_one_position: int = 0, order_flag: int = 0):
144
+ params = {
145
+ "orderFlag": f"{order_flag}",
146
+ }
147
+ headers = self.get_headers(params)
148
+ headers["Only_one_position"] = f"{only_one_position}"
149
+ response = await self.httpx_client.get(
150
+ f"{self.we_api_base_url}/coin/v1/rank/quotation-rank",
151
+ headers=headers,
152
+ params=params,
153
+ )
154
+ return QuotationRankResponse.deserialize(response.json(parse_float=Decimal))
155
+
156
+ async def get_hot_search(self, only_one_position: int = 0, biz_type: int = 30):
157
+ params = {
158
+ "bizType": f"{biz_type}",
159
+ }
160
+ headers = self.get_headers(params)
161
+ headers["Only_one_position"] = f"{only_one_position}"
162
+ response = await self.httpx_client.get(
163
+ f"{self.we_api_base_url}/coin/v1/quotation/hot-search",
164
+ headers=headers,
165
+ params=params,
166
+ )
167
+ return HotSearchResponse.deserialize(response.json(parse_float=Decimal))
168
+
169
+ async def get_homepage(self, only_one_position: int = 0, biz_type: int = 30):
170
+ params = {
171
+ "biz-type": f"{biz_type}",
172
+ }
173
+ headers = self.get_headers(params)
174
+ headers["Only_one_position"] = f"{only_one_position}"
175
+ response = await self.httpx_client.get(
176
+ f"{self.we_api_base_url}/coin/v1/discovery/homepage",
177
+ headers=headers,
178
+ params=params,
179
+ )
180
+ return HomePageResponse.deserialize(response.json(parse_float=Decimal))
181
+
182
+ # endregion
183
+ ###########################################################
184
+ # region customer
185
+ async def get_zendesk_ab_status(self):
186
+ headers = self.get_headers()
187
+ response = await self.httpx_client.get(
188
+ f"{self.we_api_base_url}/customer/v1/zendesk/ab-status",
189
+ headers=headers,
190
+ )
191
+ return ZenDeskABStatusResponse.deserialize(response.json(parse_float=Decimal))
192
+
193
+ # endregion
194
+ ###########################################################
195
+ # region platform-tool
196
+ async def get_hint_list(self) -> HintListResponse:
197
+ headers = self.get_headers()
198
+ response = await self.httpx_client.get(
199
+ f"{self.we_api_base_url}/platform-tool/v1/hint/list",
200
+ headers=headers,
201
+ )
202
+ return HintListResponse.deserialize(response.json(parse_float=Decimal))
203
+
204
+ # endregion
205
+ ###########################################################
206
+ # region asset-manager
207
+ async def get_assets_info(self) -> AssetsInfoResponse:
208
+ headers = self.get_headers(needs_auth=True)
209
+ response = await self.httpx_client.get(
210
+ f"{self.we_api_base_url}/asset-manager/v1/assets/account-total-overview",
211
+ headers=headers,
212
+ )
213
+ return AssetsInfoResponse.deserialize(response.json(parse_float=Decimal))
214
+
215
+ # endregion
216
+ ###########################################################
217
+ # region contract
218
+ async def get_contract_list(
219
+ self,
220
+ quotation_coin_id: int = -1,
221
+ margin_type: int = -1,
222
+ page_size: int = 20,
223
+ page_id: int = 0,
224
+ margin_coin_name: str = "",
225
+ create_type: str = -1,
226
+ ) -> ContractsListResponse:
227
+ params = {
228
+ "quotationCoinId": f"{quotation_coin_id}",
229
+ "marginType": f"{margin_type}",
230
+ "pageSize": f"{page_size}",
231
+ "pageId": f"{page_id}",
232
+ "createType": f"{create_type}",
233
+ }
234
+ if margin_coin_name:
235
+ params["marginCoinName"] = margin_coin_name
236
+ headers = self.get_headers(params, needs_auth=True)
237
+ response = await self.httpx_client.get(
238
+ f"{self.we_api_base_url}/v4/contract/order/hold",
239
+ headers=headers,
240
+ params=params,
241
+ )
242
+ return ContractsListResponse.deserialize(response.json(parse_float=Decimal))
243
+
244
+ # endregion
245
+ ###########################################################
246
+ # region copy-trade-facade
247
+ async def get_copy_trade_trader_positions(
248
+ self,
249
+ uid: str,
250
+ api_identity: str,
251
+ page_size: int = 20,
252
+ page_id: int = 0,
253
+ copy_trade_label_type: int = 1,
254
+ ) -> CopyTraderTradePositionsResponse:
255
+ params = {
256
+ "uid": f"{uid}",
257
+ "apiIdentity": f"{api_identity}",
258
+ "pageSize": f"{page_size}",
259
+ "pageId": f"{page_id}",
260
+ "copyTradeLabelType": f"{copy_trade_label_type}",
261
+ }
262
+ headers = self.get_headers(params)
263
+ response = await self.httpx_client.get(
264
+ f"{self.we_api_base_url}/copy-trade-facade/v2/real/trader/positions",
265
+ headers=headers,
266
+ params=params,
267
+ )
268
+ return CopyTraderTradePositionsResponse.deserialize(
269
+ response.json(parse_float=Decimal)
270
+ )
271
+
272
+ async def search_copy_traders(
273
+ self,
274
+ exchange_id: int = 2,
275
+ nick_name: str = "",
276
+ conditions: list[SearchCopyTraderCondition] = None,
277
+ page_id: int = 0,
278
+ page_size: int = 20,
279
+ sort: str = "comprehensive",
280
+ order: str = "desc",
281
+ ) -> SearchCopyTradersResponse:
282
+ params = {
283
+ "pageId": f"{page_id}",
284
+ "pageSize": f"{page_size}",
285
+ "sort": sort,
286
+ "order": order,
287
+ }
288
+ if conditions is None:
289
+ conditions = [
290
+ {"key": "exchangeId", "selected": "2", "type": "singleSelect"}
291
+ ]
292
+ else:
293
+ conditions = [x.to_dict() for x in conditions]
294
+
295
+ payload = {
296
+ "conditions": conditions,
297
+ "exchangeId": f"{exchange_id}",
298
+ "nickName": nick_name,
299
+ }
300
+ headers = self.get_headers(payload)
301
+ response = await self.httpx_client.post(
302
+ f"{self.we_api_base_url}/v6/copy-trade/search/search",
303
+ headers=headers,
304
+ params=params,
305
+ content=json.dumps(payload, separators=(",", ":"), sort_keys=True),
306
+ )
307
+ return SearchCopyTradersResponse.deserialize(response.json(parse_float=Decimal))
308
+
309
+ # endregion
310
+ ###########################################################
311
+ # region welfare
312
+ async def do_daily_check_in(self):
313
+ headers = self.get_headers(needs_auth=True)
314
+ response = await self.httpx_client.post(
315
+ f"{self.original_base_host}/api/act-operation/v1/welfare/sign-in/do",
316
+ headers=headers,
317
+ content="",
318
+ )
319
+ return response.json()
320
+
321
+ # endregion
322
+ ###########################################################
323
+ # region client helper methods
324
+ def get_headers(self, payload=None, needs_auth: bool = False) -> dict:
325
+ the_timestamp = int(time.time() * 1000)
326
+ the_headers = {
327
+ "Host": self.we_api_base_host,
328
+ "Content-Type": "application/json",
329
+ "Mainappid": self.main_app_id,
330
+ "Accept": "application/json",
331
+ "Origin": self.origin_header,
332
+ "Traceid": self.trace_id,
333
+ "App_version": self.app_version,
334
+ "Platformid": self.platform_id,
335
+ "Device_id": self.device_id,
336
+ "Device_brand": self.device_brand,
337
+ "Channel": self.channel_header,
338
+ "Appid": self.app_id,
339
+ "Trade_env": self.trade_env,
340
+ "Timezone": self.timezone,
341
+ "Lang": self.platform_lang,
342
+ "Syslang": self.sys_lang,
343
+ "Sign": do_ultra_ss(
344
+ e_param=None,
345
+ se_param=None,
346
+ le_param=None,
347
+ timestamp=the_timestamp,
348
+ trace_id=self.trace_id,
349
+ device_id=self.device_id,
350
+ platform_id=self.platform_id,
351
+ app_version=self.app_version,
352
+ payload_data=payload,
353
+ ),
354
+ "Timestamp": f"{the_timestamp}",
355
+ # 'Accept-Encoding': 'gzip, deflate',
356
+ "User-Agent": self.user_agent,
357
+ "Connection": "close",
358
+ "appsiteid": "0",
359
+ }
360
+
361
+ if self.x_requested_with:
362
+ the_headers["X-Requested-With"] = self.x_requested_with
363
+
364
+ if needs_auth:
365
+ the_headers["Authorization"] = f"Bearer {self.authorization_token}"
366
+ return the_headers
367
+
368
+ async def aclose(self) -> None:
369
+ await self.httpx_client.aclose()
370
+ logger.info("BXUltraClient closed")
371
+ return True
372
+
373
+ def read_from_session_file(self, file_path: str) -> None:
374
+ """
375
+ Reads from session file; if it doesn't exist, creates it.
376
+ """
377
+ # check if path exists
378
+ target_path = Path(file_path)
379
+ if not target_path.exists():
380
+ return self._save_session_file(file_path=file_path)
381
+
382
+ aes = AESCipher(key=f"bx_{self.account_name}_bx", fav_letter=self._fav_letter)
383
+ content = aes.decrypt(target_path.read_text()).decode("utf-8")
384
+ json_data: dict = json.loads(content)
385
+
386
+ self.device_id = json_data.get("device_id", self.device_id)
387
+ self.trace_id = json_data.get("trace_id", self.trace_id)
388
+ self.app_version = json_data.get("app_version", self.app_version)
389
+ self.platform_id = json_data.get("platform_id", self.platform_id)
390
+ self.install_channel = json_data.get("install_channel", self.install_channel)
391
+ self.channel_header = json_data.get("channel_header", self.channel_header)
392
+ self.authorization_token = json_data.get(
393
+ "authorization_token", self.authorization_token
394
+ )
395
+ self.app_id = json_data.get("app_id", self.app_id)
396
+ self.trade_env = json_data.get("trade_env", self.trade_env)
397
+ self.timezone = json_data.get("timezone", self.timezone)
398
+ self.os_version = json_data.get("os_version", self.os_version)
399
+ self.device_brand = json_data.get("device_brand", self.device_brand)
400
+ self.platform_lang = json_data.get("platform_lang", self.platform_lang)
401
+ self.sys_lang = json_data.get("sys_lang", self.sys_lang)
402
+ self.user_agent = json_data.get("user_agent", self.user_agent)
403
+
404
+ def _save_session_file(self, file_path: str) -> None:
405
+ """
406
+ Saves current information to the session file.
407
+ """
408
+ if not self.device_id:
409
+ self.device_id = uuid.uuid4().hex.replace("-", "") + "##"
410
+
411
+ if not self.trace_id:
412
+ self.trace_id = uuid.uuid4().hex.replace("-", "")
413
+
414
+ json_data = {
415
+ "device_id": self.device_id,
416
+ "trace_id": self.trace_id,
417
+ "app_version": self.app_version,
418
+ "platform_id": self.platform_id,
419
+ "install_channel": self.install_channel,
420
+ "channel_header": self.channel_header,
421
+ "authorization_token": self.authorization_token,
422
+ "app_id": self.app_id,
423
+ "trade_env": self.trade_env,
424
+ "timezone": self.timezone,
425
+ "os_version": self.os_version,
426
+ "device_brand": self.device_brand,
427
+ "platform_lang": self.platform_lang,
428
+ "sys_lang": self.sys_lang,
429
+ "user_agent": self.user_agent,
430
+ }
431
+ aes = AESCipher(key=f"bx_{self.account_name}_bx", fav_letter=self._fav_letter)
432
+ target_path = Path(file_path)
433
+ target_path.write_text(aes.encrypt(json.dumps(json_data)))
434
+
435
+ # endregion
436
+ ###########################################################
@@ -0,0 +1,51 @@
1
+ import hashlib
2
+ import json
3
+ import uuid
4
+
5
+ default_e: str = (
6
+ "\u0039\u0035\u0064\u0036\u0035\u0063\u0037\u0033\u0064\u0063\u0035"
7
+ + "\u0063\u0034\u0033\u0037"
8
+ )
9
+ default_se: str = "\u0030\u0061\u0065\u0039\u0030\u0031\u0038\u0066\u0062\u0037"
10
+ default_le: str = "\u0066\u0032\u0065\u0061\u0062\u0036\u0039"
11
+
12
+ long_accept_header1: str = (
13
+ "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,"
14
+ + "image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"
15
+ )
16
+
17
+
18
+ def do_ultra_ss(
19
+ e_param: str,
20
+ se_param: str,
21
+ le_param: str,
22
+ timestamp: int,
23
+ trace_id: str,
24
+ device_id: str,
25
+ platform_id: str,
26
+ app_version: str,
27
+ payload_data: str = None,
28
+ ) -> str:
29
+ if not e_param:
30
+ e_param = default_e
31
+
32
+ if not se_param:
33
+ se_param = default_se
34
+
35
+ if not le_param:
36
+ le_param = default_le
37
+
38
+ first_part = f"{e_param}{se_param}{le_param}{timestamp}{trace_id}"
39
+ if not payload_data:
40
+ payload_data = "{}"
41
+ elif not isinstance(payload_data, str):
42
+ # convert to json
43
+ payload_data = json.dumps(payload_data, separators=(",", ":"), sort_keys=True)
44
+
45
+ if not trace_id:
46
+ trace_id = uuid.uuid4().hex.replace("-", "")
47
+
48
+ whole_parts = f"{first_part}{device_id}{platform_id}{app_version}{payload_data}"
49
+
50
+ # do SHA256
51
+ return hashlib.sha256(whole_parts.encode()).hexdigest().upper()
@@ -0,0 +1,44 @@
1
+
2
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
3
+ from cryptography.hazmat.backends import default_backend
4
+ from cryptography.hazmat.primitives import padding
5
+ from base64 import b64encode, b64decode
6
+ from os import urandom
7
+
8
+
9
+ class AESCipher:
10
+ def __init__(self, key: str, fav_letter: str):
11
+ if len(key) > 32:
12
+ raise ValueError("Key length must be 32 bytes or less")
13
+ elif len(key) < 32:
14
+ key = key.ljust(len(key) + (32 - len(key) % 32), fav_letter)
15
+
16
+ key = key.encode('utf-8')
17
+ if len(key) != 32:
18
+ raise ValueError("Key length must be 32 bytes")
19
+
20
+ self.key = key
21
+ self.backend = default_backend()
22
+
23
+ def encrypt(self, plaintext):
24
+ if isinstance(plaintext, str):
25
+ plaintext = plaintext.encode('utf-8')
26
+
27
+ iv = urandom(16)
28
+ cipher = Cipher(algorithms.AES(self.key), modes.CBC(iv), backend=self.backend)
29
+ padder = padding.PKCS7(128).padder()
30
+ padded_data = padder.update(plaintext) + padder.finalize()
31
+ encryptor = cipher.encryptor()
32
+ ciphertext = encryptor.update(padded_data) + encryptor.finalize()
33
+ return b64encode(iv + ciphertext).decode('utf-8')
34
+
35
+ def decrypt(self, b64_encrypted_data):
36
+ encrypted_data = b64decode(b64_encrypted_data)
37
+ iv = encrypted_data[:16]
38
+ ciphertext = encrypted_data[16:]
39
+ cipher = Cipher(algorithms.AES(self.key), modes.CBC(iv), backend=self.backend)
40
+ unpadder = padding.PKCS7(128).unpadder()
41
+ decryptor = cipher.decryptor()
42
+ padded_plaintext = decryptor.update(ciphertext) + decryptor.finalize()
43
+ plaintext = unpadder.update(padded_plaintext) + unpadder.finalize()
44
+ return plaintext
@@ -0,0 +1,26 @@
1
+
2
+ from .html_formats import (
3
+ get_html_normal,
4
+ html_normal,
5
+ html_mono,
6
+ html_in_parenthesis,
7
+ html_bold,
8
+ html_italic,
9
+ html_link,
10
+ html_code_snippets,
11
+ html_pre,
12
+ html_spoiler
13
+ )
14
+
15
+ __all__ = [
16
+ "get_html_normal",
17
+ "html_normal",
18
+ "html_mono",
19
+ "html_in_parenthesis",
20
+ "html_bold",
21
+ "html_italic",
22
+ "html_link",
23
+ "html_code_snippets",
24
+ "html_pre",
25
+ "html_spoiler"
26
+ ]
@@ -0,0 +1,72 @@
1
+ import html
2
+
3
+ def camel_to_snake(str_value: str) -> str:
4
+ """
5
+ Convert CamelCase to snake_case.
6
+ https://stackoverflow.com/a/44969381/16518789
7
+ """
8
+ return ''.join(['_'+c.lower() if c.isupper() else c for c in str_value]).lstrip('_')
9
+
10
+ def to_camel_case(snake_str: str) -> str:
11
+ """
12
+ Convert snake_case to CamelCase.
13
+ https://stackoverflow.com/a/19053800/16518789
14
+ """
15
+ return "".join(x.capitalize() for x in snake_str.lower().split("_"))
16
+
17
+ def to_lower_camel_case(snake_str: str) -> str:
18
+ # We capitalize the first letter of each component except the first one
19
+ # with the 'capitalize' method and join them together.
20
+ camel_string = to_camel_case(snake_str)
21
+ return snake_str[0].lower() + camel_string[1:]
22
+
23
+ def get_html_normal(*argv) -> str:
24
+ if argv is None or len(argv) == 0:
25
+ return ""
26
+
27
+ my_str = ""
28
+ for value in argv:
29
+ if not value:
30
+ continue
31
+ if isinstance(value, str):
32
+ my_str += value
33
+ else:
34
+ my_str += str(value)
35
+
36
+ return my_str
37
+
38
+ def html_normal(value, *argv) -> str:
39
+ my_str = html.escape(str(value))
40
+ for value in argv:
41
+ if isinstance(value, str):
42
+ my_str += value
43
+ return my_str
44
+
45
+
46
+ def html_mono(value, *argv) -> str:
47
+ return f"<code>{html.escape(str(value))}</code>" + get_html_normal(*argv)
48
+
49
+ def html_in_parenthesis(value) -> str:
50
+ if not value:
51
+ return ": "
52
+ return f" ({html.escape(str(value))}): "
53
+
54
+ def html_bold(value, *argv) -> str:
55
+ return f"<b>{html.escape(str(value))}</b>" + get_html_normal(*argv)
56
+
57
+ def html_italic(value, *argv) -> str:
58
+ return f"<i>{html.escape(str(value))}</i>" + get_html_normal(*argv)
59
+
60
+ def html_link(value, link: str, *argv) -> str:
61
+ if not isinstance(link, str) or len(link) == 0:
62
+ return html_mono(value, *argv)
63
+ return f"<a href={html.escape(link)}>{html.escape(str(value))}</a>" + get_html_normal(*argv)
64
+
65
+ def html_code_snippets(value, language: str, *argv):
66
+ return html_pre(value, language, *argv)
67
+
68
+ def html_pre(value, language: str, *argv):
69
+ return f"<pre language={html.escape(language)}>{html.escape(str(value))}</pre>" + get_html_normal(*argv)
70
+
71
+ def html_spoiler(value, *argv):
72
+ return f"<spoiler>{html.escape(str(value))}</spoiler>" + get_html_normal(*argv)
@@ -0,0 +1,8 @@
1
+ from .tradingview_client import TradingViewClient
2
+ from .tradingview_types import CoinScanInfo
3
+
4
+
5
+ __all__ = [
6
+ "TradingViewClient",
7
+ "CoinScanInfo",
8
+ ]