Queer European MD passionate about IT

utilities.py 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734
  1. """Useful functions used by Davte when programming in python."""
  2. # Standard library modules
  3. import asyncio
  4. import collections
  5. import csv
  6. import datetime
  7. import inspect
  8. import io
  9. import json
  10. import logging
  11. import os
  12. import random
  13. import re
  14. import string
  15. import time
  16. from difflib import SequenceMatcher
  17. # Third party modules
  18. from typing import Tuple, Union
  19. import aiohttp
  20. from bs4 import BeautifulSoup
  21. weekdays = collections.OrderedDict()
  22. weekdays[0] = {
  23. 'en': "Monday",
  24. 'it': "Lunedì",
  25. }
  26. weekdays[1] = {
  27. 'en': "Tuesday",
  28. 'it': "Martedì",
  29. }
  30. weekdays[2] = {
  31. 'en': "Wednesday",
  32. 'it': "Mercoledì",
  33. }
  34. weekdays[3] = {
  35. 'en': "Thursday",
  36. 'it': "Giovedì",
  37. }
  38. weekdays[4] = {
  39. 'en': "Friday",
  40. 'it': "Venerdì",
  41. }
  42. weekdays[5] = {
  43. 'en': "Saturday",
  44. 'it': "Sabato",
  45. }
  46. weekdays[6] = {
  47. 'en': "Sunday",
  48. 'it': "Domenica",
  49. }
  50. def sumif(iterable, condition):
  51. """Sum all `iterable` items matching `condition`."""
  52. return sum(
  53. filter(
  54. condition,
  55. iterable
  56. )
  57. )
  58. def markdown_check(text, symbols):
  59. """Check that all `symbols` occur an even number of times in `text`."""
  60. for s in symbols:
  61. if (len(text.replace(s, "")) - len(text)) % 2 != 0:
  62. return False
  63. return True
  64. def shorten_text(text, limit, symbol="[...]"):
  65. """Return a given text truncated at limit if longer than limit.
  66. On truncation, add symbol.
  67. """
  68. assert type(text) is str and type(symbol) is str and type(limit) is int
  69. if len(text) <= limit:
  70. return text
  71. return text[:limit-len(symbol)] + symbol
  72. def extract(text, starter=None, ender=None):
  73. """Return string in text between starter and ender.
  74. If starter is None, truncate at ender.
  75. """
  76. if starter and starter in text:
  77. text = text.partition(starter)[2]
  78. if ender:
  79. return text.partition(ender)[0]
  80. return text
  81. def make_button(text=None, callback_data='',
  82. prefix='', delimiter='|', data=None):
  83. """Return a Telegram bot API-compliant button.
  84. callback_data can be either a ready-to-use string or a
  85. prefix + delimiter-joined data.
  86. If callback_data exceeds Telegram limits (currently 60 characters),
  87. it gets truncated at the last delimiter before that limit.
  88. If absent, text is the same as callback_data.
  89. """
  90. if data is None:
  91. data = []
  92. if len(data):
  93. callback_data += delimiter.join(map(str, data))
  94. callback_data = "{p}{c}".format(
  95. p=prefix,
  96. c=callback_data
  97. )
  98. if len(callback_data) > 60:
  99. callback_data = callback_data[:61]
  100. callback_data = callback_data[:-1-callback_data[::-1].find(delimiter)]
  101. if text is None:
  102. text = callback_data
  103. return dict(
  104. text=text,
  105. callback_data=callback_data
  106. )
  107. def mkbtn(x, y):
  108. """Backward compatibility.
  109. Warning: this function will be deprecated sooner or later,
  110. stop using it and migrate to make_button.
  111. """
  112. return make_button(text=x, callback_data=y)
  113. def make_lines_of_buttons(buttons, row_len=1):
  114. """Split `buttons` list in a list of lists having length = `row_len`."""
  115. return [
  116. buttons[i:i + row_len]
  117. for i in range(
  118. 0,
  119. len(buttons),
  120. row_len
  121. )
  122. ]
  123. def make_inline_keyboard(buttons, row_len=1):
  124. """Make a Telegram API compliant inline keyboard."""
  125. return dict(
  126. inline_keyboard=make_lines_of_buttons(
  127. buttons,
  128. row_len
  129. )
  130. )
  131. async def async_get(url, mode='json', **kwargs):
  132. """Make an async get request.
  133. `mode`s allowed:
  134. * html
  135. * json
  136. * string
  137. Additional **kwargs may be passed.
  138. """
  139. if 'mode' in kwargs:
  140. mode = kwargs['mode']
  141. del kwargs['mode']
  142. return await async_request(
  143. url,
  144. method='get',
  145. mode=mode,
  146. **kwargs
  147. )
  148. async def async_post(url, mode='html', **kwargs):
  149. """Make an async post request.
  150. `mode`s allowed:
  151. * html
  152. * json
  153. * string
  154. Additional **kwargs may be passed (will be converted to `data`).
  155. """
  156. return await async_request(
  157. url,
  158. method='post',
  159. mode=mode,
  160. **kwargs
  161. )
  162. async def async_request(url, method='get', mode='json', encoding=None, errors='strict',
  163. **kwargs):
  164. """Make an async html request.
  165. `types` allowed
  166. * get
  167. * post
  168. `mode`s allowed:
  169. * html
  170. * json
  171. * string
  172. * picture
  173. Additional **kwargs may be passed.
  174. """
  175. try:
  176. async with aiohttp.ClientSession() as s:
  177. async with (
  178. s.get(url, timeout=30)
  179. if method == 'get'
  180. else s.post(url, timeout=30, data=kwargs)
  181. ) as r:
  182. if mode in ['html', 'json', 'string']:
  183. result = await r.text(encoding=encoding, errors=errors)
  184. else:
  185. result = await r.read()
  186. if encoding is not None:
  187. result = result.decode(encoding)
  188. except Exception as e:
  189. logging.error(
  190. 'Error making async request to {}:\n{}'.format(
  191. url,
  192. e
  193. ),
  194. exc_info=False
  195. ) # Set exc_info=True to debug
  196. return e
  197. if mode == 'json':
  198. try:
  199. result = json.loads(
  200. result
  201. )
  202. except json.decoder.JSONDecodeError:
  203. result = {}
  204. elif mode == 'html':
  205. result = BeautifulSoup(result, "html.parser")
  206. elif mode == 'string':
  207. result = result
  208. return result
  209. def json_read(file_, default=None, encoding='utf-8', **kwargs):
  210. """Return json parsing of `file_`, or `default` if file does not exist.
  211. `encoding` refers to how the file should be read.
  212. `kwargs` will be passed to json.load()
  213. """
  214. if default is None:
  215. default = {}
  216. if not os.path.isfile(file_):
  217. return default
  218. with open(file_, "r", encoding=encoding) as f:
  219. return json.load(f, **kwargs)
  220. def json_write(what, file_, encoding='utf-8', **kwargs):
  221. """Store `what` in json `file_`.
  222. `encoding` refers to how the file should be written.
  223. `kwargs` will be passed to json.dump()
  224. """
  225. with open(file_, "w", encoding=encoding) as f:
  226. return json.dump(what, f, indent=4, **kwargs)
  227. def csv_read(file_, default=None, encoding='utf-8',
  228. delimiter=',', quotechar='"', **kwargs):
  229. """Return csv parsing of `file_`, or `default` if file does not exist.
  230. `encoding` refers to how the file should be read.
  231. `delimiter` is the separator of fields.
  232. `quotechar` is the string delimiter.
  233. `kwargs` will be passed to csv.reader()
  234. """
  235. if default is None:
  236. default = []
  237. if not os.path.isfile(file_):
  238. return default
  239. result = []
  240. keys = []
  241. with open(file_, newline='', encoding=encoding) as csv_file:
  242. csv_reader = csv.reader(
  243. csv_file,
  244. delimiter=delimiter,
  245. quotechar=quotechar,
  246. **kwargs
  247. )
  248. for row in csv_reader:
  249. if not keys:
  250. keys = row
  251. continue
  252. item = collections.OrderedDict()
  253. for key, val in zip(keys, row):
  254. item[key] = val
  255. result.append(item)
  256. return result
  257. def csv_write(info=None, file_='output.csv', encoding='utf-8',
  258. delimiter=',', quotechar='"', **kwargs):
  259. """Store `info` in CSV `file_`.
  260. `encoding` refers to how the file should be read.
  261. `delimiter` is the separator of fields.
  262. `quotechar` is the string delimiter.
  263. `encoding` refers to how the file should be written.
  264. `kwargs` will be passed to csv.writer()
  265. """
  266. if info is None:
  267. info = []
  268. assert (
  269. type(info) is list
  270. and len(info) > 0
  271. ), "info must be a non-empty list"
  272. assert all(
  273. isinstance(row, dict)
  274. for row in info
  275. ), "Rows must be dictionaries!"
  276. with open(file_, 'w', newline='', encoding=encoding) as csv_file:
  277. csv_writer = csv.writer(
  278. csv_file,
  279. delimiter=delimiter,
  280. quotechar=quotechar,
  281. **kwargs
  282. )
  283. csv_writer.writerow(info[0].keys())
  284. for row in info:
  285. csv_writer.writerow(row.values())
  286. return
  287. class MyOD(collections.OrderedDict):
  288. """Subclass of OrderedDict.
  289. It features `get_by_val` and `get_by_key_val` methods.
  290. """
  291. def __init__(self, *args, **kwargs):
  292. """Return a MyOD instance."""
  293. super().__init__(*args, **kwargs)
  294. self._anti_list_casesensitive = None
  295. self._anti_list_caseinsensitive = None
  296. @property
  297. def anti_list_casesensitive(self):
  298. """Case-sensitive reverse dictionary.
  299. Keys and values are swapped.
  300. """
  301. if not self._anti_list_casesensitive:
  302. self._anti_list_casesensitive = {
  303. val: key
  304. for key, val in self.items()
  305. }
  306. return self._anti_list_casesensitive
  307. @property
  308. def anti_list_caseinsensitive(self):
  309. """Case-sensitive reverse dictionary.
  310. Keys and values are swapped and lowered.
  311. """
  312. if not self._anti_list_caseinsensitive:
  313. self._anti_list_caseinsensitive = {
  314. (val.lower() if type(val) is str else val): key
  315. for key, val in self.items()
  316. }
  317. return self._anti_list_caseinsensitive
  318. def get_by_val(self, val, case_sensitive=True):
  319. """Get key pointing to given val.
  320. Can be case-sensitive or insensitive.
  321. MyOD[key] = val <-- MyOD.get(key) = val <--> MyOD.get_by_val(val) = key
  322. """
  323. return (
  324. self.anti_list_casesensitive
  325. if case_sensitive
  326. else self.anti_list_caseinsensitive
  327. )[val]
  328. def get_by_key_val(self, key, val,
  329. case_sensitive=True, return_value=False):
  330. """Get key (or val) of a dict-like object having key == val.
  331. Perform case-sensitive or insensitive search.
  332. """
  333. for x, y in self.items():
  334. if (
  335. (
  336. y[key] == val
  337. and case_sensitive
  338. ) or (
  339. y[key].lower() == val.lower()
  340. and not case_sensitive
  341. )
  342. ):
  343. return (
  344. y if return_value
  345. else x
  346. )
  347. return None
  348. def line_drawing_unordered_list(list_):
  349. """Draw an old-fashioned unordered list.
  350. Unordered list example
  351. ├ An element
  352. ├ Another element
  353. └Last element
  354. """
  355. result = ""
  356. if list_:
  357. for x in list_[:-1]:
  358. result += "├ {}\n".format(x)
  359. result += "└ {}".format(list_[-1])
  360. return result
  361. def str_to_datetime(d):
  362. """Convert string to datetime.
  363. Dataset library often casts datetimes to str, this is a workaround.
  364. """
  365. if isinstance(d, datetime.datetime):
  366. return d
  367. return datetime.datetime.strptime(
  368. d,
  369. '%Y-%m-%d %H:%M:%S.%f'
  370. )
  371. def datetime_to_str(d):
  372. """Cast datetime to string."""
  373. if isinstance(d, str):
  374. d = str_to_datetime(d)
  375. if not isinstance(d, datetime.datetime):
  376. raise TypeError(
  377. 'Input of datetime_to_str function must be a datetime.datetime '
  378. 'object. Output is a str'
  379. )
  380. return '{:%Y-%m-%d %H:%M:%S.%f}'.format(d)
  381. class MyCounter:
  382. """Counter object, with a `lvl` method incrementing `n` property."""
  383. def __init__(self):
  384. """Initialize and get MyCounter instance."""
  385. self._n = 0
  386. return
  387. def lvl(self):
  388. """Increments and return self.n."""
  389. self._n += 1
  390. return self.n
  391. def reset(self):
  392. """Set self.n = 0."""
  393. self._n = 0
  394. return self.n
  395. def n(self):
  396. """Counter's value."""
  397. return self._n
  398. def wrapper(func, *args, **kwargs):
  399. """Wrap a function so that it can be later called with one argument."""
  400. def wrapped(update):
  401. return func(update, *args, **kwargs)
  402. return wrapped
  403. async def async_wrapper(coroutine, *args1, **kwargs1):
  404. """Wrap a `coroutine` so that it can be later awaited with more arguments.
  405. Set some of the arguments, let the coroutine be awaited with the rest of
  406. them later.
  407. The wrapped coroutine will always pass only supported parameters to
  408. `coroutine`.
  409. Example:
  410. ```
  411. import asyncio
  412. from davtelepot.utilities import async_wrapper
  413. async def printer(a, b, c, d):
  414. print(a, a+b, b+c, c+d)
  415. return
  416. async def main():
  417. my_coroutine = await async_wrapper(
  418. printer,
  419. c=3, d=2
  420. )
  421. await my_coroutine(a=1, b=5)
  422. loop = asyncio.new_event_loop()
  423. asyncio.set_event_loop(loop)
  424. asyncio.run(main())
  425. ```
  426. """
  427. async def wrapped_coroutine(*args2, bot=None, update=None, user_record=None, **kwargs2):
  428. # Update keyword arguments
  429. kwargs1.update(kwargs2)
  430. kwargs1['bot'] = bot
  431. kwargs1['update'] = update
  432. kwargs1['user_record'] = user_record
  433. # Pass only supported arguments
  434. kwargs = {
  435. name: argument
  436. for name, argument in kwargs1.items()
  437. if name in inspect.signature(
  438. coroutine
  439. ).parameters
  440. }
  441. return await coroutine(*args1, *args2, **kwargs)
  442. return wrapped_coroutine
  443. def forwarded(by=None):
  444. """Check that update is forwarded, optionally `by` someone in particular.
  445. Decorator: such decorated functions have effect only if update
  446. is forwarded from someone (you can specify `by` whom).
  447. """
  448. def is_forwarded_by(update):
  449. if 'forward_from' not in update:
  450. return False
  451. if by and update['forward_from']['id'] != by:
  452. return False
  453. return True
  454. def decorator(view_func):
  455. if asyncio.iscoroutinefunction(view_func):
  456. async def decorated(update):
  457. if is_forwarded_by(update):
  458. return await view_func(update)
  459. else:
  460. def decorated(update):
  461. if is_forwarded_by(update):
  462. return view_func(update)
  463. return decorated
  464. return decorator
  465. def chat_selective(chat_id=None):
  466. """Check that update comes from a chat, optionally having `chat_id`.
  467. Such decorated functions have effect only if update comes from
  468. a specific (if `chat_id` is given) or generic chat.
  469. """
  470. def check_function(update):
  471. if 'chat' not in update:
  472. return False
  473. if chat_id:
  474. if update['chat']['id'] != chat_id:
  475. return False
  476. return True
  477. def decorator(view_func):
  478. if asyncio.iscoroutinefunction(view_func):
  479. async def decorated(update):
  480. if check_function(update):
  481. return await view_func(update)
  482. else:
  483. def decorated(update):
  484. if check_function(update):
  485. return view_func(update)
  486. return decorated
  487. return decorator
  488. async def sleep_until(when: Union[datetime.datetime, datetime.timedelta]):
  489. """Sleep until now > `when`.
  490. `when` could be a datetime.datetime or a datetime.timedelta instance.
  491. """
  492. if not (
  493. isinstance(when, datetime.datetime)
  494. or isinstance(when, datetime.timedelta)
  495. ):
  496. raise TypeError(
  497. "sleep_until takes a datetime.datetime or datetime.timedelta "
  498. "object as argument!"
  499. )
  500. if isinstance(when, datetime.datetime):
  501. delta = when - datetime.datetime.now()
  502. elif isinstance(when, datetime.timedelta):
  503. delta = when
  504. else:
  505. delta = datetime.timedelta(seconds=1)
  506. if delta.days >= 0:
  507. await asyncio.sleep(
  508. delta.seconds
  509. )
  510. return
  511. async def wait_and_do(when, what, *args, **kwargs):
  512. """Sleep until `when`, then call `what` passing `args` and `kwargs`."""
  513. await sleep_until(when)
  514. return await what(*args, **kwargs)
  515. def get_csv_string(list_, delimiter=',', quotechar='"'):
  516. """Return a `delimiter`-delimited string of `list_` items.
  517. Wrap strings in `quotechar`s.
  518. """
  519. return delimiter.join(
  520. str(item) if type(item) is not str
  521. else '{q}{i}{q}'.format(
  522. i=item,
  523. q=quotechar
  524. )
  525. for item in list_
  526. )
  527. def case_accent_insensitive_sql(field):
  528. """Get a SQL string to perform a case- and accent-insensitive query.
  529. Given a `field`, return a part of SQL string necessary to perform
  530. a case- and accent-insensitive query.
  531. """
  532. replacements = [
  533. (' ', ''),
  534. ('à', 'a'),
  535. ('è', 'e'),
  536. ('é', 'e'),
  537. ('ì', 'i'),
  538. ('ò', 'o'),
  539. ('ù', 'u'),
  540. ]
  541. return "{r}LOWER({f}){w}".format(
  542. r="replace(".upper()*len(replacements),
  543. f=field,
  544. w=''.join(
  545. ", '{w[0]}', '{w[1]}')".format(w=w)
  546. for w in replacements
  547. )
  548. )
  549. # Italian definite articles.
  550. ARTICOLI = MyOD()
  551. ARTICOLI[1] = {
  552. 'ind': 'un',
  553. 'dets': 'il',
  554. 'detp': 'i',
  555. 'dess': 'l',
  556. 'desp': 'i'
  557. }
  558. ARTICOLI[2] = {
  559. 'ind': 'una',
  560. 'dets': 'la',
  561. 'detp': 'le',
  562. 'dess': 'lla',
  563. 'desp': 'lle'
  564. }
  565. ARTICOLI[3] = {
  566. 'ind': 'uno',
  567. 'dets': 'lo',
  568. 'detp': 'gli',
  569. 'dess': 'llo',
  570. 'desp': 'gli'
  571. }
  572. ARTICOLI[4] = {
  573. 'ind': 'un',
  574. 'dets': 'l\'',
  575. 'detp': 'gli',
  576. 'dess': 'll\'',
  577. 'desp': 'gli'
  578. }
  579. class Gettable:
  580. """Gettable objects can be retrieved from memory without being duplicated.
  581. Key is the primary key.
  582. Use class method get to instantiate (or retrieve) Gettable objects.
  583. Assign SubClass.instances = {}, otherwise Gettable.instances will
  584. contain SubClass objects.
  585. """
  586. instances = {}
  587. def __init__(self, *args, key=None, **kwargs):
  588. if key is None:
  589. key = args[0]
  590. if key not in self.__class__.instances:
  591. self.__class__.instances[key] = self
  592. @classmethod
  593. def get(cls, *args, key=None, **kwargs):
  594. """Instantiate and/or retrieve Gettable object.
  595. SubClass.instances is searched if exists.
  596. Gettable.instances is searched otherwise.
  597. """
  598. if key is None:
  599. key = args[0]
  600. else:
  601. kwargs['key'] = key
  602. if key not in cls.instances:
  603. cls.instances[key] = cls(*args, **kwargs)
  604. return cls.instances[key]
  605. class Confirmable:
  606. """Confirmable objects are provided with a confirm instance method.
  607. It evaluates True if it was called within self._confirm_timedelta,
  608. False otherwise.
  609. When it returns True, timer is reset.
  610. """
  611. CONFIRM_TIMEDELTA = datetime.timedelta(seconds=10)
  612. def __init__(self, confirm_timedelta: Union[datetime.timedelta, int] = None):
  613. """Instantiate Confirmable instance.
  614. If `confirm_timedelta` is not passed,
  615. `self.__class__.CONFIRM_TIMEDELTA` is used as default.
  616. """
  617. if confirm_timedelta is None:
  618. confirm_timedelta = self.__class__.CONFIRM_TIMEDELTA
  619. elif type(confirm_timedelta) is int:
  620. confirm_timedelta = datetime.timedelta(seconds=confirm_timedelta)
  621. self._confirm_timedelta = None
  622. self.set_confirm_timedelta(confirm_timedelta)
  623. self._confirm_datetimes = {}
  624. @property
  625. def confirm_timedelta(self):
  626. """Maximum timedelta between two calls of `confirm`."""
  627. return self._confirm_timedelta
  628. def confirm_datetime(self, who='unique'):
  629. """Get datetime of `who`'s last confirm.
  630. If `who` never called `confirm`, fake an expired call.
  631. """
  632. if who not in self._confirm_datetimes:
  633. self._confirm_datetimes[who] = (
  634. datetime.datetime.now()
  635. - 2*self.confirm_timedelta
  636. )
  637. return self._confirm_datetimes[who]
  638. def set_confirm_timedelta(self, confirm_timedelta):
  639. """Change self._confirm_timedelta."""
  640. if type(confirm_timedelta) is int:
  641. confirm_timedelta = datetime.timedelta(
  642. seconds=confirm_timedelta
  643. )
  644. assert isinstance(
  645. confirm_timedelta, datetime.timedelta
  646. ), "confirm_timedelta must be a datetime.timedelta instance!"
  647. self._confirm_timedelta = confirm_timedelta
  648. def confirm(self, who='unique'):
  649. """Return True if `confirm` was called by `who` recetly enough."""
  650. now = datetime.datetime.now()
  651. if now >= self.confirm_datetime(who) + self.confirm_timedelta:
  652. self._confirm_datetimes[who] = now
  653. return False
  654. self._confirm_datetimes[who] = now - 2*self.confirm_timedelta
  655. return True
  656. class HasBot:
  657. """Objects having a Bot subclass object as `.bot` attribute.
  658. HasBot objects have a .bot and .db properties for faster access.
  659. """
  660. _bot = None
  661. @property
  662. def bot(self):
  663. """Class bot."""
  664. return self.__class__._bot
  665. @property
  666. def db(self):
  667. """Class bot db."""
  668. return self.bot.db
  669. @classmethod
  670. def set_bot(cls, bot):
  671. """Change class bot."""
  672. cls._bot = bot
  673. class CachedPage(Gettable):
  674. """Cache a web page and return it during CACHE_TIME, otherwise refresh.
  675. Usage:
  676. cached_page = CachedPage.get(
  677. 'https://www.google.com',
  678. datetime.timedelta(seconds=30),
  679. **kwargs
  680. )
  681. page = await cached_page.get_page()
  682. """
  683. CACHE_TIME = datetime.timedelta(minutes=5)
  684. instances = {}
  685. def __init__(self, url, cache_time=None, **async_get_kwargs):
  686. """Instantiate CachedPage object.
  687. `url`: the URL to be cached
  688. `cache_time`: timedelta from last_update during which
  689. page will be cached
  690. `**kwargs` will be passed to async_get function
  691. """
  692. self._url = url
  693. if type(cache_time) is int:
  694. cache_time = datetime.timedelta(seconds=cache_time)
  695. if cache_time is None:
  696. cache_time = self.__class__.CACHE_TIME
  697. assert type(cache_time) is datetime.timedelta, (
  698. "Cache time must be a datetime.timedelta object!"
  699. )
  700. self._cache_time = cache_time
  701. self._page = None
  702. self._last_update = datetime.datetime.now() - self.cache_time
  703. self._async_get_kwargs = async_get_kwargs
  704. super().__init__(key=url)
  705. @property
  706. def url(self):
  707. """Get cached page url."""
  708. return self._url
  709. @property
  710. def cache_time(self):
  711. """Get cache time."""
  712. return self._cache_time
  713. @property
  714. def page(self):
  715. """Get webpage."""
  716. return self._page
  717. @property
  718. def last_update(self):
  719. """Get datetime of last update."""
  720. return self._last_update
  721. @property
  722. def async_get_kwargs(self):
  723. """Get async get request keyword arguments."""
  724. return self._async_get_kwargs
  725. @property
  726. def is_old(self):
  727. """Evaluate True if `chache_time` has passed since last update."""
  728. return datetime.datetime.now() > self.last_update + self.cache_time
  729. async def refresh(self):
  730. """Update cached web page."""
  731. try:
  732. self._page = await async_get(self.url, **self.async_get_kwargs)
  733. self._last_update = datetime.datetime.now()
  734. return 0
  735. except Exception as e:
  736. self._page = None
  737. logging.error(
  738. '{e}'.format(
  739. e=e
  740. ),
  741. exc_info=False
  742. ) # Set exc_info=True to debug
  743. return 1
  744. async def get_page(self):
  745. """Refresh if necessary and return web page."""
  746. if self.is_old:
  747. await self.refresh()
  748. return self.page
  749. class Confirmator(Gettable, Confirmable):
  750. """Gettable Confirmable object."""
  751. instances = {}
  752. def __init__(self, key, *args, confirm_timedelta=None):
  753. """Call Confirmable.__init__ passing `confirm_timedelta`."""
  754. Confirmable.__init__(self, confirm_timedelta)
  755. Gettable.__init__(self, key=key, *args)
  756. def get_cleaned_text(update, bot=None, replace=None, strip='/ @'):
  757. """Clean `update`['text'] and return it.
  758. Strip `bot`.name and items to be `replace`d from the beginning of text.
  759. Strip `strip` characters from both ends.
  760. """
  761. if replace is None:
  762. replace = []
  763. if bot is not None:
  764. replace.append(
  765. '@{.name}'.format(
  766. bot
  767. )
  768. )
  769. text = update['text'].strip(strip)
  770. # Replace longer strings first
  771. for s in sorted(replace, key=len, reverse=True):
  772. while s and text.lower().startswith(s.lower()):
  773. text = text[len(s):]
  774. return text.strip(strip)
  775. def get_user(record, link_profile=True):
  776. """Get an HTML Telegram tag for user `record`."""
  777. if not record:
  778. return
  779. from_ = {key: val for key, val in record.items()}
  780. result = '{name}'
  781. if 'telegram_id' in from_:
  782. from_['id'] = from_['telegram_id']
  783. if (
  784. 'id' in from_
  785. and from_['id'] is not None
  786. and link_profile
  787. ):
  788. result = f"""<a href="tg://user?id={from_['id']}">{{name}}</a>"""
  789. if 'username' in from_ and from_['username']:
  790. result = result.format(
  791. name=from_['username']
  792. )
  793. elif (
  794. 'first_name' in from_
  795. and from_['first_name']
  796. and 'last_name' in from_
  797. and from_['last_name']
  798. ):
  799. result = result.format(
  800. name=f"{from_['first_name']} {from_['last_name']}"
  801. )
  802. elif 'first_name' in from_ and from_['first_name']:
  803. result = result.format(
  804. name=from_['first_name']
  805. )
  806. elif 'last_name' in from_ and from_['last_name']:
  807. result = result.format(
  808. name=from_['last_name']
  809. )
  810. else:
  811. result = result.format(
  812. name="Utente anonimo"
  813. )
  814. return result
  815. def datetime_from_utc_to_local(utc_datetime):
  816. """Convert `utc_datetime` to local datetime."""
  817. now_timestamp = time.time()
  818. offset = (
  819. datetime.datetime.fromtimestamp(now_timestamp)
  820. - datetime.datetime.utcfromtimestamp(now_timestamp)
  821. )
  822. return utc_datetime + offset
  823. # TIME_SYMBOLS from more specific to less specific (avoid false positives!)
  824. TIME_SYMBOLS = MyOD()
  825. TIME_SYMBOLS["'"] = 'minutes'
  826. TIME_SYMBOLS["settimana"] = 'weeks'
  827. TIME_SYMBOLS["settimane"] = 'weeks'
  828. TIME_SYMBOLS["weeks"] = 'weeks'
  829. TIME_SYMBOLS["week"] = 'weeks'
  830. TIME_SYMBOLS["giorno"] = 'days'
  831. TIME_SYMBOLS["giorni"] = 'days'
  832. TIME_SYMBOLS["secondi"] = 'seconds'
  833. TIME_SYMBOLS["seconds"] = 'seconds'
  834. TIME_SYMBOLS["secondo"] = 'seconds'
  835. TIME_SYMBOLS["minuti"] = 'minutes'
  836. TIME_SYMBOLS["minuto"] = 'minutes'
  837. TIME_SYMBOLS["minute"] = 'minutes'
  838. TIME_SYMBOLS["minutes"] = 'minutes'
  839. TIME_SYMBOLS["day"] = 'days'
  840. TIME_SYMBOLS["days"] = 'days'
  841. TIME_SYMBOLS["ore"] = 'hours'
  842. TIME_SYMBOLS["ora"] = 'hours'
  843. TIME_SYMBOLS["sec"] = 'seconds'
  844. TIME_SYMBOLS["min"] = 'minutes'
  845. TIME_SYMBOLS["m"] = 'minutes'
  846. TIME_SYMBOLS["h"] = 'hours'
  847. TIME_SYMBOLS["d"] = 'days'
  848. TIME_SYMBOLS["s"] = 'seconds'
  849. def _interval_parser(text, result):
  850. text = text.lower()
  851. succeeded = False
  852. if result is None:
  853. result = []
  854. if len(result) == 0 or result[-1]['ok']:
  855. text_part = ''
  856. _text = text # I need to iterate through _text modifying text
  857. for char in _text:
  858. if not char.isnumeric():
  859. break
  860. else:
  861. text_part += char
  862. text = text[1:]
  863. if text_part.isnumeric():
  864. result.append(
  865. dict(
  866. unit=None,
  867. value=int(text_part),
  868. ok=False
  869. )
  870. )
  871. succeeded = True, True
  872. if text:
  873. dummy, result = _interval_parser(text, result)
  874. elif len(result) > 0 and not result[-1]['ok']:
  875. text_part = ''
  876. _text = text # I need to iterate through _text modifying text
  877. for char in _text:
  878. if char.isnumeric():
  879. break
  880. else:
  881. text_part += char
  882. text = text[1:]
  883. for time_symbol, unit in TIME_SYMBOLS.items():
  884. if time_symbol in text_part:
  885. result[-1]['unit'] = unit
  886. result[-1]['ok'] = True
  887. succeeded = True, True
  888. break
  889. else:
  890. result.pop()
  891. if text:
  892. dummy, result = _interval_parser(text, result)
  893. return succeeded, result
  894. def _date_parser(text, result):
  895. succeeded = False
  896. if 3 <= len(text) <= 10 and text.count('/') >= 1:
  897. if 3 <= len(text) <= 5 and text.count('/') == 1:
  898. text += '/{:%y}'.format(datetime.datetime.now())
  899. if 6 <= len(text) <= 10 and text.count('/') == 2:
  900. day, month, year = [
  901. int(n) for n in [
  902. ''.join(char)
  903. for char in text.split('/')
  904. if char.isnumeric()
  905. ]
  906. ]
  907. if year < 100:
  908. year += 2000
  909. if result is None:
  910. result = []
  911. result += [
  912. dict(
  913. unit='day',
  914. value=day,
  915. ok=True
  916. ),
  917. dict(
  918. unit='month',
  919. value=month,
  920. ok=True
  921. ),
  922. dict(
  923. unit='year',
  924. value=year,
  925. ok=True
  926. )
  927. ]
  928. succeeded = True, True
  929. return succeeded, result
  930. def _time_parser(text, result):
  931. succeeded = False
  932. if (1 <= len(text) <= 8) and any(char.isnumeric() for char in text):
  933. text = ''.join(
  934. ':' if char == '.' else char
  935. for char in text
  936. if char.isnumeric() or char in (':', '.')
  937. )
  938. if len(text) <= 2:
  939. text = '{:02d}:00:00'.format(int(text))
  940. elif len(text) == 4 and ':' not in text:
  941. text = '{:02d}:{:02d}:00'.format(
  942. *[int(x) for x in (text[:2], text[2:])]
  943. )
  944. elif text.count(':') == 1:
  945. text = '{:02d}:{:02d}:00'.format(
  946. *[int(x) for x in text.split(':')]
  947. )
  948. if text.count(':') == 2:
  949. hour, minute, second = (int(x) for x in text.split(':'))
  950. if (
  951. 0 <= hour <= 24
  952. and 0 <= minute <= 60
  953. and 0 <= second <= 60
  954. ):
  955. if result is None:
  956. result = []
  957. result += [
  958. dict(
  959. unit='hour',
  960. value=hour,
  961. ok=True
  962. ),
  963. dict(
  964. unit='minute',
  965. value=minute,
  966. ok=True
  967. ),
  968. dict(
  969. unit='second',
  970. value=second,
  971. ok=True
  972. )
  973. ]
  974. succeeded = True
  975. return succeeded, result
  976. WEEKDAY_NAMES_ITA = ["Lunedì", "Martedì", "Mercoledì", "Giovedì",
  977. "Venerdì", "Sabato", "Domenica"]
  978. WEEKDAY_NAMES_ENG = ["Monday", "Tuesday", "Wednesday", "Thursday",
  979. "Friday", "Saturday", "Sunday"]
  980. def _period_parser(text, result):
  981. if text.title() in WEEKDAY_NAMES_ITA + WEEKDAY_NAMES_ENG:
  982. day_code = (WEEKDAY_NAMES_ITA + WEEKDAY_NAMES_ENG).index(text.title())
  983. if day_code > 6:
  984. day_code -= 7
  985. today = datetime.date.today()
  986. days = 1
  987. while (today + datetime.timedelta(days=days)).weekday() != day_code:
  988. days += 1
  989. if result is None:
  990. result = []
  991. result.append(
  992. dict(
  993. unit='days',
  994. value=days,
  995. ok=True,
  996. weekly=True
  997. )
  998. )
  999. succeeded = True
  1000. else:
  1001. succeeded, result = _interval_parser(text, result)
  1002. return succeeded, result
  1003. TIME_WORDS = {
  1004. 'tra': dict(
  1005. parser=_interval_parser,
  1006. recurring=False,
  1007. type_='delta'
  1008. ),
  1009. 'in': dict(
  1010. parser=_interval_parser,
  1011. recurring=False,
  1012. type_='delta'
  1013. ),
  1014. 'at': dict(
  1015. parser=_time_parser,
  1016. recurring=False,
  1017. type_='set'
  1018. ),
  1019. 'on': dict(
  1020. parser=_date_parser,
  1021. recurring=False,
  1022. type_='set'
  1023. ),
  1024. 'alle': dict(
  1025. parser=_time_parser,
  1026. recurring=False,
  1027. type_='set'
  1028. ),
  1029. 'il': dict(
  1030. parser=_date_parser,
  1031. recurring=False,
  1032. type_='set'
  1033. ),
  1034. 'every': dict(
  1035. parser=_period_parser,
  1036. recurring=True,
  1037. type_='delta'
  1038. ),
  1039. 'ogni': dict(
  1040. parser=_period_parser,
  1041. recurring=True,
  1042. type_='delta'
  1043. ),
  1044. }
  1045. def parse_datetime_interval_string(text):
  1046. """Parse `text` and return text, datetime and timedelta."""
  1047. parsers = []
  1048. result_text, result_datetime, result_timedelta = [], None, None
  1049. is_quoted_text = False
  1050. # Replace multiple spaces with single space character
  1051. text = re.sub(r'\s\s+', ' ', text)
  1052. for word in text.split(' '):
  1053. if word.count('"') % 2:
  1054. is_quoted_text = not is_quoted_text
  1055. if is_quoted_text or '"' in word:
  1056. result_text.append(
  1057. word.replace('"', '') if 'href=' not in word else word
  1058. )
  1059. continue
  1060. result_text.append(word)
  1061. word = word.lower()
  1062. succeeded = False
  1063. if len(parsers) > 0:
  1064. succeeded, result = parsers[-1]['parser'](
  1065. word,
  1066. parsers[-1]['result']
  1067. )
  1068. if succeeded:
  1069. parsers[-1]['result'] = result
  1070. if not succeeded and word in TIME_WORDS:
  1071. parsers.append(
  1072. dict(
  1073. result=None,
  1074. parser=TIME_WORDS[word]['parser'],
  1075. recurring=TIME_WORDS[word]['recurring'],
  1076. type_=TIME_WORDS[word]['type_']
  1077. )
  1078. )
  1079. if succeeded:
  1080. result_text.pop()
  1081. if len(result_text) > 0 and result_text[-1].lower() in TIME_WORDS:
  1082. result_text.pop()
  1083. result_text = escape_html_chars(
  1084. ' '.join(result_text)
  1085. )
  1086. parsers = list(
  1087. filter(
  1088. lambda x: 'result' in x and x['result'],
  1089. parsers
  1090. )
  1091. )
  1092. recurring_event = False
  1093. weekly = False
  1094. _timedelta = datetime.timedelta()
  1095. _datetime = None
  1096. _now = datetime.datetime.now()
  1097. for parser in parsers:
  1098. if parser['recurring']:
  1099. recurring_event = True
  1100. type_ = parser['type_']
  1101. for result in parser['result']:
  1102. if not isinstance(result, dict) or not result['ok']:
  1103. continue
  1104. if recurring_event and 'weekly' in result and result['weekly']:
  1105. weekly = True
  1106. if type_ == 'set':
  1107. if _datetime is None:
  1108. _datetime = _now
  1109. _datetime = _datetime.replace(
  1110. **{
  1111. result['unit']: result['value']
  1112. }
  1113. )
  1114. elif type_ == 'delta':
  1115. _timedelta += datetime.timedelta(
  1116. **{
  1117. result['unit']: result['value']
  1118. }
  1119. )
  1120. if _datetime:
  1121. result_datetime = _datetime
  1122. if _timedelta:
  1123. if result_datetime is None:
  1124. result_datetime = _now
  1125. if recurring_event:
  1126. result_timedelta = _timedelta
  1127. if weekly:
  1128. result_timedelta = datetime.timedelta(days=7)
  1129. else:
  1130. result_datetime += _timedelta
  1131. while result_datetime and result_datetime < datetime.datetime.now():
  1132. result_datetime += (
  1133. result_timedelta
  1134. if result_timedelta
  1135. else datetime.timedelta(days=1)
  1136. )
  1137. return result_text, result_datetime, result_timedelta
  1138. DAY_GAPS = {
  1139. -1: 'ieri',
  1140. -2: 'avantieri',
  1141. 0: 'oggi',
  1142. 1: 'domani',
  1143. 2: 'dopodomani'
  1144. }
  1145. MONTH_NAMES_ITA = MyOD()
  1146. MONTH_NAMES_ITA[1] = "gennaio"
  1147. MONTH_NAMES_ITA[2] = "febbraio"
  1148. MONTH_NAMES_ITA[3] = "marzo"
  1149. MONTH_NAMES_ITA[4] = "aprile"
  1150. MONTH_NAMES_ITA[5] = "maggio"
  1151. MONTH_NAMES_ITA[6] = "giugno"
  1152. MONTH_NAMES_ITA[7] = "luglio"
  1153. MONTH_NAMES_ITA[8] = "agosto"
  1154. MONTH_NAMES_ITA[9] = "settembre"
  1155. MONTH_NAMES_ITA[10] = "ottobre"
  1156. MONTH_NAMES_ITA[11] = "novembre"
  1157. MONTH_NAMES_ITA[12] = "dicembre"
  1158. def beautytd(td):
  1159. """Format properly timedeltas."""
  1160. result = ''
  1161. if type(td) is int:
  1162. td = datetime.timedelta(seconds=td)
  1163. assert isinstance(
  1164. td,
  1165. datetime.timedelta
  1166. ), "td must be a datetime.timedelta object!"
  1167. mtd = datetime.timedelta
  1168. if td < mtd(minutes=1):
  1169. result = "{:.0f} secondi".format(
  1170. td.total_seconds()
  1171. )
  1172. elif td < mtd(minutes=60):
  1173. result = "{:.0f} min{}".format(
  1174. td.total_seconds()//60,
  1175. (
  1176. " {:.0f} s".format(
  1177. td.total_seconds() % 60
  1178. )
  1179. ) if td.total_seconds() % 60 else ''
  1180. )
  1181. elif td < mtd(days=1):
  1182. result = "{:.0f} h{}".format(
  1183. td.total_seconds()//3600,
  1184. (
  1185. " {:.0f} min".format(
  1186. (td.total_seconds() % 3600) // 60
  1187. )
  1188. ) if td.total_seconds() % 3600 else ''
  1189. )
  1190. elif td < mtd(days=30):
  1191. result = "{} giorni{}".format(
  1192. td.days,
  1193. (
  1194. " {:.0f} h".format(
  1195. td.total_seconds() % (3600*24) // 3600
  1196. )
  1197. ) if td.total_seconds() % (3600*24) else ''
  1198. )
  1199. return result
  1200. def beautydt(dt):
  1201. """Format a datetime in a smart way."""
  1202. if type(dt) is str:
  1203. dt = str_to_datetime(dt)
  1204. assert isinstance(
  1205. dt,
  1206. datetime.datetime
  1207. ), "dt must be a datetime.datetime object!"
  1208. now = datetime.datetime.now()
  1209. gap = dt - now
  1210. gap_days = (dt.date() - now.date()).days
  1211. result = "alle {dt:%H:%M}".format(
  1212. dt=dt
  1213. )
  1214. if abs(gap) < datetime.timedelta(minutes=30):
  1215. result += ":{dt:%S}".format(dt=dt)
  1216. if -2 <= gap_days <= 2:
  1217. result += " di {dg}".format(
  1218. dg=DAY_GAPS[gap_days]
  1219. )
  1220. elif gap.days not in (-1, 0):
  1221. result += " del {d}{m}".format(
  1222. d=dt.day,
  1223. m=(
  1224. "" if now.year == dt.year and now.month == dt.month
  1225. else " {m}{y}".format(
  1226. m=MONTH_NAMES_ITA[dt.month].title(),
  1227. y="" if now.year == dt.year
  1228. else " {}".format(dt.year)
  1229. )
  1230. )
  1231. )
  1232. return result
  1233. HTML_SYMBOLS = MyOD()
  1234. HTML_SYMBOLS["&"] = "&amp;"
  1235. HTML_SYMBOLS["<"] = "&lt;"
  1236. HTML_SYMBOLS[">"] = "&gt;"
  1237. HTML_SYMBOLS["\""] = "&quot;"
  1238. HTML_SYMBOLS["&lt;b&gt;"] = "<b>"
  1239. HTML_SYMBOLS["&lt;/b&gt;"] = "</b>"
  1240. HTML_SYMBOLS["&lt;i&gt;"] = "<i>"
  1241. HTML_SYMBOLS["&lt;/i&gt;"] = "</i>"
  1242. HTML_SYMBOLS["&lt;code&gt;"] = "<code>"
  1243. HTML_SYMBOLS["&lt;/code&gt;"] = "</code>"
  1244. HTML_SYMBOLS["&lt;pre&gt;"] = "<pre>"
  1245. HTML_SYMBOLS["&lt;/pre&gt;"] = "</pre>"
  1246. HTML_SYMBOLS["&lt;a href=&quot;"] = "<a href=\""
  1247. HTML_SYMBOLS["&quot;&gt;"] = "\">"
  1248. HTML_SYMBOLS["&lt;/a&gt;"] = "</a>"
  1249. HTML_TAGS = [
  1250. None, "<b>", "</b>",
  1251. None, "<i>", "</i>",
  1252. None, "<code>", "</code>",
  1253. None, "<pre>", "</pre>",
  1254. None, "<a href=\"", "\">", "</a>",
  1255. None
  1256. ]
  1257. def remove_html_tags(text):
  1258. """Remove HTML tags from `text`."""
  1259. for tag in HTML_TAGS:
  1260. if tag is None:
  1261. continue
  1262. text = text.replace(tag, '')
  1263. return text
  1264. def escape_html_chars(text):
  1265. """Escape HTML chars if not part of a tag."""
  1266. for s, r in HTML_SYMBOLS.items():
  1267. text = text.replace(s, r)
  1268. copy = text
  1269. expected_tag = None
  1270. while copy:
  1271. min_ = min(
  1272. (
  1273. dict(
  1274. position=copy.find(tag) if tag in copy else len(copy),
  1275. tag=tag
  1276. )
  1277. for tag in HTML_TAGS
  1278. if tag
  1279. ),
  1280. key=lambda x: x['position'],
  1281. default=0
  1282. )
  1283. if min_['position'] == len(copy):
  1284. break
  1285. if expected_tag and min_['tag'] != expected_tag:
  1286. return text.replace('<', '_').replace('>', '_')
  1287. expected_tag = HTML_TAGS[HTML_TAGS.index(min_['tag'])+1]
  1288. copy = extract(copy, min_['tag'])
  1289. return text
  1290. def accents_to_jolly(text, lower=True):
  1291. """Replace letters with Italian accents with SQL jolly character."""
  1292. to_be_replaced = ('à', 'è', 'é', 'ì', 'ò', 'ù')
  1293. if lower:
  1294. text = text.lower()
  1295. else:
  1296. to_be_replaced += tuple(s.upper() for s in to_be_replaced)
  1297. for s in to_be_replaced:
  1298. text = text.replace(s, '_')
  1299. return text.replace("'", "''")
  1300. def get_secure_key(allowed_chars=None, length=6):
  1301. """Get a randomly-generate secure key.
  1302. You can specify a set of `allowed_chars` and a `length`.
  1303. """
  1304. if allowed_chars is None:
  1305. allowed_chars = string.ascii_uppercase + string.digits
  1306. return ''.join(
  1307. random.SystemRandom().choice(
  1308. allowed_chars
  1309. )
  1310. for _ in range(length)
  1311. )
  1312. def round_to_minute(datetime_):
  1313. """Round `datetime_` to closest minute."""
  1314. return (
  1315. datetime_ + datetime.timedelta(seconds=30)
  1316. ).replace(second=0, microsecond=0)
  1317. def get_line_by_content(text, key):
  1318. """Get line of `text` containing `key`."""
  1319. for line in text.split('\n'):
  1320. if key in line:
  1321. return line
  1322. return
  1323. def str_to_int(string_):
  1324. """Cast str to int, ignoring non-numeric characters."""
  1325. string_ = ''.join(
  1326. char
  1327. for char in string_
  1328. if char.isnumeric()
  1329. )
  1330. if len(string_) == 0:
  1331. string_ = '0'
  1332. return int(string_)
  1333. def starting_with_or_similar_to(a, b):
  1334. """Return similarity between two strings.
  1335. Least similar equals 0, most similar evaluates 1.
  1336. If similarity is less than 0.75, return 1 if one string starts with
  1337. the other and return 0.5 if one string is contained in the other.
  1338. """
  1339. a = a.lower()
  1340. b = b.lower()
  1341. similarity = SequenceMatcher(None, a, b).ratio()
  1342. if similarity < 0.75:
  1343. if b.startswith(a) or a.startswith(b):
  1344. return 1
  1345. if b in a or a in b:
  1346. return 0.5
  1347. return similarity
  1348. def pick_most_similar_from_list(list_, item):
  1349. """Return element from `list_` which is most similar to `item`.
  1350. Similarity is evaluated using `starting_with_or_similar_to`.
  1351. """
  1352. return max(
  1353. list_,
  1354. key=lambda element: starting_with_or_similar_to(
  1355. item,
  1356. element
  1357. )
  1358. )
  1359. def run_aiohttp_server(app, *args, **kwargs):
  1360. """Run an aiohttp web app, with its positional and keyword arguments.
  1361. Useful to run apps in dedicated threads.
  1362. """
  1363. loop = asyncio.new_event_loop()
  1364. asyncio.set_event_loop(loop)
  1365. aiohttp.web.run_app(app, *args, **kwargs)
  1366. def custom_join(_list, joiner, final=None):
  1367. """Join elements of `_list` using `joiner` (`final` as last joiner)."""
  1368. _list = list(map(str, _list))
  1369. if final is None:
  1370. final = joiner
  1371. if len(_list) == 0:
  1372. return ''
  1373. if len(_list) == 1:
  1374. return _list[0]
  1375. if len(_list) == 2:
  1376. return final.join(_list)
  1377. return joiner.join(_list[:-1]) + final + _list[-1]
  1378. def make_inline_query_answer(answer):
  1379. """Return an article-type answer to inline query.
  1380. Takes either a string or a dictionary and returns a list.
  1381. """
  1382. if type(answer) is str:
  1383. answer = dict(
  1384. type='article',
  1385. id=0,
  1386. title=remove_html_tags(answer),
  1387. input_message_content=dict(
  1388. message_text=answer,
  1389. parse_mode='HTML'
  1390. )
  1391. )
  1392. if type(answer) is dict:
  1393. answer = [answer]
  1394. return answer
  1395. # noinspection PyUnusedLocal
  1396. async def dummy_coroutine(*args, **kwargs):
  1397. """Accept everything as argument and do nothing."""
  1398. return
  1399. async def send_csv_file(bot, chat_id: int, query: str, caption: str = None,
  1400. file_name: str = 'File.csv', language: str = None,
  1401. user_record=None, update=None):
  1402. """Run a query on `bot` database and send result as CSV file to `chat_id`.
  1403. Optional parameters `caption` and `file_name` may be passed to this
  1404. function.
  1405. """
  1406. if update is None:
  1407. update = dict()
  1408. if language is None:
  1409. language = bot.get_language(update=update,
  1410. user_record=user_record)
  1411. try:
  1412. with bot.db as db:
  1413. record = db.query(
  1414. query
  1415. )
  1416. header_line = []
  1417. body_lines = []
  1418. for row in record:
  1419. if not header_line:
  1420. header_line.append(get_csv_string(row.keys()))
  1421. body_lines.append(get_csv_string(row.values()))
  1422. text = '\n'.join(header_line + body_lines)
  1423. except Exception as e:
  1424. text = "{message}\n{e}".format(
  1425. message=bot.get_message('admin', 'query_button', 'error',
  1426. language=language),
  1427. e=e
  1428. )
  1429. for x, y in {'&lt;': '<', '\n': '\r\n'}.items():
  1430. text = text.replace(x, y)
  1431. if len(text) == 0:
  1432. text = bot.get_message('admin', 'query_button', 'empty_file',
  1433. language=language)
  1434. with io.BytesIO(text.encode('utf-8')) as f:
  1435. f.name = file_name
  1436. return await bot.send_document(
  1437. chat_id=chat_id,
  1438. document=f,
  1439. caption=caption
  1440. )
  1441. async def send_part_of_text_file(bot, chat_id, file_path, caption=None,
  1442. file_name='File.txt', user_record=None,
  1443. update=None,
  1444. reversed_=True,
  1445. limit=None):
  1446. """Send `lines` lines of text file via `bot` in `chat_id`.
  1447. If `reversed`, read the file from last line.
  1448. TODO: do not load whole file in RAM. At the moment this is the easiest
  1449. way to allow `reversed` files, but it is inefficient and requires a lot
  1450. of memory.
  1451. """
  1452. if update is None:
  1453. update = dict()
  1454. try:
  1455. with open(file_path, 'r') as log_file:
  1456. lines = log_file.readlines()
  1457. if reversed_:
  1458. lines = lines[::-1]
  1459. if limit:
  1460. lines = lines[:limit]
  1461. with io.BytesIO(
  1462. ''.join(lines).encode('utf-8')
  1463. ) as document:
  1464. document.name = file_name
  1465. return await bot.send_document(
  1466. chat_id=chat_id,
  1467. document=document,
  1468. caption=caption
  1469. )
  1470. except Exception as e:
  1471. return e
  1472. def recursive_dictionary_update(one: dict, other: dict) -> dict:
  1473. """Extension of `dict.update()` method.
  1474. For each key of `other`, if key is not in `one` or the values differ, set
  1475. `one[key]` to `other[key]`. If the value is a dict, apply this function
  1476. recursively.
  1477. """
  1478. for key, val in other.items():
  1479. if key not in one:
  1480. one[key] = val
  1481. elif one[key] != val:
  1482. if isinstance(val, dict):
  1483. one[key] = recursive_dictionary_update(one[key], val)
  1484. else:
  1485. one[key] = val
  1486. return one
  1487. async def aio_subprocess_shell(command: str) -> Tuple[str, str]:
  1488. """Run `command` in a subprocess shell.
  1489. Await for the subprocess to end and return standard error and output.
  1490. On error, log errors.
  1491. """
  1492. stdout, stderr = None, None
  1493. try:
  1494. _subprocess = await asyncio.create_subprocess_shell(
  1495. command
  1496. )
  1497. stdout, stderr = await _subprocess.communicate()
  1498. if stdout:
  1499. stdout = stdout.decode().strip()
  1500. if stderr:
  1501. stderr = stderr.decode().strip()
  1502. except Exception as e:
  1503. logging.error(
  1504. "Exception {e}:\n{o}\n{er}".format(
  1505. e=e,
  1506. o=(stdout.decode().strip() if stdout else ''),
  1507. er=(stderr.decode().strip() if stderr else '')
  1508. )
  1509. )
  1510. return stdout, stderr