generator.py 10 KB

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