Queer European MD passionate about IT

utilities.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986
  1. # Standard library modules
  2. import aiohttp
  3. import asyncio
  4. import collections
  5. import csv
  6. import datetime
  7. import io
  8. import json
  9. import logging
  10. import os
  11. import random
  12. import string
  13. import sys
  14. import time
  15. # Third party modules
  16. from bs4 import BeautifulSoup
  17. import telepot, telepot.aio
  18. def sumif(it, cond):
  19. return sum(filter(cond, it))
  20. def markdown_check(text, symbols):
  21. #Dato un testo text e una lista symbols di simboli, restituisce vero solo se TUTTI i simboli della lista sono presenti in numero pari di volte nel testo.
  22. for s in symbols:
  23. if (len(text.replace(s,"")) - len(text))%2 != 0:
  24. return False
  25. return True
  26. def shorten_text(text, limit, symbol="[...]"):
  27. """Return a given text truncated at limit if longer than limit. On truncation, add symbol.
  28. """
  29. assert type(text) is str and type(symbol) is str and type(limit) is int
  30. if len(text) <= limit:
  31. return text
  32. return text[:limit-len(symbol)] + symbol
  33. def extract(text, starter=None, ender=None):
  34. """Return string in text between starter and ender.
  35. If starter is None, truncates at ender.
  36. """
  37. if starter and starter in text:
  38. text = text.partition(starter)[2]
  39. if ender:
  40. return text.partition(ender)[0]
  41. return text
  42. def mkbtn(x, y):
  43. if len(y) > 60:#If callback_data exceeeds 60 characters (max is 64), it gets trunkated at the last comma
  44. y = y[:61]
  45. y = y[:- y[::-1].find(",")-1]
  46. return {'text': x, 'callback_data': y}
  47. def make_lines_of_buttons(btns, row_len=1):
  48. return [btns[i:i + row_len] for i in range(0, len(btns), row_len)]
  49. def make_inline_keyboard(btns, row_len=1):
  50. return dict(inline_keyboard=make_lines_of_buttons(btns, row_len))
  51. async def async_get(url, mode='json', **kwargs):
  52. if 'mode' in kwargs:
  53. mode = kwargs['mode']
  54. del kwargs['mode']
  55. return await async_request(url, type='get', mode=mode, **kwargs)
  56. async def async_post(url, mode='html', **kwargs):
  57. return await async_request(url, type='post', mode=mode, **kwargs)
  58. async def async_request(url, type='get', mode='json', **kwargs):
  59. try:
  60. async with aiohttp.ClientSession() as s:
  61. async with (s.get(url, timeout=30) if type=='get' else s.post(url, timeout=30, data=kwargs)) as r:
  62. result = await r.read()
  63. except Exception as e:
  64. logging.error('Error making async request to {}:\n{}'.format(url, e), exc_info=False) # Set exc_info=True to debug
  65. return e
  66. if mode=='json':
  67. if not result:
  68. return {}
  69. return json.loads(result.decode('utf-8'))
  70. if mode=='html':
  71. return BeautifulSoup(result.decode('utf-8'), "html.parser")
  72. if mode=='string':
  73. return result.decode('utf-8')
  74. return result
  75. def json_read(file, default={}):
  76. if not os.path.isfile(file):
  77. return default
  78. with open(file, "r", encoding='utf-8') as f:
  79. return json.load(f)
  80. def json_write(what, file):
  81. with open(file, "w") as f:
  82. return json.dump(what, f, indent=4)
  83. def csv_read(file_, default=[]):
  84. if not os.path.isfile(file_):
  85. return default
  86. result = []
  87. keys = []
  88. with open(file_, newline='', encoding='utf8') as csv_file:
  89. csv_reader = csv.reader(csv_file, delimiter=',', quotechar='"')
  90. for row in csv_reader:
  91. if not keys:
  92. keys = row
  93. continue
  94. item = collections.OrderedDict()
  95. for key, val in zip(keys, row):
  96. item[key] = val
  97. result.append(item)
  98. return result
  99. def csv_write(info=[], file_='output.csv'):
  100. assert type(info) is list and len(info)>0, "info must be a non-empty list"
  101. assert all(isinstance(row, dict) for row in info), "Rows must be dictionaries!"
  102. with open(file_, 'w', newline='', encoding='utf8') as csv_file:
  103. csv_writer = csv.writer(csv_file, delimiter=',', quotechar='"')
  104. csv_writer.writerow(info[0].keys())
  105. for row in info:
  106. csv_writer.writerow(row.values())
  107. return
  108. class MyOD(collections.OrderedDict):
  109. def __init__(self, *args, **kwargs):
  110. super().__init__(*args, **kwargs)
  111. self._anti_list_casesensitive = None
  112. self._anti_list_caseinsensitive = None
  113. @property
  114. def anti_list_casesensitive(self):
  115. if not self._anti_list_casesensitive:
  116. self._anti_list_casesensitive = {self[x]:x for x in self}
  117. return self._anti_list_casesensitive
  118. @property
  119. def anti_list_caseinsensitive(self):
  120. if not self._anti_list_caseinsensitive:
  121. self._anti_list_caseinsensitive = {self[x].lower() if type(self[x]) is str else self[x] :x for x in self}
  122. return self._anti_list_caseinsensitive
  123. #MyOD[key] = val <-- MyOD.get(key) = val <--> MyOD.get_by_val(val) = key
  124. def get_by_val(self, val, case_sensitive=True):
  125. return (self.anti_list_casesensitive if case_sensitive else self.anti_list_caseinsensitive)[val]
  126. def get_by_key_val(self, key, val, case_sensitive=True, return_value=False):
  127. for x, y in self.items():
  128. if (y[key] == val and case_sensitive) or (y[key].lower() == val.lower() and not case_sensitive):
  129. return y if return_value else x
  130. return None
  131. def line_drawing_unordered_list(l):
  132. result = ""
  133. if l:
  134. for x in l[:-1]:
  135. result += "├ {}\n".format(x)
  136. result += "└ {}".format(l[-1])
  137. return result
  138. def str_to_datetime(d):
  139. if isinstance(d, datetime.datetime):
  140. return d
  141. return datetime.datetime.strptime(d, '%Y-%m-%d %H:%M:%S.%f')
  142. def datetime_to_str(d):
  143. if not isinstance(d, datetime.datetime):
  144. raise TypeError('Input of datetime_to_str function must be a datetime.datetime object. Output is a str')
  145. return '{:%Y-%m-%d %H:%M:%S.%f}'.format(d)
  146. class MyCounter():
  147. def __init__(self):
  148. self.n = 0
  149. return
  150. def lvl(self):
  151. self.n += 1
  152. return self.n
  153. def reset(self):
  154. self.n = 0
  155. return self.n
  156. def wrapper(func, *args, **kwargs):
  157. def wrapped(update):
  158. return func(update, *args, **kwargs)
  159. return wrapped
  160. async def async_wrapper(func, *args, **kwargs):
  161. async def wrapped(update):
  162. return await func(update, *args, **kwargs)
  163. return wrapped
  164. #Decorator: such decorated functions have effect only if update is forwarded from someone (you can specify *by* whom)
  165. def forwarded(by=None):
  166. def isforwardedby(update, by):
  167. if 'forward_from' not in update:
  168. return False
  169. if by:
  170. if update['forward_from']['id']!=by:
  171. return False
  172. return True
  173. def decorator(view_func):
  174. if asyncio.iscoroutinefunction(view_func):
  175. async def decorated(update):
  176. if isforwardedby(update, by):
  177. return await view_func(update)
  178. else:
  179. def decorated(update):
  180. if isforwardedby(update, by):
  181. return view_func(update)
  182. return decorated
  183. return decorator
  184. #Decorator: such decorated functions have effect only if update comes from specific chat.
  185. def chat_selective(chat_id=None):
  186. def check_function(update, chat_id):
  187. if 'chat' not in update:
  188. return False
  189. if chat_id:
  190. if update['chat']['id']!=chat_id:
  191. return False
  192. return True
  193. def decorator(view_func):
  194. if asyncio.iscoroutinefunction(view_func):
  195. async def decorated(update):
  196. if check_function(update, chat_id):
  197. return await view_func(update)
  198. else:
  199. def decorated(update):
  200. if check_function(update, chat_id):
  201. return view_func(update)
  202. return decorated
  203. return decorator
  204. async def sleep_until(when):
  205. if not isinstance(when, datetime.datetime):
  206. raise TypeError("sleep_until takes a datetime.datetime object as argument!")
  207. delta = when - datetime.datetime.now()
  208. if delta.days>=0:
  209. await asyncio.sleep(max(1, delta.seconds//2))
  210. return
  211. async def wait_and_do(when, what, *args, **kwargs):
  212. while when >= datetime.datetime.now():
  213. await sleep_until(when)
  214. return await what(*args, **kwargs)
  215. def get_csv_string(l):
  216. return ','.join(
  217. str(x) if type(x) is not str
  218. else '"{}"'.format(x)
  219. for x in l
  220. )
  221. def case_accent_insensitive_sql(field):
  222. """Given a field, return a part of SQL string necessary to perform a case- and accent-insensitive query."""
  223. replacements = [
  224. (' ', ''),
  225. ('à', 'a'),
  226. ('è', 'e'),
  227. ('é', 'e'),
  228. ('ì', 'i'),
  229. ('ò', 'o'),
  230. ('ù', 'u'),
  231. ]
  232. return "{r}LOWER({f}){w}".format(
  233. r="replace(".upper()*len(replacements),
  234. f=field,
  235. w=''.join(
  236. ", '{w[0]}', '{w[1]}')".format(w=w)
  237. for w in replacements
  238. )
  239. )
  240. ARTICOLI = MyOD()
  241. ARTICOLI[1] = {
  242. 'ind': 'un',
  243. 'dets': 'il',
  244. 'detp': 'i',
  245. 'dess': 'l',
  246. 'desp': 'i'
  247. }
  248. ARTICOLI[2] = {
  249. 'ind': 'una',
  250. 'dets': 'la',
  251. 'detp': 'le',
  252. 'dess': 'lla',
  253. 'desp': 'lle'
  254. }
  255. ARTICOLI[3] = {
  256. 'ind': 'uno',
  257. 'dets': 'lo',
  258. 'detp': 'gli',
  259. 'dess': 'llo',
  260. 'desp': 'gli'
  261. }
  262. ARTICOLI[4] = {
  263. 'ind': 'un',
  264. 'dets': 'l\'',
  265. 'detp': 'gli',
  266. 'dess': 'll\'',
  267. 'desp': 'gli'
  268. }
  269. class Gettable():
  270. """Gettable objects can be retrieved from memory without being duplicated.
  271. Key is the primary key.
  272. Use classmethod get to instanciate (or retrieve) Gettable objects.
  273. Assign SubClass.instances = {} or Gettable.instances will contain SubClass objects.
  274. """
  275. instances = {}
  276. @classmethod
  277. def get(cls, key, *args, **kwargs):
  278. if key not in cls.instances:
  279. cls.instances[key] = cls(key, *args, **kwargs)
  280. return cls.instances[key]
  281. class Confirmable():
  282. """Confirmable objects are provided with a confirm instance method.
  283. It evaluates True if it was called within self._confirm_timedelta, False otherwise.
  284. When it returns True, timer is reset.
  285. """
  286. CONFIRM_TIMEDELTA = datetime.timedelta(seconds=10)
  287. def __init__(self, confirm_timedelta=None):
  288. if confirm_timedelta is None:
  289. confirm_timedelta = self.__class__.CONFIRM_TIMEDELTA
  290. self.set_confirm_timedelta(confirm_timedelta)
  291. self._confirm_datetimes = {}
  292. @property
  293. def confirm_timedelta(self):
  294. return self._confirm_timedelta
  295. def confirm_datetime(self, who='unique'):
  296. if who not in self._confirm_datetimes:
  297. self._confirm_datetimes[who] = datetime.datetime.now() - 2*self.confirm_timedelta
  298. confirm_datetime = self._confirm_datetimes[who]
  299. return confirm_datetime
  300. def set_confirm_timedelta(self, confirm_timedelta):
  301. if type(confirm_timedelta) is int:
  302. confirm_timedelta = datetime.timedelta(seconds=confirm_timedelta)
  303. assert isinstance(confirm_timedelta, datetime.timedelta), "confirm_timedelta must be a datetime.timedelta instance!"
  304. self._confirm_timedelta = confirm_timedelta
  305. def confirm(self, who='unique'):
  306. now = datetime.datetime.now()
  307. if now >= self.confirm_datetime(who) + self.confirm_timedelta:
  308. self._confirm_datetimes[who] = now
  309. return False
  310. self._confirm_datetimes[who] = now - 2*self.confirm_timedelta
  311. return True
  312. class HasBot():
  313. """HasBot objects have a class method which sets the class attribute bot (set_bot)\
  314. and an instance property which returns it (bot).
  315. """
  316. bot = None
  317. @property
  318. def bot(self):
  319. return self.__class__.bot
  320. @property
  321. def db(self):
  322. return self.bot.db
  323. @classmethod
  324. def set_bot(cls, bot):
  325. cls.bot = bot
  326. class CachedPage(Gettable):
  327. """Store a webpage in this object, return cached webpage during CACHE_TIME, otherwise refresh.
  328. Usage:
  329. cached_page = CachedPage.get('https://www.google.com', datetime.timedelta(seconds=30), **kwargs)
  330. page = await cached_page.get_page()
  331. __init__ arguments
  332. url: the URL to be cached
  333. cache_time: timedelta from last_update during which page is not refreshed
  334. **kwargs will be passed to async_get function
  335. """
  336. CACHE_TIME = datetime.timedelta(minutes=5)
  337. instances = {}
  338. def __init__(self, url, cache_time=None, **async_get_kwargs):
  339. self._url = url
  340. if type(cache_time) is int:
  341. cache_time = datetime.timedelta(seconds=cache_time)
  342. if cache_time is None:
  343. cache_time = self.__class__.CACHE_TIME
  344. assert type(cache_time) is datetime.timedelta, "Cache time must be a datetime.timedelta object!"
  345. self._cache_time = cache_time
  346. self._page = None
  347. self._last_update = datetime.datetime.now() - self.cache_time
  348. self._async_get_kwargs = async_get_kwargs
  349. @property
  350. def url(self):
  351. return self._url
  352. @property
  353. def cache_time(self):
  354. return self._cache_time
  355. @property
  356. def page(self):
  357. return self._page
  358. @property
  359. def last_update(self):
  360. return self._last_update
  361. @property
  362. def async_get_kwargs(self):
  363. return self._async_get_kwargs
  364. @property
  365. def is_old(self):
  366. return datetime.datetime.now() > self.last_update + self.cache_time
  367. async def refresh(self):
  368. try:
  369. self._page = await async_get(self.url, **self.async_get_kwargs)
  370. self._last_update = datetime.datetime.now()
  371. return 0
  372. except Exception as e:
  373. self._page = None
  374. logging.error(''.format(e), exc_info=True)
  375. return 1
  376. return 1
  377. async def get_page(self):
  378. if self.is_old:
  379. await self.refresh()
  380. return self.page
  381. class Confirmator(Gettable, Confirmable):
  382. instances = {}
  383. def __init__(self, key, *args, confirm_timedelta=None):
  384. Confirmable.__init__(self, confirm_timedelta)
  385. def get_cleaned_text(update, bot=None, replace=[], strip='/ @'):
  386. if bot is not None:
  387. replace.append(
  388. '@{.name}'.format(
  389. bot
  390. )
  391. )
  392. text = update['text'].strip(strip)
  393. for s in replace:
  394. while s and text.lower().startswith(s.lower()):
  395. text = text[len(s):]
  396. return text.strip(strip)
  397. def get_user(record):
  398. if not record:
  399. return
  400. from_ = {key: val for key, val in record.items()}
  401. first_name, last_name, username, id_ = None, None, None, None
  402. result = ''
  403. if 'telegram_id' in from_:
  404. from_['id'] = from_['telegram_id']
  405. if 'id' in from_:
  406. result = '<a href="tg://user?id={}">{{name}}</a>'.format(from_['id'])
  407. if 'username' in from_ and from_['username']:
  408. result = result.format(
  409. name=from_['username']
  410. )
  411. elif 'first_name' in from_ and from_['first_name'] and 'last_name' in from_ and from_['last_name']:
  412. result = result.format(
  413. name='{} {}'.format(
  414. from_['first_name'],
  415. from_['last_name']
  416. )
  417. )
  418. elif 'first_name' in from_ and from_['first_name']:
  419. result = result.format(
  420. name=from_['first_name']
  421. )
  422. elif 'last_name' in from_ and from_['last_name']:
  423. result = result.format(
  424. name=from_['last_name']
  425. )
  426. else:
  427. result = result.format(
  428. name="Utente anonimo"
  429. )
  430. return result
  431. def datetime_from_utc_to_local(utc_datetime):
  432. now_timestamp = time.time()
  433. offset = datetime.datetime.fromtimestamp(now_timestamp) - datetime.datetime.utcfromtimestamp(now_timestamp)
  434. return utc_datetime + offset
  435. # TIME_SYMBOLS from more specific to less specific (avoid false positives!)
  436. TIME_SYMBOLS = MyOD()
  437. TIME_SYMBOLS["'"] = 'minutes'
  438. TIME_SYMBOLS["settimana"] = 'weeks'
  439. TIME_SYMBOLS["settimane"] = 'weeks'
  440. TIME_SYMBOLS["weeks"] = 'weeks'
  441. TIME_SYMBOLS["week"] = 'weeks'
  442. TIME_SYMBOLS["giorno"] = 'days'
  443. TIME_SYMBOLS["giorni"] = 'days'
  444. TIME_SYMBOLS["secondi"] = 'seconds'
  445. TIME_SYMBOLS["seconds"] = 'seconds'
  446. TIME_SYMBOLS["secondo"] = 'seconds'
  447. TIME_SYMBOLS["minuti"] = 'minutes'
  448. TIME_SYMBOLS["minuto"] = 'minutes'
  449. TIME_SYMBOLS["minute"] = 'minutes'
  450. TIME_SYMBOLS["minutes"] = 'minutes'
  451. TIME_SYMBOLS["day"] = 'days'
  452. TIME_SYMBOLS["days"] = 'days'
  453. TIME_SYMBOLS["ore"] = 'hours'
  454. TIME_SYMBOLS["ora"] = 'hours'
  455. TIME_SYMBOLS["sec"] = 'seconds'
  456. TIME_SYMBOLS["min"] = 'minutes'
  457. TIME_SYMBOLS["m"] = 'minutes'
  458. TIME_SYMBOLS["h"] = 'hours'
  459. TIME_SYMBOLS["d"] = 'days'
  460. TIME_SYMBOLS["s"] = 'seconds'
  461. def _interval_parser(text, result):
  462. text = text.lower()
  463. succeeded = False
  464. if result is None:
  465. result = []
  466. if len(result)==0 or result[-1]['ok']:
  467. text_part = ''
  468. _text = text # I need to iterate through _text modifying text
  469. for char in _text:
  470. if not char.isnumeric():
  471. break
  472. else:
  473. text_part += char
  474. text = text[1:]
  475. if text_part.isnumeric():
  476. result.append(
  477. dict(
  478. unit=None,
  479. value=int(text_part),
  480. ok=False
  481. )
  482. )
  483. succeeded = True, True
  484. if text:
  485. dummy, result = _interval_parser(text, result)
  486. elif len(result)>0 and not result[-1]['ok']:
  487. text_part = ''
  488. _text = text # I need to iterate through _text modifying text
  489. for char in _text:
  490. if char.isnumeric():
  491. break
  492. else:
  493. text_part += char
  494. text = text[1:]
  495. for time_symbol, unit in TIME_SYMBOLS.items():
  496. if time_symbol in text_part:
  497. result[-1]['unit'] = unit
  498. result[-1]['ok'] = True
  499. succeeded = True, True
  500. break
  501. else:
  502. result.pop()
  503. if text:
  504. dummy, result = _interval_parser(text, result)
  505. return succeeded, result
  506. def _date_parser(text, result):
  507. succeeded = False
  508. if 3 <= len(text) <= 10 and text.count('/')>=1:
  509. if 3 <= len(text) <= 5 and text.count('/')==1:
  510. text += '/{:%y}'.format(datetime.datetime.now())
  511. if 6 <= len(text) <= 10 and text.count('/')==2:
  512. day, month, year = [
  513. int(n) for n in [
  514. ''.join(char)
  515. for char in text.split('/')
  516. if char.isnumeric()
  517. ]
  518. ]
  519. if year < 100: year += 2000
  520. if result is None: result = []
  521. result += [
  522. dict(
  523. unit='day',
  524. value=day,
  525. ok=True
  526. ),
  527. dict(
  528. unit='month',
  529. value=month,
  530. ok=True
  531. ),
  532. dict(
  533. unit='year',
  534. value=year,
  535. ok=True
  536. )
  537. ]
  538. succeeded = True, True
  539. return succeeded, result
  540. def _time_parser(text, result):
  541. succeeded = False
  542. if (1 <= len(text) <= 8) and any(char.isnumeric() for char in text):
  543. text = text.replace('.', ':')
  544. if len(text) <= 2:
  545. text = '{:02d}:00:00'.format(int(text))
  546. elif len(text) == 4 and ':' not in text:
  547. text = '{:02d}:{:02d}:00'.format(*[int(x) for x in (text[:2], text[2:])])
  548. elif text.count(':')==1:
  549. text = '{:02d}:{:02d}:00'.format(*[int(x) for x in text.split(':')])
  550. if text.count(':')==2:
  551. hour, minute, second = (int(x) for x in text.split(':'))
  552. if (0 <= hour <= 24) and (0 <= minute <= 60) and (0 <= second <= 60):
  553. if result is None: result = []
  554. result += [
  555. dict(
  556. unit='hour',
  557. value=hour,
  558. ok=True
  559. ),
  560. dict(
  561. unit='minute',
  562. value=minute,
  563. ok=True
  564. ),
  565. dict(
  566. unit='second',
  567. value=second,
  568. ok=True
  569. )
  570. ]
  571. succeeded = True
  572. return succeeded, result
  573. WEEKDAY_NAMES_ITA = ["Lunedì", "Martedì", "Mercoledì", "Giovedì", "Venerdì", "Sabato", "Domenica"]
  574. WEEKDAY_NAMES_ENG = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
  575. def _period_parser(text, result):
  576. succeeded = False
  577. if text in ('every', 'ogni',):
  578. succeeded = True
  579. if text.title() in WEEKDAY_NAMES_ITA + WEEKDAY_NAMES_ENG:
  580. day_code = (WEEKDAY_NAMES_ITA + WEEKDAY_NAMES_ENG).index(text.title())
  581. if day_code > 6: day_code -= 7
  582. today = datetime.date.today()
  583. days = 1
  584. while (today + datetime.timedelta(days=days)).weekday() != day_code:
  585. days += 1
  586. if result is None:
  587. result = []
  588. result.append(
  589. dict(
  590. unit='days',
  591. value=days,
  592. ok=True,
  593. weekly=True
  594. )
  595. )
  596. succeeded = True
  597. else:
  598. succeeded, result = _interval_parser(text, result)
  599. return succeeded, result
  600. TIME_WORDS = {
  601. 'tra': dict(
  602. parser=_interval_parser,
  603. recurring=False,
  604. type_='delta'
  605. ),
  606. 'in': dict(
  607. parser=_interval_parser,
  608. recurring=False,
  609. type_='delta'
  610. ),
  611. 'at': dict(
  612. parser=_time_parser,
  613. recurring=False,
  614. type_='set'
  615. ),
  616. 'on': dict(
  617. parser=_date_parser,
  618. recurring=False,
  619. type_='set'
  620. ),
  621. 'alle': dict(
  622. parser=_time_parser,
  623. recurring=False,
  624. type_='set'
  625. ),
  626. 'il': dict(
  627. parser=_date_parser,
  628. recurring=False,
  629. type_='set'
  630. ),
  631. 'every': dict(
  632. parser=_period_parser,
  633. recurring=True,
  634. type_='delta'
  635. ),
  636. 'ogni': dict(
  637. parser=_period_parser,
  638. recurring=True,
  639. type_='delta'
  640. ),
  641. }
  642. def parse_datetime_interval_string(text):
  643. parsers = []
  644. result_text, result_datetime, result_timedelta = [], None, None
  645. is_quoted_text = False
  646. for word in text.split(' '):
  647. if word.count('"') % 2:
  648. is_quoted_text = not is_quoted_text
  649. if is_quoted_text or '"' in word:
  650. result_text.append(
  651. word.replace('"', '') if 'href=' not in word else word
  652. )
  653. continue
  654. result_text.append(word)
  655. word = word.lower()
  656. succeeded = False
  657. if len(parsers) > 0:
  658. succeeded, result = parsers[-1]['parser'](word, parsers[-1]['result'])
  659. if succeeded:
  660. parsers[-1]['result'] = result
  661. if not succeeded and word in TIME_WORDS:
  662. parsers.append(
  663. dict(
  664. result=None,
  665. parser=TIME_WORDS[word]['parser'],
  666. recurring=TIME_WORDS[word]['recurring'],
  667. type_=TIME_WORDS[word]['type_']
  668. )
  669. )
  670. if succeeded:
  671. result_text.pop()
  672. if len(result_text)>0 and result_text[-1].lower() in TIME_WORDS:
  673. result_text.pop()
  674. result_text = escape_html_chars(
  675. ' '.join(result_text)
  676. )
  677. parsers = list(
  678. filter(
  679. lambda x: 'result' in x and x['result'],
  680. parsers
  681. )
  682. )
  683. recurring_event = False
  684. weekly = False
  685. _timedelta = datetime.timedelta()
  686. _datetime = None
  687. _now = datetime.datetime.now()
  688. for parser in parsers:
  689. if parser['recurring']:
  690. recurring_event = True
  691. type_ = parser['type_']
  692. for result in parser['result']:
  693. if not result['ok']:
  694. continue
  695. if recurring_event and 'weekly' in result and result['weekly']:
  696. weekly = True
  697. if type_ == 'set':
  698. if _datetime is None:
  699. _datetime = _now
  700. _datetime = _datetime.replace(
  701. **{
  702. result['unit']: result['value']
  703. }
  704. )
  705. elif type_ == 'delta':
  706. _timedelta += datetime.timedelta(
  707. **{
  708. result['unit']: result['value']
  709. }
  710. )
  711. if _datetime:
  712. result_datetime = _datetime
  713. if _timedelta:
  714. if result_datetime is None: result_datetime = _now
  715. if recurring_event:
  716. result_timedelta = _timedelta
  717. if weekly:
  718. result_timedelta = datetime.timedelta(days=7)
  719. else:
  720. result_datetime += _timedelta
  721. while result_datetime and result_datetime < datetime.datetime.now():
  722. result_datetime += (result_timedelta if result_timedelta else datetime.timedelta(days=1))
  723. return result_text, result_datetime, result_timedelta
  724. DAY_GAPS = {
  725. -1: 'ieri',
  726. -2: 'avantieri',
  727. 0: 'oggi',
  728. 1: 'domani',
  729. 2: 'dopodomani'
  730. }
  731. MONTH_NAMES_ITA = MyOD()
  732. MONTH_NAMES_ITA[1] = "gennaio"
  733. MONTH_NAMES_ITA[2] = "febbraio"
  734. MONTH_NAMES_ITA[3] = "marzo"
  735. MONTH_NAMES_ITA[4] = "aprile"
  736. MONTH_NAMES_ITA[5] = "maggio"
  737. MONTH_NAMES_ITA[6] = "giugno"
  738. MONTH_NAMES_ITA[7] = "luglio"
  739. MONTH_NAMES_ITA[8] = "agosto"
  740. MONTH_NAMES_ITA[9] = "settembre"
  741. MONTH_NAMES_ITA[10] = "ottobre"
  742. MONTH_NAMES_ITA[11] = "novembre"
  743. MONTH_NAMES_ITA[12] = "dicembre"
  744. def beautytd(td):
  745. result = ''
  746. if type(td) is int:
  747. td = datetime.timedelta(seconds=td)
  748. assert isinstance(td, datetime.timedelta), "td must be a datetime.timedelta object!"
  749. mtd = datetime.timedelta
  750. if td < mtd(minutes=1):
  751. result = "{:.0f} secondi".format(
  752. td.total_seconds()
  753. )
  754. elif td < mtd(minutes=10):
  755. result = "{:.0f} min{}".format(
  756. td.total_seconds()//60,
  757. (
  758. " {:.0f} s".format(
  759. td.total_seconds()%60
  760. )
  761. ) if td.total_seconds()%60 else ''
  762. )
  763. elif td < mtd(days=1):
  764. result = "{:.0f} h{}".format(
  765. td.total_seconds()//3600,
  766. (
  767. " {:.0f} min".format(
  768. (td.total_seconds()%3600)//60)
  769. ) if td.total_seconds()%3600 else ''
  770. )
  771. elif td < mtd(days=30):
  772. result = "{} giorni{}".format(
  773. td.days,
  774. (
  775. " {:.0f} h".format(
  776. td.total_seconds()%(3600*24)//3600
  777. )
  778. ) if td.total_seconds()%(3600*24) else ''
  779. )
  780. return result
  781. def beautydt(dt):
  782. """Format a datetime in a smart way
  783. """
  784. if type(dt) is str:
  785. dt = str_to_datetime(dt)
  786. assert isinstance(dt, datetime.datetime), "dt must be a datetime.datetime object!"
  787. now = datetime.datetime.now()
  788. gap = dt - now
  789. gap_days = (dt.date() - now.date()).days
  790. result = "{dt:alle %H:%M}".format(
  791. dt=dt
  792. )
  793. if abs(gap) < datetime.timedelta(minutes=30):
  794. result += "{dt::%S}".format(dt=dt)
  795. if -2 <= gap_days <= 2:
  796. result += " di {dg}".format(
  797. dg=DAY_GAPS[gap_days]
  798. )
  799. elif gap.days not in (-1, 0):
  800. result += " del {d}{m}".format(
  801. d=dt.day,
  802. m=(
  803. "" if now.year == dt.year and now.month == dt.month
  804. else " {m}{y}".format(
  805. m=MONTH_NAMES_ITA[dt.month].title(),
  806. y="" if now.year == dt.year
  807. else " {}".format(dt.year)
  808. )
  809. )
  810. )
  811. return result
  812. HTML_SYMBOLS = MyOD()
  813. HTML_SYMBOLS["&"] = "&amp;"
  814. HTML_SYMBOLS["<"] = "&lt;"
  815. HTML_SYMBOLS[">"] = "&gt;"
  816. HTML_SYMBOLS["\""] = "&quot;"
  817. HTML_SYMBOLS["&lt;b&gt;"] = "<b>"
  818. HTML_SYMBOLS["&lt;/b&gt;"] = "</b>"
  819. HTML_SYMBOLS["&lt;i&gt;"] = "<i>"
  820. HTML_SYMBOLS["&lt;/i&gt;"] = "</i>"
  821. HTML_SYMBOLS["&lt;code&gt;"] = "<code>"
  822. HTML_SYMBOLS["&lt;/code&gt;"] = "</code>"
  823. HTML_SYMBOLS["&lt;pre&gt;"] = "<pre>"
  824. HTML_SYMBOLS["&lt;/pre&gt;"] = "</pre>"
  825. HTML_SYMBOLS["&lt;a href=&quot;"] = "<a href=\""
  826. HTML_SYMBOLS["&quot;&gt;"] = "\">"
  827. HTML_SYMBOLS["&lt;/a&gt;"] = "</a>"
  828. HTML_TAGS = [
  829. None, "<b>", "</b>",
  830. None, "<i>", "</i>",
  831. None, "<code>", "</code>",
  832. None, "<pre>", "</pre>",
  833. None, "<a href=\"", "\">", "</a>",
  834. None
  835. ]
  836. def remove_html_tags(text):
  837. for tag in HTML_TAGS:
  838. if tag is None:
  839. continue
  840. text = text.replace(tag, '')
  841. return text
  842. def escape_html_chars(text):
  843. for s, r in HTML_SYMBOLS.items():
  844. text = text.replace(s, r)
  845. copy = text
  846. expected_tag = None
  847. while copy:
  848. min_ = min(
  849. (
  850. dict(
  851. position=copy.find(tag) if tag in copy else len(copy),
  852. tag=tag
  853. )
  854. for tag in HTML_TAGS
  855. if tag
  856. ),
  857. key=lambda x: x['position'],
  858. default=0
  859. )
  860. if min_['position'] == len(copy):
  861. break
  862. if expected_tag and min_['tag'] != expected_tag:
  863. return text.replace('<', '_').replace('>', '_')
  864. expected_tag = HTML_TAGS[HTML_TAGS.index(min_['tag'])+1]
  865. copy = extract(copy, min_['tag'])
  866. return text
  867. def accents_to_jolly(text, lower=True):
  868. to_be_replaced = ('à', 'è', 'é', 'ì', 'ò', 'ù')
  869. if lower:
  870. text = text.lower()
  871. else:
  872. to_be_replaced += tuple(s.upper() for s in to_be_replaced)
  873. for s in to_be_replaced:
  874. text = text.replace(s, '_')
  875. return text.replace("'", "''")
  876. def get_secure_key(allowed_chars=None, length=6):
  877. if allowed_chars is None:
  878. allowed_chars = string.ascii_uppercase + string.digits
  879. return ''.join(
  880. random.SystemRandom().choice(
  881. allowed_chars
  882. )
  883. for _ in range(length)
  884. )
  885. def round_to_minute(datetime_):
  886. return (
  887. datetime_ + datetime.timedelta(seconds=30)
  888. ).replace(second=0, microsecond=0)
  889. def get_line_by_content(text, key):
  890. for line in text.split('\n'):
  891. if key in line:
  892. return line
  893. return
  894. def str_to_int(string):
  895. string = ''.join(
  896. char
  897. for char in string
  898. if char.isnumeric()
  899. )
  900. if len(string) == 0:
  901. string = '0'
  902. return int(string)