Queer European MD passionate about IT

utilities.py 48 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732
  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. asyncio.get_event_loop().run_until_complete(main())
  423. ```
  424. """
  425. async def wrapped_coroutine(*args2, bot=None, update=None, user_record=None, **kwargs2):
  426. # Update keyword arguments
  427. kwargs1.update(kwargs2)
  428. kwargs1['bot'] = bot
  429. kwargs1['update'] = update
  430. kwargs1['user_record'] = user_record
  431. # Pass only supported arguments
  432. kwargs = {
  433. name: argument
  434. for name, argument in kwargs1.items()
  435. if name in inspect.signature(
  436. coroutine
  437. ).parameters
  438. }
  439. return await coroutine(*args1, *args2, **kwargs)
  440. return wrapped_coroutine
  441. def forwarded(by=None):
  442. """Check that update is forwarded, optionally `by` someone in particular.
  443. Decorator: such decorated functions have effect only if update
  444. is forwarded from someone (you can specify `by` whom).
  445. """
  446. def is_forwarded_by(update):
  447. if 'forward_from' not in update:
  448. return False
  449. if by and update['forward_from']['id'] != by:
  450. return False
  451. return True
  452. def decorator(view_func):
  453. if asyncio.iscoroutinefunction(view_func):
  454. async def decorated(update):
  455. if is_forwarded_by(update):
  456. return await view_func(update)
  457. else:
  458. def decorated(update):
  459. if is_forwarded_by(update):
  460. return view_func(update)
  461. return decorated
  462. return decorator
  463. def chat_selective(chat_id=None):
  464. """Check that update comes from a chat, optionally having `chat_id`.
  465. Such decorated functions have effect only if update comes from
  466. a specific (if `chat_id` is given) or generic chat.
  467. """
  468. def check_function(update):
  469. if 'chat' not in update:
  470. return False
  471. if chat_id:
  472. if update['chat']['id'] != chat_id:
  473. return False
  474. return True
  475. def decorator(view_func):
  476. if asyncio.iscoroutinefunction(view_func):
  477. async def decorated(update):
  478. if check_function(update):
  479. return await view_func(update)
  480. else:
  481. def decorated(update):
  482. if check_function(update):
  483. return view_func(update)
  484. return decorated
  485. return decorator
  486. async def sleep_until(when: Union[datetime.datetime, datetime.timedelta]):
  487. """Sleep until now > `when`.
  488. `when` could be a datetime.datetime or a datetime.timedelta instance.
  489. """
  490. if not (
  491. isinstance(when, datetime.datetime)
  492. or isinstance(when, datetime.timedelta)
  493. ):
  494. raise TypeError(
  495. "sleep_until takes a datetime.datetime or datetime.timedelta "
  496. "object as argument!"
  497. )
  498. if isinstance(when, datetime.datetime):
  499. delta = when - datetime.datetime.now()
  500. elif isinstance(when, datetime.timedelta):
  501. delta = when
  502. else:
  503. delta = datetime.timedelta(seconds=1)
  504. if delta.days >= 0:
  505. await asyncio.sleep(
  506. delta.seconds
  507. )
  508. return
  509. async def wait_and_do(when, what, *args, **kwargs):
  510. """Sleep until `when`, then call `what` passing `args` and `kwargs`."""
  511. await sleep_until(when)
  512. return await what(*args, **kwargs)
  513. def get_csv_string(list_, delimiter=',', quotechar='"'):
  514. """Return a `delimiter`-delimited string of `list_` items.
  515. Wrap strings in `quotechar`s.
  516. """
  517. return delimiter.join(
  518. str(item) if type(item) is not str
  519. else '{q}{i}{q}'.format(
  520. i=item,
  521. q=quotechar
  522. )
  523. for item in list_
  524. )
  525. def case_accent_insensitive_sql(field):
  526. """Get a SQL string to perform a case- and accent-insensitive query.
  527. Given a `field`, return a part of SQL string necessary to perform
  528. a case- and accent-insensitive query.
  529. """
  530. replacements = [
  531. (' ', ''),
  532. ('à', 'a'),
  533. ('è', 'e'),
  534. ('é', 'e'),
  535. ('ì', 'i'),
  536. ('ò', 'o'),
  537. ('ù', 'u'),
  538. ]
  539. return "{r}LOWER({f}){w}".format(
  540. r="replace(".upper()*len(replacements),
  541. f=field,
  542. w=''.join(
  543. ", '{w[0]}', '{w[1]}')".format(w=w)
  544. for w in replacements
  545. )
  546. )
  547. # Italian definite articles.
  548. ARTICOLI = MyOD()
  549. ARTICOLI[1] = {
  550. 'ind': 'un',
  551. 'dets': 'il',
  552. 'detp': 'i',
  553. 'dess': 'l',
  554. 'desp': 'i'
  555. }
  556. ARTICOLI[2] = {
  557. 'ind': 'una',
  558. 'dets': 'la',
  559. 'detp': 'le',
  560. 'dess': 'lla',
  561. 'desp': 'lle'
  562. }
  563. ARTICOLI[3] = {
  564. 'ind': 'uno',
  565. 'dets': 'lo',
  566. 'detp': 'gli',
  567. 'dess': 'llo',
  568. 'desp': 'gli'
  569. }
  570. ARTICOLI[4] = {
  571. 'ind': 'un',
  572. 'dets': 'l\'',
  573. 'detp': 'gli',
  574. 'dess': 'll\'',
  575. 'desp': 'gli'
  576. }
  577. class Gettable:
  578. """Gettable objects can be retrieved from memory without being duplicated.
  579. Key is the primary key.
  580. Use class method get to instantiate (or retrieve) Gettable objects.
  581. Assign SubClass.instances = {}, otherwise Gettable.instances will
  582. contain SubClass objects.
  583. """
  584. instances = {}
  585. def __init__(self, *args, key=None, **kwargs):
  586. if key is None:
  587. key = args[0]
  588. if key not in self.__class__.instances:
  589. self.__class__.instances[key] = self
  590. @classmethod
  591. def get(cls, *args, key=None, **kwargs):
  592. """Instantiate and/or retrieve Gettable object.
  593. SubClass.instances is searched if exists.
  594. Gettable.instances is searched otherwise.
  595. """
  596. if key is None:
  597. key = args[0]
  598. else:
  599. kwargs['key'] = key
  600. if key not in cls.instances:
  601. cls.instances[key] = cls(*args, **kwargs)
  602. return cls.instances[key]
  603. class Confirmable:
  604. """Confirmable objects are provided with a confirm instance method.
  605. It evaluates True if it was called within self._confirm_timedelta,
  606. False otherwise.
  607. When it returns True, timer is reset.
  608. """
  609. CONFIRM_TIMEDELTA = datetime.timedelta(seconds=10)
  610. def __init__(self, confirm_timedelta: Union[datetime.timedelta, int] = None):
  611. """Instantiate Confirmable instance.
  612. If `confirm_timedelta` is not passed,
  613. `self.__class__.CONFIRM_TIMEDELTA` is used as default.
  614. """
  615. if confirm_timedelta is None:
  616. confirm_timedelta = self.__class__.CONFIRM_TIMEDELTA
  617. elif type(confirm_timedelta) is int:
  618. confirm_timedelta = datetime.timedelta(seconds=confirm_timedelta)
  619. self._confirm_timedelta = None
  620. self.set_confirm_timedelta(confirm_timedelta)
  621. self._confirm_datetimes = {}
  622. @property
  623. def confirm_timedelta(self):
  624. """Maximum timedelta between two calls of `confirm`."""
  625. return self._confirm_timedelta
  626. def confirm_datetime(self, who='unique'):
  627. """Get datetime of `who`'s last confirm.
  628. If `who` never called `confirm`, fake an expired call.
  629. """
  630. if who not in self._confirm_datetimes:
  631. self._confirm_datetimes[who] = (
  632. datetime.datetime.now()
  633. - 2*self.confirm_timedelta
  634. )
  635. return self._confirm_datetimes[who]
  636. def set_confirm_timedelta(self, confirm_timedelta):
  637. """Change self._confirm_timedelta."""
  638. if type(confirm_timedelta) is int:
  639. confirm_timedelta = datetime.timedelta(
  640. seconds=confirm_timedelta
  641. )
  642. assert isinstance(
  643. confirm_timedelta, datetime.timedelta
  644. ), "confirm_timedelta must be a datetime.timedelta instance!"
  645. self._confirm_timedelta = confirm_timedelta
  646. def confirm(self, who='unique'):
  647. """Return True if `confirm` was called by `who` recetly enough."""
  648. now = datetime.datetime.now()
  649. if now >= self.confirm_datetime(who) + self.confirm_timedelta:
  650. self._confirm_datetimes[who] = now
  651. return False
  652. self._confirm_datetimes[who] = now - 2*self.confirm_timedelta
  653. return True
  654. class HasBot:
  655. """Objects having a Bot subclass object as `.bot` attribute.
  656. HasBot objects have a .bot and .db properties for faster access.
  657. """
  658. _bot = None
  659. @property
  660. def bot(self):
  661. """Class bot."""
  662. return self.__class__._bot
  663. @property
  664. def db(self):
  665. """Class bot db."""
  666. return self.bot.db
  667. @classmethod
  668. def set_bot(cls, bot):
  669. """Change class bot."""
  670. cls._bot = bot
  671. class CachedPage(Gettable):
  672. """Cache a web page and return it during CACHE_TIME, otherwise refresh.
  673. Usage:
  674. cached_page = CachedPage.get(
  675. 'https://www.google.com',
  676. datetime.timedelta(seconds=30),
  677. **kwargs
  678. )
  679. page = await cached_page.get_page()
  680. """
  681. CACHE_TIME = datetime.timedelta(minutes=5)
  682. instances = {}
  683. def __init__(self, url, cache_time=None, **async_get_kwargs):
  684. """Instantiate CachedPage object.
  685. `url`: the URL to be cached
  686. `cache_time`: timedelta from last_update during which
  687. page will be cached
  688. `**kwargs` will be passed to async_get function
  689. """
  690. self._url = url
  691. if type(cache_time) is int:
  692. cache_time = datetime.timedelta(seconds=cache_time)
  693. if cache_time is None:
  694. cache_time = self.__class__.CACHE_TIME
  695. assert type(cache_time) is datetime.timedelta, (
  696. "Cache time must be a datetime.timedelta object!"
  697. )
  698. self._cache_time = cache_time
  699. self._page = None
  700. self._last_update = datetime.datetime.now() - self.cache_time
  701. self._async_get_kwargs = async_get_kwargs
  702. super().__init__(key=url)
  703. @property
  704. def url(self):
  705. """Get cached page url."""
  706. return self._url
  707. @property
  708. def cache_time(self):
  709. """Get cache time."""
  710. return self._cache_time
  711. @property
  712. def page(self):
  713. """Get webpage."""
  714. return self._page
  715. @property
  716. def last_update(self):
  717. """Get datetime of last update."""
  718. return self._last_update
  719. @property
  720. def async_get_kwargs(self):
  721. """Get async get request keyword arguments."""
  722. return self._async_get_kwargs
  723. @property
  724. def is_old(self):
  725. """Evaluate True if `chache_time` has passed since last update."""
  726. return datetime.datetime.now() > self.last_update + self.cache_time
  727. async def refresh(self):
  728. """Update cached web page."""
  729. try:
  730. self._page = await async_get(self.url, **self.async_get_kwargs)
  731. self._last_update = datetime.datetime.now()
  732. return 0
  733. except Exception as e:
  734. self._page = None
  735. logging.error(
  736. '{e}'.format(
  737. e=e
  738. ),
  739. exc_info=False
  740. ) # Set exc_info=True to debug
  741. return 1
  742. async def get_page(self):
  743. """Refresh if necessary and return web page."""
  744. if self.is_old:
  745. await self.refresh()
  746. return self.page
  747. class Confirmator(Gettable, Confirmable):
  748. """Gettable Confirmable object."""
  749. instances = {}
  750. def __init__(self, key, *args, confirm_timedelta=None):
  751. """Call Confirmable.__init__ passing `confirm_timedelta`."""
  752. Confirmable.__init__(self, confirm_timedelta)
  753. Gettable.__init__(self, key=key, *args)
  754. def get_cleaned_text(update, bot=None, replace=None, strip='/ @'):
  755. """Clean `update`['text'] and return it.
  756. Strip `bot`.name and items to be `replace`d from the beginning of text.
  757. Strip `strip` characters from both ends.
  758. """
  759. if replace is None:
  760. replace = []
  761. if bot is not None:
  762. replace.append(
  763. '@{.name}'.format(
  764. bot
  765. )
  766. )
  767. text = update['text'].strip(strip)
  768. # Replace longer strings first
  769. for s in sorted(replace, key=len, reverse=True):
  770. while s and text.lower().startswith(s.lower()):
  771. text = text[len(s):]
  772. return text.strip(strip)
  773. def get_user(record, link_profile=True):
  774. """Get an HTML Telegram tag for user `record`."""
  775. if not record:
  776. return
  777. from_ = {key: val for key, val in record.items()}
  778. result = '{name}'
  779. if 'telegram_id' in from_:
  780. from_['id'] = from_['telegram_id']
  781. if (
  782. 'id' in from_
  783. and from_['id'] is not None
  784. and link_profile
  785. ):
  786. result = f"""<a href="tg://user?id={from_['id']}">{{name}}</a>"""
  787. if 'username' in from_ and from_['username']:
  788. result = result.format(
  789. name=from_['username']
  790. )
  791. elif (
  792. 'first_name' in from_
  793. and from_['first_name']
  794. and 'last_name' in from_
  795. and from_['last_name']
  796. ):
  797. result = result.format(
  798. name=f"{from_['first_name']} {from_['last_name']}"
  799. )
  800. elif 'first_name' in from_ and from_['first_name']:
  801. result = result.format(
  802. name=from_['first_name']
  803. )
  804. elif 'last_name' in from_ and from_['last_name']:
  805. result = result.format(
  806. name=from_['last_name']
  807. )
  808. else:
  809. result = result.format(
  810. name="Utente anonimo"
  811. )
  812. return result
  813. def datetime_from_utc_to_local(utc_datetime):
  814. """Convert `utc_datetime` to local datetime."""
  815. now_timestamp = time.time()
  816. offset = (
  817. datetime.datetime.fromtimestamp(now_timestamp)
  818. - datetime.datetime.utcfromtimestamp(now_timestamp)
  819. )
  820. return utc_datetime + offset
  821. # TIME_SYMBOLS from more specific to less specific (avoid false positives!)
  822. TIME_SYMBOLS = MyOD()
  823. TIME_SYMBOLS["'"] = 'minutes'
  824. TIME_SYMBOLS["settimana"] = 'weeks'
  825. TIME_SYMBOLS["settimane"] = 'weeks'
  826. TIME_SYMBOLS["weeks"] = 'weeks'
  827. TIME_SYMBOLS["week"] = 'weeks'
  828. TIME_SYMBOLS["giorno"] = 'days'
  829. TIME_SYMBOLS["giorni"] = 'days'
  830. TIME_SYMBOLS["secondi"] = 'seconds'
  831. TIME_SYMBOLS["seconds"] = 'seconds'
  832. TIME_SYMBOLS["secondo"] = 'seconds'
  833. TIME_SYMBOLS["minuti"] = 'minutes'
  834. TIME_SYMBOLS["minuto"] = 'minutes'
  835. TIME_SYMBOLS["minute"] = 'minutes'
  836. TIME_SYMBOLS["minutes"] = 'minutes'
  837. TIME_SYMBOLS["day"] = 'days'
  838. TIME_SYMBOLS["days"] = 'days'
  839. TIME_SYMBOLS["ore"] = 'hours'
  840. TIME_SYMBOLS["ora"] = 'hours'
  841. TIME_SYMBOLS["sec"] = 'seconds'
  842. TIME_SYMBOLS["min"] = 'minutes'
  843. TIME_SYMBOLS["m"] = 'minutes'
  844. TIME_SYMBOLS["h"] = 'hours'
  845. TIME_SYMBOLS["d"] = 'days'
  846. TIME_SYMBOLS["s"] = 'seconds'
  847. def _interval_parser(text, result):
  848. text = text.lower()
  849. succeeded = False
  850. if result is None:
  851. result = []
  852. if len(result) == 0 or result[-1]['ok']:
  853. text_part = ''
  854. _text = text # I need to iterate through _text modifying text
  855. for char in _text:
  856. if not char.isnumeric():
  857. break
  858. else:
  859. text_part += char
  860. text = text[1:]
  861. if text_part.isnumeric():
  862. result.append(
  863. dict(
  864. unit=None,
  865. value=int(text_part),
  866. ok=False
  867. )
  868. )
  869. succeeded = True, True
  870. if text:
  871. dummy, result = _interval_parser(text, result)
  872. elif len(result) > 0 and not result[-1]['ok']:
  873. text_part = ''
  874. _text = text # I need to iterate through _text modifying text
  875. for char in _text:
  876. if char.isnumeric():
  877. break
  878. else:
  879. text_part += char
  880. text = text[1:]
  881. for time_symbol, unit in TIME_SYMBOLS.items():
  882. if time_symbol in text_part:
  883. result[-1]['unit'] = unit
  884. result[-1]['ok'] = True
  885. succeeded = True, True
  886. break
  887. else:
  888. result.pop()
  889. if text:
  890. dummy, result = _interval_parser(text, result)
  891. return succeeded, result
  892. def _date_parser(text, result):
  893. succeeded = False
  894. if 3 <= len(text) <= 10 and text.count('/') >= 1:
  895. if 3 <= len(text) <= 5 and text.count('/') == 1:
  896. text += '/{:%y}'.format(datetime.datetime.now())
  897. if 6 <= len(text) <= 10 and text.count('/') == 2:
  898. day, month, year = [
  899. int(n) for n in [
  900. ''.join(char)
  901. for char in text.split('/')
  902. if char.isnumeric()
  903. ]
  904. ]
  905. if year < 100:
  906. year += 2000
  907. if result is None:
  908. result = []
  909. result += [
  910. dict(
  911. unit='day',
  912. value=day,
  913. ok=True
  914. ),
  915. dict(
  916. unit='month',
  917. value=month,
  918. ok=True
  919. ),
  920. dict(
  921. unit='year',
  922. value=year,
  923. ok=True
  924. )
  925. ]
  926. succeeded = True, True
  927. return succeeded, result
  928. def _time_parser(text, result):
  929. succeeded = False
  930. if (1 <= len(text) <= 8) and any(char.isnumeric() for char in text):
  931. text = ''.join(
  932. ':' if char == '.' else char
  933. for char in text
  934. if char.isnumeric() or char in (':', '.')
  935. )
  936. if len(text) <= 2:
  937. text = '{:02d}:00:00'.format(int(text))
  938. elif len(text) == 4 and ':' not in text:
  939. text = '{:02d}:{:02d}:00'.format(
  940. *[int(x) for x in (text[:2], text[2:])]
  941. )
  942. elif text.count(':') == 1:
  943. text = '{:02d}:{:02d}:00'.format(
  944. *[int(x) for x in text.split(':')]
  945. )
  946. if text.count(':') == 2:
  947. hour, minute, second = (int(x) for x in text.split(':'))
  948. if (
  949. 0 <= hour <= 24
  950. and 0 <= minute <= 60
  951. and 0 <= second <= 60
  952. ):
  953. if result is None:
  954. result = []
  955. result += [
  956. dict(
  957. unit='hour',
  958. value=hour,
  959. ok=True
  960. ),
  961. dict(
  962. unit='minute',
  963. value=minute,
  964. ok=True
  965. ),
  966. dict(
  967. unit='second',
  968. value=second,
  969. ok=True
  970. )
  971. ]
  972. succeeded = True
  973. return succeeded, result
  974. WEEKDAY_NAMES_ITA = ["Lunedì", "Martedì", "Mercoledì", "Giovedì",
  975. "Venerdì", "Sabato", "Domenica"]
  976. WEEKDAY_NAMES_ENG = ["Monday", "Tuesday", "Wednesday", "Thursday",
  977. "Friday", "Saturday", "Sunday"]
  978. def _period_parser(text, result):
  979. if text.title() in WEEKDAY_NAMES_ITA + WEEKDAY_NAMES_ENG:
  980. day_code = (WEEKDAY_NAMES_ITA + WEEKDAY_NAMES_ENG).index(text.title())
  981. if day_code > 6:
  982. day_code -= 7
  983. today = datetime.date.today()
  984. days = 1
  985. while (today + datetime.timedelta(days=days)).weekday() != day_code:
  986. days += 1
  987. if result is None:
  988. result = []
  989. result.append(
  990. dict(
  991. unit='days',
  992. value=days,
  993. ok=True,
  994. weekly=True
  995. )
  996. )
  997. succeeded = True
  998. else:
  999. succeeded, result = _interval_parser(text, result)
  1000. return succeeded, result
  1001. TIME_WORDS = {
  1002. 'tra': dict(
  1003. parser=_interval_parser,
  1004. recurring=False,
  1005. type_='delta'
  1006. ),
  1007. 'in': dict(
  1008. parser=_interval_parser,
  1009. recurring=False,
  1010. type_='delta'
  1011. ),
  1012. 'at': dict(
  1013. parser=_time_parser,
  1014. recurring=False,
  1015. type_='set'
  1016. ),
  1017. 'on': dict(
  1018. parser=_date_parser,
  1019. recurring=False,
  1020. type_='set'
  1021. ),
  1022. 'alle': dict(
  1023. parser=_time_parser,
  1024. recurring=False,
  1025. type_='set'
  1026. ),
  1027. 'il': dict(
  1028. parser=_date_parser,
  1029. recurring=False,
  1030. type_='set'
  1031. ),
  1032. 'every': dict(
  1033. parser=_period_parser,
  1034. recurring=True,
  1035. type_='delta'
  1036. ),
  1037. 'ogni': dict(
  1038. parser=_period_parser,
  1039. recurring=True,
  1040. type_='delta'
  1041. ),
  1042. }
  1043. def parse_datetime_interval_string(text):
  1044. """Parse `text` and return text, datetime and timedelta."""
  1045. parsers = []
  1046. result_text, result_datetime, result_timedelta = [], None, None
  1047. is_quoted_text = False
  1048. # Replace multiple spaces with single space character
  1049. text = re.sub(r'\s\s+', ' ', text)
  1050. for word in text.split(' '):
  1051. if word.count('"') % 2:
  1052. is_quoted_text = not is_quoted_text
  1053. if is_quoted_text or '"' in word:
  1054. result_text.append(
  1055. word.replace('"', '') if 'href=' not in word else word
  1056. )
  1057. continue
  1058. result_text.append(word)
  1059. word = word.lower()
  1060. succeeded = False
  1061. if len(parsers) > 0:
  1062. succeeded, result = parsers[-1]['parser'](
  1063. word,
  1064. parsers[-1]['result']
  1065. )
  1066. if succeeded:
  1067. parsers[-1]['result'] = result
  1068. if not succeeded and word in TIME_WORDS:
  1069. parsers.append(
  1070. dict(
  1071. result=None,
  1072. parser=TIME_WORDS[word]['parser'],
  1073. recurring=TIME_WORDS[word]['recurring'],
  1074. type_=TIME_WORDS[word]['type_']
  1075. )
  1076. )
  1077. if succeeded:
  1078. result_text.pop()
  1079. if len(result_text) > 0 and result_text[-1].lower() in TIME_WORDS:
  1080. result_text.pop()
  1081. result_text = escape_html_chars(
  1082. ' '.join(result_text)
  1083. )
  1084. parsers = list(
  1085. filter(
  1086. lambda x: 'result' in x and x['result'],
  1087. parsers
  1088. )
  1089. )
  1090. recurring_event = False
  1091. weekly = False
  1092. _timedelta = datetime.timedelta()
  1093. _datetime = None
  1094. _now = datetime.datetime.now()
  1095. for parser in parsers:
  1096. if parser['recurring']:
  1097. recurring_event = True
  1098. type_ = parser['type_']
  1099. for result in parser['result']:
  1100. if not isinstance(result, dict) or not result['ok']:
  1101. continue
  1102. if recurring_event and 'weekly' in result and result['weekly']:
  1103. weekly = True
  1104. if type_ == 'set':
  1105. if _datetime is None:
  1106. _datetime = _now
  1107. _datetime = _datetime.replace(
  1108. **{
  1109. result['unit']: result['value']
  1110. }
  1111. )
  1112. elif type_ == 'delta':
  1113. _timedelta += datetime.timedelta(
  1114. **{
  1115. result['unit']: result['value']
  1116. }
  1117. )
  1118. if _datetime:
  1119. result_datetime = _datetime
  1120. if _timedelta:
  1121. if result_datetime is None:
  1122. result_datetime = _now
  1123. if recurring_event:
  1124. result_timedelta = _timedelta
  1125. if weekly:
  1126. result_timedelta = datetime.timedelta(days=7)
  1127. else:
  1128. result_datetime += _timedelta
  1129. while result_datetime and result_datetime < datetime.datetime.now():
  1130. result_datetime += (
  1131. result_timedelta
  1132. if result_timedelta
  1133. else datetime.timedelta(days=1)
  1134. )
  1135. return result_text, result_datetime, result_timedelta
  1136. DAY_GAPS = {
  1137. -1: 'ieri',
  1138. -2: 'avantieri',
  1139. 0: 'oggi',
  1140. 1: 'domani',
  1141. 2: 'dopodomani'
  1142. }
  1143. MONTH_NAMES_ITA = MyOD()
  1144. MONTH_NAMES_ITA[1] = "gennaio"
  1145. MONTH_NAMES_ITA[2] = "febbraio"
  1146. MONTH_NAMES_ITA[3] = "marzo"
  1147. MONTH_NAMES_ITA[4] = "aprile"
  1148. MONTH_NAMES_ITA[5] = "maggio"
  1149. MONTH_NAMES_ITA[6] = "giugno"
  1150. MONTH_NAMES_ITA[7] = "luglio"
  1151. MONTH_NAMES_ITA[8] = "agosto"
  1152. MONTH_NAMES_ITA[9] = "settembre"
  1153. MONTH_NAMES_ITA[10] = "ottobre"
  1154. MONTH_NAMES_ITA[11] = "novembre"
  1155. MONTH_NAMES_ITA[12] = "dicembre"
  1156. def beautytd(td):
  1157. """Format properly timedeltas."""
  1158. result = ''
  1159. if type(td) is int:
  1160. td = datetime.timedelta(seconds=td)
  1161. assert isinstance(
  1162. td,
  1163. datetime.timedelta
  1164. ), "td must be a datetime.timedelta object!"
  1165. mtd = datetime.timedelta
  1166. if td < mtd(minutes=1):
  1167. result = "{:.0f} secondi".format(
  1168. td.total_seconds()
  1169. )
  1170. elif td < mtd(minutes=60):
  1171. result = "{:.0f} min{}".format(
  1172. td.total_seconds()//60,
  1173. (
  1174. " {:.0f} s".format(
  1175. td.total_seconds() % 60
  1176. )
  1177. ) if td.total_seconds() % 60 else ''
  1178. )
  1179. elif td < mtd(days=1):
  1180. result = "{:.0f} h{}".format(
  1181. td.total_seconds()//3600,
  1182. (
  1183. " {:.0f} min".format(
  1184. (td.total_seconds() % 3600) // 60
  1185. )
  1186. ) if td.total_seconds() % 3600 else ''
  1187. )
  1188. elif td < mtd(days=30):
  1189. result = "{} giorni{}".format(
  1190. td.days,
  1191. (
  1192. " {:.0f} h".format(
  1193. td.total_seconds() % (3600*24) // 3600
  1194. )
  1195. ) if td.total_seconds() % (3600*24) else ''
  1196. )
  1197. return result
  1198. def beautydt(dt):
  1199. """Format a datetime in a smart way."""
  1200. if type(dt) is str:
  1201. dt = str_to_datetime(dt)
  1202. assert isinstance(
  1203. dt,
  1204. datetime.datetime
  1205. ), "dt must be a datetime.datetime object!"
  1206. now = datetime.datetime.now()
  1207. gap = dt - now
  1208. gap_days = (dt.date() - now.date()).days
  1209. result = "alle {dt:%H:%M}".format(
  1210. dt=dt
  1211. )
  1212. if abs(gap) < datetime.timedelta(minutes=30):
  1213. result += ":{dt:%S}".format(dt=dt)
  1214. if -2 <= gap_days <= 2:
  1215. result += " di {dg}".format(
  1216. dg=DAY_GAPS[gap_days]
  1217. )
  1218. elif gap.days not in (-1, 0):
  1219. result += " del {d}{m}".format(
  1220. d=dt.day,
  1221. m=(
  1222. "" if now.year == dt.year and now.month == dt.month
  1223. else " {m}{y}".format(
  1224. m=MONTH_NAMES_ITA[dt.month].title(),
  1225. y="" if now.year == dt.year
  1226. else " {}".format(dt.year)
  1227. )
  1228. )
  1229. )
  1230. return result
  1231. HTML_SYMBOLS = MyOD()
  1232. HTML_SYMBOLS["&"] = "&amp;"
  1233. HTML_SYMBOLS["<"] = "&lt;"
  1234. HTML_SYMBOLS[">"] = "&gt;"
  1235. HTML_SYMBOLS["\""] = "&quot;"
  1236. HTML_SYMBOLS["&lt;b&gt;"] = "<b>"
  1237. HTML_SYMBOLS["&lt;/b&gt;"] = "</b>"
  1238. HTML_SYMBOLS["&lt;i&gt;"] = "<i>"
  1239. HTML_SYMBOLS["&lt;/i&gt;"] = "</i>"
  1240. HTML_SYMBOLS["&lt;code&gt;"] = "<code>"
  1241. HTML_SYMBOLS["&lt;/code&gt;"] = "</code>"
  1242. HTML_SYMBOLS["&lt;pre&gt;"] = "<pre>"
  1243. HTML_SYMBOLS["&lt;/pre&gt;"] = "</pre>"
  1244. HTML_SYMBOLS["&lt;a href=&quot;"] = "<a href=\""
  1245. HTML_SYMBOLS["&quot;&gt;"] = "\">"
  1246. HTML_SYMBOLS["&lt;/a&gt;"] = "</a>"
  1247. HTML_TAGS = [
  1248. None, "<b>", "</b>",
  1249. None, "<i>", "</i>",
  1250. None, "<code>", "</code>",
  1251. None, "<pre>", "</pre>",
  1252. None, "<a href=\"", "\">", "</a>",
  1253. None
  1254. ]
  1255. def remove_html_tags(text):
  1256. """Remove HTML tags from `text`."""
  1257. for tag in HTML_TAGS:
  1258. if tag is None:
  1259. continue
  1260. text = text.replace(tag, '')
  1261. return text
  1262. def escape_html_chars(text):
  1263. """Escape HTML chars if not part of a tag."""
  1264. for s, r in HTML_SYMBOLS.items():
  1265. text = text.replace(s, r)
  1266. copy = text
  1267. expected_tag = None
  1268. while copy:
  1269. min_ = min(
  1270. (
  1271. dict(
  1272. position=copy.find(tag) if tag in copy else len(copy),
  1273. tag=tag
  1274. )
  1275. for tag in HTML_TAGS
  1276. if tag
  1277. ),
  1278. key=lambda x: x['position'],
  1279. default=0
  1280. )
  1281. if min_['position'] == len(copy):
  1282. break
  1283. if expected_tag and min_['tag'] != expected_tag:
  1284. return text.replace('<', '_').replace('>', '_')
  1285. expected_tag = HTML_TAGS[HTML_TAGS.index(min_['tag'])+1]
  1286. copy = extract(copy, min_['tag'])
  1287. return text
  1288. def accents_to_jolly(text, lower=True):
  1289. """Replace letters with Italian accents with SQL jolly character."""
  1290. to_be_replaced = ('à', 'è', 'é', 'ì', 'ò', 'ù')
  1291. if lower:
  1292. text = text.lower()
  1293. else:
  1294. to_be_replaced += tuple(s.upper() for s in to_be_replaced)
  1295. for s in to_be_replaced:
  1296. text = text.replace(s, '_')
  1297. return text.replace("'", "''")
  1298. def get_secure_key(allowed_chars=None, length=6):
  1299. """Get a randomly-generate secure key.
  1300. You can specify a set of `allowed_chars` and a `length`.
  1301. """
  1302. if allowed_chars is None:
  1303. allowed_chars = string.ascii_uppercase + string.digits
  1304. return ''.join(
  1305. random.SystemRandom().choice(
  1306. allowed_chars
  1307. )
  1308. for _ in range(length)
  1309. )
  1310. def round_to_minute(datetime_):
  1311. """Round `datetime_` to closest minute."""
  1312. return (
  1313. datetime_ + datetime.timedelta(seconds=30)
  1314. ).replace(second=0, microsecond=0)
  1315. def get_line_by_content(text, key):
  1316. """Get line of `text` containing `key`."""
  1317. for line in text.split('\n'):
  1318. if key in line:
  1319. return line
  1320. return
  1321. def str_to_int(string_):
  1322. """Cast str to int, ignoring non-numeric characters."""
  1323. string_ = ''.join(
  1324. char
  1325. for char in string_
  1326. if char.isnumeric()
  1327. )
  1328. if len(string_) == 0:
  1329. string_ = '0'
  1330. return int(string_)
  1331. def starting_with_or_similar_to(a, b):
  1332. """Return similarity between two strings.
  1333. Least similar equals 0, most similar evaluates 1.
  1334. If similarity is less than 0.75, return 1 if one string starts with
  1335. the other and return 0.5 if one string is contained in the other.
  1336. """
  1337. a = a.lower()
  1338. b = b.lower()
  1339. similarity = SequenceMatcher(None, a, b).ratio()
  1340. if similarity < 0.75:
  1341. if b.startswith(a) or a.startswith(b):
  1342. return 1
  1343. if b in a or a in b:
  1344. return 0.5
  1345. return similarity
  1346. def pick_most_similar_from_list(list_, item):
  1347. """Return element from `list_` which is most similar to `item`.
  1348. Similarity is evaluated using `starting_with_or_similar_to`.
  1349. """
  1350. return max(
  1351. list_,
  1352. key=lambda element: starting_with_or_similar_to(
  1353. item,
  1354. element
  1355. )
  1356. )
  1357. def run_aiohttp_server(app, *args, **kwargs):
  1358. """Run an aiohttp web app, with its positional and keyword arguments.
  1359. Useful to run apps in dedicated threads.
  1360. """
  1361. loop = asyncio.new_event_loop()
  1362. asyncio.set_event_loop(loop)
  1363. aiohttp.web.run_app(app, *args, **kwargs)
  1364. def custom_join(_list, joiner, final=None):
  1365. """Join elements of `_list` using `joiner` (`final` as last joiner)."""
  1366. _list = list(map(str, _list))
  1367. if final is None:
  1368. final = joiner
  1369. if len(_list) == 0:
  1370. return ''
  1371. if len(_list) == 1:
  1372. return _list[0]
  1373. if len(_list) == 2:
  1374. return final.join(_list)
  1375. return joiner.join(_list[:-1]) + final + _list[-1]
  1376. def make_inline_query_answer(answer):
  1377. """Return an article-type answer to inline query.
  1378. Takes either a string or a dictionary and returns a list.
  1379. """
  1380. if type(answer) is str:
  1381. answer = dict(
  1382. type='article',
  1383. id=0,
  1384. title=remove_html_tags(answer),
  1385. input_message_content=dict(
  1386. message_text=answer,
  1387. parse_mode='HTML'
  1388. )
  1389. )
  1390. if type(answer) is dict:
  1391. answer = [answer]
  1392. return answer
  1393. # noinspection PyUnusedLocal
  1394. async def dummy_coroutine(*args, **kwargs):
  1395. """Accept everything as argument and do nothing."""
  1396. return
  1397. async def send_csv_file(bot, chat_id: int, query: str, caption: str = None,
  1398. file_name: str = 'File.csv', language: str = None,
  1399. 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. if language is None:
  1407. language = bot.get_language(update=update,
  1408. user_record=user_record)
  1409. try:
  1410. with bot.db as db:
  1411. record = db.query(
  1412. query
  1413. )
  1414. header_line = []
  1415. body_lines = []
  1416. for row in record:
  1417. if not header_line:
  1418. header_line.append(get_csv_string(row.keys()))
  1419. body_lines.append(get_csv_string(row.values()))
  1420. text = '\n'.join(header_line + body_lines)
  1421. except Exception as e:
  1422. text = "{message}\n{e}".format(
  1423. message=bot.get_message('admin', 'query_button', 'error',
  1424. language=language),
  1425. e=e
  1426. )
  1427. for x, y in {'&lt;': '<', '\n': '\r\n'}.items():
  1428. text = text.replace(x, y)
  1429. if len(text) == 0:
  1430. text = bot.get_message('admin', 'query_button', 'empty_file',
  1431. language=language)
  1432. with io.BytesIO(text.encode('utf-8')) as f:
  1433. f.name = file_name
  1434. return await bot.send_document(
  1435. chat_id=chat_id,
  1436. document=f,
  1437. caption=caption
  1438. )
  1439. async def send_part_of_text_file(bot, chat_id, file_path, caption=None,
  1440. file_name='File.txt', user_record=None,
  1441. update=None,
  1442. reversed_=True,
  1443. limit=None):
  1444. """Send `lines` lines of text file via `bot` in `chat_id`.
  1445. If `reversed`, read the file from last line.
  1446. TODO: do not load whole file in RAM. At the moment this is the easiest
  1447. way to allow `reversed` files, but it is inefficient and requires a lot
  1448. of memory.
  1449. """
  1450. if update is None:
  1451. update = dict()
  1452. try:
  1453. with open(file_path, 'r') as log_file:
  1454. lines = log_file.readlines()
  1455. if reversed_:
  1456. lines = lines[::-1]
  1457. if limit:
  1458. lines = lines[:limit]
  1459. with io.BytesIO(
  1460. ''.join(lines).encode('utf-8')
  1461. ) as document:
  1462. document.name = file_name
  1463. return await bot.send_document(
  1464. chat_id=chat_id,
  1465. document=document,
  1466. caption=caption
  1467. )
  1468. except Exception as e:
  1469. return e
  1470. def recursive_dictionary_update(one: dict, other: dict) -> dict:
  1471. """Extension of `dict.update()` method.
  1472. For each key of `other`, if key is not in `one` or the values differ, set
  1473. `one[key]` to `other[key]`. If the value is a dict, apply this function
  1474. recursively.
  1475. """
  1476. for key, val in other.items():
  1477. if key not in one:
  1478. one[key] = val
  1479. elif one[key] != val:
  1480. if isinstance(val, dict):
  1481. one[key] = recursive_dictionary_update(one[key], val)
  1482. else:
  1483. one[key] = val
  1484. return one
  1485. async def aio_subprocess_shell(command: str) -> Tuple[str, str]:
  1486. """Run `command` in a subprocess shell.
  1487. Await for the subprocess to end and return standard error and output.
  1488. On error, log errors.
  1489. """
  1490. stdout, stderr = None, None
  1491. try:
  1492. _subprocess = await asyncio.create_subprocess_shell(
  1493. command
  1494. )
  1495. stdout, stderr = await _subprocess.communicate()
  1496. if stdout:
  1497. stdout = stdout.decode().strip()
  1498. if stderr:
  1499. stderr = stderr.decode().strip()
  1500. except Exception as e:
  1501. logging.error(
  1502. "Exception {e}:\n{o}\n{er}".format(
  1503. e=e,
  1504. o=(stdout.decode().strip() if stdout else ''),
  1505. er=(stderr.decode().strip() if stderr else '')
  1506. )
  1507. )
  1508. return stdout, stderr