Queer European MD passionate about IT

utilities.py 42 KB

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