generator.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. from __future__ import absolute_import
  2. import os
  3. import zlib
  4. import json
  5. import click
  6. import logging
  7. import six
  8. from datetime import datetime
  9. from subprocess import Popen, PIPE
  10. from contextlib import contextmanager
  11. from six.moves.urllib.parse import urlparse
  12. HERE = os.path.abspath(os.path.dirname(__file__))
  13. SENTRY_CONFIG = os.environ['SENTRY_CONF'] = os.path.join(HERE, 'sentry.conf.py')
  14. os.environ['SENTRY_SKIP_BACKEND_VALIDATION'] = '1'
  15. # No sentry or django imports before this point
  16. from sentry.runner import configure
  17. configure()
  18. from django.conf import settings
  19. # Fair game from here
  20. from django.core.management import call_command
  21. from sentry.utils.apidocs import (
  22. Runner, MockUtils, iter_scenarios,
  23. iter_endpoints, get_sections
  24. )
  25. from sentry.web.helpers import render_to_string
  26. OUTPUT_PATH = os.path.join(HERE, 'cache')
  27. HOST = urlparse(settings.SENTRY_OPTIONS['system.url-prefix']).netloc
  28. # We don't care about you, go away
  29. _logger = logging.getLogger('sentry.events')
  30. _logger.disabled = True
  31. def color_for_string(s):
  32. colors = ('red', 'green', 'yellow', 'blue', 'cyan', 'magenta')
  33. return colors[zlib.crc32(s) % len(colors)]
  34. def report(category, message, fg=None):
  35. if fg is None:
  36. fg = color_for_string(category)
  37. click.echo('[%s] %s: %s' % (
  38. six.text_type(datetime.utcnow()).split('.')[0],
  39. click.style(category, fg=fg),
  40. message
  41. ))
  42. def launch_redis():
  43. report('redis', 'Launching redis server')
  44. cl = Popen(['redis-server', '-'], stdin=PIPE, stdout=open(os.devnull, 'r+'))
  45. cl.stdin.write('''
  46. port %(port)s
  47. databases %(databases)d
  48. save ""
  49. ''' % {
  50. 'port': six.text_type(settings.SENTRY_APIDOCS_REDIS_PORT),
  51. 'databases': 4,
  52. })
  53. cl.stdin.flush()
  54. cl.stdin.close()
  55. return cl
  56. def spawn_sentry():
  57. report('sentry', 'Launching sentry server')
  58. cl = Popen(['sentry', '--config=' + SENTRY_CONFIG, 'run', 'web',
  59. '-w', '1', '--bind', '127.0.0.1:%s' % settings.SENTRY_APIDOCS_WEB_PORT])
  60. return cl
  61. @contextmanager
  62. def management_connection():
  63. from sqlite3 import connect
  64. cfg = settings.DATABASES['default']
  65. con = connect(cfg['NAME'])
  66. try:
  67. con.cursor()
  68. yield con
  69. finally:
  70. con.close()
  71. def init_db():
  72. drop_db()
  73. report('db', 'Migrating database (this can take some time)')
  74. call_command('syncdb', migrate=True, interactive=False,
  75. traceback=True, verbosity=0)
  76. def drop_db():
  77. report('db', 'Dropping database')
  78. try:
  79. os.remove(settings.DATABASES['default']['NAME'])
  80. except (OSError, IOError):
  81. pass
  82. class SentryBox(object):
  83. def __init__(self):
  84. self.redis = None
  85. self.sentry = None
  86. self.task_runner = None
  87. def __enter__(self):
  88. self.redis = launch_redis()
  89. self.sentry = spawn_sentry()
  90. init_db()
  91. return self
  92. def __exit__(self, exc_type, exc_value, tb):
  93. if self.sentry is not None:
  94. report('sentry', 'Shutting down sentry server')
  95. self.sentry.kill()
  96. self.sentry.wait()
  97. if self.redis is not None:
  98. report('redis', 'Stopping redis server')
  99. self.redis.kill()
  100. self.redis.wait()
  101. drop_db()
  102. def run_scenario(vars, scenario_ident, func):
  103. runner = Runner(scenario_ident, func, **vars)
  104. report('scenario', 'Running scenario "%s"' % scenario_ident)
  105. func(runner)
  106. return runner.to_json()
  107. @click.command()
  108. @click.option('--output-path', type=click.Path())
  109. @click.option('--output-format', type=click.Choice(['json', 'markdown', 'both']), default='both')
  110. def cli(output_path, output_format):
  111. """API docs dummy generator."""
  112. global OUTPUT_PATH
  113. if output_path is not None:
  114. OUTPUT_PATH = os.path.abspath(output_path)
  115. with SentryBox():
  116. utils = MockUtils()
  117. report('org', 'Creating user and organization')
  118. user = utils.create_user('john@interstellar.invalid')
  119. org = utils.create_org('The Interstellar Jurisdiction',
  120. owner=user)
  121. report('auth', 'Creating api token')
  122. api_token = utils.create_api_token(user)
  123. report('org', 'Creating team')
  124. team = utils.create_team('Powerful Abolitionist',
  125. org=org)
  126. utils.join_team(team, user)
  127. projects = []
  128. for project_name in 'Pump Station', 'Prime Mover':
  129. report('project', 'Creating project "%s"' % project_name)
  130. project = utils.create_project(project_name, teams=[team], org=org)
  131. release = utils.create_release(project=project, user=user)
  132. report('event', 'Creating event for "%s"' % project_name)
  133. event1 = utils.create_event(project=project, release=release,
  134. platform='python')
  135. event2 = utils.create_event(project=project, release=release,
  136. platform='java')
  137. projects.append({
  138. 'project': project,
  139. 'release': release,
  140. 'events': [event1, event2],
  141. })
  142. vars = {
  143. 'org': org,
  144. 'me': user,
  145. 'api_token': api_token,
  146. 'teams': [{
  147. 'team': team,
  148. 'projects': projects,
  149. }],
  150. }
  151. scenario_map = {}
  152. report('docs', 'Collecting scenarios')
  153. for scenario_ident, func in iter_scenarios():
  154. scenario = run_scenario(vars, scenario_ident, func)
  155. scenario_map[scenario_ident] = scenario
  156. section_mapping = {}
  157. report('docs', 'Collecting endpoint documentation')
  158. for endpoint in iter_endpoints():
  159. report('endpoint', 'Collecting docs for "%s"' %
  160. endpoint['endpoint_name'])
  161. section_mapping \
  162. .setdefault(endpoint['section'], []) \
  163. .append(endpoint)
  164. sections = get_sections()
  165. if output_format in ('json', 'both'):
  166. output_json(sections, scenario_map, section_mapping)
  167. if output_format in ('markdown', 'both'):
  168. output_markdown(sections, scenario_map, section_mapping)
  169. def output_json(sections, scenarios, section_mapping):
  170. report('docs', 'Generating JSON documents')
  171. for id, scenario in scenarios.items():
  172. dump_json('scenarios/%s.json' % id, scenario)
  173. section_listings = {}
  174. for section, title in sections.items():
  175. entries = {}
  176. for endpoint in section_mapping.get(section, []):
  177. entries[endpoint['endpoint_name']] = endpoint['title']
  178. dump_json('endpoints/%s.json' % endpoint['endpoint_name'],
  179. endpoint)
  180. section_listings[section] = {
  181. 'title': title,
  182. 'entries': entries
  183. }
  184. dump_json('sections.json', {'sections': section_listings})
  185. def output_markdown(sections, scenarios, section_mapping):
  186. report('docs', 'Generating markdown documents')
  187. for section, title in sections.items():
  188. i = 0
  189. links = []
  190. for endpoint in section_mapping.get(section, []):
  191. i += 1
  192. path = u"{}/{}.md".format(section, endpoint['endpoint_name'])
  193. auth = ''
  194. if len(endpoint['params'].get('auth', [])):
  195. auth = endpoint['params']['auth'][0]['description']
  196. payload = dict(
  197. title=endpoint['title'],
  198. sidebar_order=i,
  199. description='\n'.join(endpoint['text']).strip(),
  200. warning=endpoint['warning'],
  201. method=endpoint['method'],
  202. api_path=endpoint['path'],
  203. query_parameters=endpoint['params'].get('query'),
  204. path_parameters=endpoint['params'].get('path'),
  205. parameters=endpoint['params'].get('param'),
  206. authentication=auth,
  207. example_request=format_request(endpoint, scenarios),
  208. example_response=format_response(endpoint, scenarios)
  209. )
  210. dump_markdown(path, payload)
  211. links.append({'title': endpoint['title'], 'path': path})
  212. dump_index_markdown(section, title, links)
  213. def dump_json(path, data):
  214. path = os.path.join(OUTPUT_PATH, 'json', path)
  215. try:
  216. os.makedirs(os.path.dirname(path))
  217. except OSError:
  218. pass
  219. with open(path, 'w') as f:
  220. for line in json.dumps(data, indent=2, sort_keys=True).splitlines():
  221. f.write(line.rstrip() + '\n')
  222. def dump_index_markdown(section, title, links):
  223. path = os.path.join(OUTPUT_PATH, 'markdown', section, 'index.md')
  224. try:
  225. os.makedirs(os.path.dirname(path))
  226. except OSError:
  227. pass
  228. with open(path, 'w') as f:
  229. contents = render_to_string(
  230. 'sentry/apidocs/index.md',
  231. dict(title=title, links=links))
  232. f.write(contents)
  233. def dump_markdown(path, data):
  234. path = os.path.join(OUTPUT_PATH, 'markdown', path)
  235. try:
  236. os.makedirs(os.path.dirname(path))
  237. except OSError:
  238. pass
  239. with open(path, 'w') as f:
  240. template = u"""---
  241. # This file is automatically generated from the API using `api-docs/generate.py`
  242. # Do not manually this file.
  243. {}
  244. ---
  245. """
  246. contents = template.format(json.dumps(data, sort_keys=True, indent=2))
  247. f.write(contents)
  248. def find_first_scenario(endpoint, scenario_map):
  249. for scene in endpoint['scenarios']:
  250. if scene not in scenario_map:
  251. continue
  252. try:
  253. return scenario_map[scene]['requests'][0]
  254. except IndexError:
  255. return None
  256. return None
  257. def format_request(endpoint, scenario_map):
  258. scene = find_first_scenario(endpoint, scenario_map)
  259. if not scene:
  260. return ''
  261. request = scene['request']
  262. lines = [
  263. u"{} {} HTTP/1.1".format(request['method'], request['path']),
  264. 'Host: sentry.io',
  265. 'Authorization: Bearer <token>',
  266. ]
  267. lines.extend(format_headers(request['headers']))
  268. if request['data']:
  269. lines.append('')
  270. lines.append(json.dumps(request['data'],
  271. sort_keys=True,
  272. indent=2))
  273. return "\n".join(lines)
  274. def format_response(endpoint, scenario_map):
  275. scene = find_first_scenario(endpoint, scenario_map)
  276. if not scene:
  277. return ''
  278. response = scene['response']
  279. lines = [
  280. u"HTTP/1.1 {} {}".format(response['status'], response['reason']),
  281. ]
  282. lines.extend(format_headers(response['headers']))
  283. if response['data']:
  284. lines.append('')
  285. lines.append(json.dumps(response['data'],
  286. sort_keys=True,
  287. indent=2))
  288. return "\n".join(lines)
  289. def format_headers(headers):
  290. """Format headers into a list."""
  291. return [
  292. u'{}: {}'.format(key, value)
  293. for key, value
  294. in headers.items()
  295. ]
  296. if __name__ == '__main__':
  297. cli()