Queer European MD passionate about IT

api_helper.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. """Get and parse Telegram API web page."""
  2. # Standard library modules
  3. import argparse
  4. import asyncio
  5. import inspect
  6. import logging
  7. # Third party modules
  8. import os
  9. from typing import List
  10. import aiohttp
  11. from bs4 import BeautifulSoup
  12. # Project modules
  13. from davtelepot.api import TelegramBot
  14. api_url = "https://core.telegram.org/bots/api"
  15. class TelegramApiMethod(object):
  16. types = {
  17. 'Array of String': "List[str]",
  18. 'Boolean': "bool",
  19. 'Integer': "int",
  20. 'Integer or String': "Union[int, str]",
  21. 'String': "str",
  22. }
  23. """Telegram bot API method."""
  24. def __init__(self, name, description, table):
  25. """Initialize object with name, description and table data."""
  26. self._name = name
  27. self._description = description
  28. self._table = table
  29. self._parameters = self.get_parameters_from_table()
  30. @property
  31. def name(self):
  32. """Return method name."""
  33. return self._name
  34. @property
  35. def description(self):
  36. """Return method description."""
  37. return self._description
  38. @property
  39. def description_80chars(self):
  40. """Return method description, breaking lines at 80 characters."""
  41. result, current_line = '', ''
  42. indentation = 8
  43. redundant_string = "Use this method to "
  44. for n, paragraph in enumerate(self.description.replace('.', '.\n').split('\n')):
  45. additional_indentation = 0
  46. if n == 0 and paragraph.startswith(redundant_string):
  47. paragraph = paragraph[len(redundant_string)].upper() + paragraph[len(redundant_string)+1:]
  48. for word in paragraph.split(' '):
  49. if len(current_line) + len(word) > 80 - indentation - additional_indentation:
  50. additional_indentation = max(additional_indentation, 4)
  51. result += f"{current_line.strip()}\n{' ' * additional_indentation}"
  52. current_line = ""
  53. current_line += f"{word} "
  54. if len(current_line):
  55. result += f"{current_line.strip()}\n"
  56. current_line = ""
  57. if n == 0:
  58. result += '\n'
  59. return result.strip()
  60. @property
  61. def table(self):
  62. """Return method parameters table."""
  63. return self._table
  64. @property
  65. def parameters(self):
  66. return self._parameters
  67. @property
  68. def parameters_with_types(self) -> List[str]:
  69. return [
  70. f"{parameter['name']}: {parameter['type']}"
  71. for parameter in self._parameters
  72. ]
  73. def print_parameters_table(self):
  74. """Extract parameters from API table."""
  75. result = ''
  76. if self.table is None:
  77. return "No parameters"
  78. rows = self.table.tbody.find_all('tr')
  79. if rows is None:
  80. rows = []
  81. for row in rows:
  82. result += '------\n'
  83. columns = row.find_all('td')
  84. if columns is None:
  85. columns = []
  86. for column in columns:
  87. result += f'| {column.text.strip()} |'
  88. result += '\n'
  89. result += '\n'
  90. return result
  91. def get_parameters_from_table(self):
  92. if self.table is None:
  93. return []
  94. parameters = []
  95. rows = self.table.tbody.find_all('tr') or []
  96. for row in rows:
  97. columns = row.find_all('td') or []
  98. name, type_, *_ = map(lambda column: column.text.strip(), columns)
  99. if type_ in self.types:
  100. type_ = self.types[type_]
  101. else:
  102. type_ = f"'{type_}'"
  103. parameters.append(
  104. dict(
  105. name=name,
  106. type=type_
  107. )
  108. )
  109. return parameters
  110. async def print_api_methods(filename=None,
  111. print_all=False,
  112. output_file=None,
  113. input_file=None):
  114. """Get information from Telegram bot API web page."""
  115. implemented_methods = dir(TelegramBot)
  116. if input_file is None or not os.path.isfile(input_file):
  117. async with aiohttp.ClientSession(
  118. timeout=aiohttp.ClientTimeout(
  119. total=100
  120. )
  121. ) as session:
  122. async with session.get(
  123. api_url
  124. ) as response:
  125. web_page = BeautifulSoup(
  126. await response.text(),
  127. "html.parser"
  128. )
  129. else:
  130. with open(input_file, 'r') as local_web_page:
  131. web_page = BeautifulSoup(
  132. ''.join(local_web_page.readlines()),
  133. "html.parser"
  134. )
  135. if filename is not None:
  136. with open(filename, 'w') as _file:
  137. _file.write(web_page.decode())
  138. methods = []
  139. for method in web_page.find_all("h4"):
  140. method_name = method.text
  141. description = ''
  142. parameters_table = None
  143. for tag in method.next_siblings:
  144. if tag.name is None:
  145. continue
  146. if tag.name == 'h4':
  147. break # Stop searching in siblings if <h4> is found
  148. if tag.name == 'table':
  149. parameters_table = tag
  150. break # Stop searching in siblings if <table> is found
  151. description += tag.get_text()
  152. # Methods start with a lowercase letter
  153. if method_name and method_name[0] == method_name[0].lower():
  154. methods.append(
  155. TelegramApiMethod(
  156. method_name,
  157. description,
  158. parameters_table
  159. )
  160. )
  161. new_line = '\n'
  162. new_methods = []
  163. edited_methods = []
  164. for method in methods:
  165. if print_all or method.name not in implemented_methods:
  166. new_methods.append(method)
  167. else:
  168. parameters = set(parameter['name'] for parameter in method.parameters)
  169. implemented_parameters = set(
  170. parameter.strip('_') # Parameter `type` becomes `type_` in python
  171. for parameter in inspect.signature(
  172. getattr(TelegramBot,
  173. method.name)
  174. ).parameters.keys()
  175. if parameter != 'self'
  176. )
  177. new_parameters = parameters - implemented_parameters
  178. deprecated_parameters = implemented_parameters - parameters - {'kwargs'}
  179. if new_parameters or deprecated_parameters:
  180. edited_methods.append(
  181. dict(
  182. name=method.name,
  183. new_parameters=new_parameters,
  184. deprecated_parameters=deprecated_parameters
  185. )
  186. )
  187. if output_file:
  188. with open(output_file, 'w') as file:
  189. if new_methods:
  190. file.write(
  191. "from typing import List, Union\n"
  192. "from davtelepot.api import TelegramBot\n\n\n"
  193. "# noinspection PyPep8Naming\n"
  194. "class Bot(TelegramBot):\n\n"
  195. )
  196. file.writelines(
  197. f" async def {method.name}("
  198. f"{', '.join(['self'] + method.parameters_with_types)}"
  199. f"):\n"
  200. f" \"\"\""
  201. f"{method.description_80chars.replace(new_line, new_line + ' ' * 8)}\n"
  202. f" See https://core.telegram.org/bots/api#"
  203. f"{method.name.lower()} for details.\n"
  204. f" \"\"\"\n"
  205. f" return await self.api_request(\n"
  206. f" '{method.name}',\n"
  207. f" parameters=locals()\n"
  208. f" )\n\n"
  209. for method in new_methods
  210. )
  211. if edited_methods:
  212. file.write('\n# === EDITED METHODS ===\n')
  213. for method in edited_methods:
  214. file.write(f'\n"""{method["name"]}\n')
  215. if method['new_parameters']:
  216. file.write(" New parameters: "
  217. + ", ".join(method['new_parameters'])
  218. + "\n")
  219. if method['deprecated_parameters']:
  220. file.write(" Deprecated parameters: "
  221. + ", ".join(method['deprecated_parameters'])
  222. + "\n")
  223. file.write('"""\n')
  224. else:
  225. print(
  226. '\n'.join(
  227. f"NAME\n\t{method.name}\n"
  228. f"PARAMETERS\n\t{', '.join(['self'] + method.parameters_with_types)}\n"
  229. f"DESCRIPTION\n\t{method.description}\n"
  230. f"TABLE\n\t{method.print_parameters_table()}\n\n"
  231. for method in new_methods
  232. )
  233. )
  234. for method in edited_methods:
  235. print(method['name'])
  236. if method['new_parameters']:
  237. print("\tNew parameters: " + ", ".join(method['new_parameters']))
  238. if method['deprecated_parameters']:
  239. print("\tDeprecated parameters: " + ", ".join(method['deprecated_parameters']))
  240. def main():
  241. cli_parser = argparse.ArgumentParser(
  242. description='Get Telegram API methods information from telegram '
  243. 'website.\n'
  244. 'Implement missing (or --all) methods in --out file, '
  245. 'or print methods information.',
  246. allow_abbrev=False,
  247. )
  248. cli_parser.add_argument('--file', '-f', '--filename', type=str,
  249. default=None,
  250. required=False,
  251. help='File path to store Telegram API web page')
  252. cli_parser.add_argument('--all', '-a',
  253. action='store_true',
  254. help='Print all methods (default: print missing '
  255. 'methods only)')
  256. cli_parser.add_argument('--out', '--output', '-o', type=str,
  257. default=None,
  258. required=False,
  259. help='File path to store methods implementation')
  260. cli_parser.add_argument('--in', '--input', '-i', type=str,
  261. default=None,
  262. required=False,
  263. help='File path to read Telegram API web page')
  264. cli_arguments = vars(cli_parser.parse_args())
  265. filename = cli_arguments['file']
  266. print_all = cli_arguments['all']
  267. output_file = cli_arguments['out']
  268. input_file = cli_arguments['in']
  269. asyncio.run(
  270. print_api_methods(filename=filename,
  271. print_all=print_all,
  272. output_file=output_file,
  273. input_file=input_file)
  274. )
  275. logging.info("Done!")
  276. if __name__ == '__main__':
  277. main()