Queer European MD passionate about IT

utilities.py 41 KB

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