Queer European MD passionate about IT

utilities.py 48 KB

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