Queer European MD passionate about IT

api_helper.py 9.6 KB

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