123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322 |
- from __future__ import absolute_import
- import os
- import zlib
- import json
- import click
- import logging
- import six
- from datetime import datetime
- from subprocess import Popen, PIPE, check_output
- from six.moves.urllib.parse import urlparse
- HERE = os.path.abspath(os.path.dirname(__file__))
- SENTRY_CONFIG = os.environ["SENTRY_CONF"] = os.path.join(HERE, "sentry.conf.py")
- os.environ["SENTRY_SKIP_BACKEND_VALIDATION"] = "1"
- # No sentry or django imports before this point
- from sentry.runner import configure
- configure()
- from django.conf import settings
- # Fair game from here
- from django.core.management import call_command
- from sentry.utils.apidocs import Runner, MockUtils, iter_scenarios, iter_endpoints, get_sections
- from sentry.web.helpers import render_to_string
- OUTPUT_PATH = os.path.join(HERE, "cache")
- HOST = urlparse(settings.SENTRY_OPTIONS["system.url-prefix"]).netloc
- # We don't care about you, go away
- _logger = logging.getLogger("sentry.events")
- _logger.disabled = True
- def color_for_string(s):
- colors = ("red", "green", "yellow", "blue", "cyan", "magenta")
- return colors[zlib.crc32(s) % len(colors)]
- def report(category, message, fg=None):
- if fg is None:
- fg = color_for_string(category)
- click.echo(
- "[%s] %s: %s"
- % (six.text_type(datetime.utcnow()).split(".")[0], click.style(category, fg=fg), message)
- )
- def launch_redis():
- report("redis", "Launching redis server")
- cl = Popen(["redis-server", "-"], stdin=PIPE, stdout=open(os.devnull, "r+"))
- cl.stdin.write(
- """
- port %(port)s
- databases %(databases)d
- save ""
- """
- % {"port": six.text_type(settings.SENTRY_APIDOCS_REDIS_PORT), "databases": 4}
- )
- cl.stdin.flush()
- cl.stdin.close()
- return cl
- def spawn_sentry():
- report("sentry", "Launching sentry server")
- cl = Popen(
- [
- "sentry",
- "--config=" + SENTRY_CONFIG,
- "run",
- "web",
- "-w",
- "1",
- "--bind",
- "127.0.0.1:%s" % settings.SENTRY_APIDOCS_WEB_PORT,
- ]
- )
- return cl
- def init_db():
- drop_db()
- report("db", "Migrating database (this can take some time)")
- call_command("syncdb", migrate=True, interactive=False, traceback=True, verbosity=0)
- def drop_db():
- report("db", "Dropping database")
- config = settings.DATABASES["default"]
- check_output(["dropdb", "-U", config["USER"], "-h", config["HOST"], config["NAME"]])
- check_output(["createdb", "-U", config["USER"], "-h", config["HOST"], config["NAME"]])
- class SentryBox(object):
- def __init__(self):
- self.redis = None
- self.sentry = None
- self.task_runner = None
- def __enter__(self):
- self.redis = launch_redis()
- self.sentry = spawn_sentry()
- init_db()
- return self
- def __exit__(self, exc_type, exc_value, tb):
- if self.sentry is not None:
- report("sentry", "Shutting down sentry server")
- self.sentry.kill()
- self.sentry.wait()
- if self.redis is not None:
- report("redis", "Stopping redis server")
- self.redis.kill()
- self.redis.wait()
- def run_scenario(vars, scenario_ident, func):
- runner = Runner(scenario_ident, func, **vars)
- report("scenario", 'Running scenario "%s"' % scenario_ident)
- func(runner)
- return runner.to_json()
- @click.command()
- @click.option("--output-path", type=click.Path())
- @click.option("--output-format", type=click.Choice(["json", "markdown", "both"]), default="both")
- def cli(output_path, output_format):
- """API docs dummy generator."""
- global OUTPUT_PATH
- if output_path is not None:
- OUTPUT_PATH = os.path.abspath(output_path)
- with SentryBox():
- utils = MockUtils()
- report("org", "Creating user and organization")
- user = utils.create_user("john@interstellar.invalid")
- org = utils.create_org("The Interstellar Jurisdiction", owner=user)
- report("auth", "Creating api token")
- api_token = utils.create_api_token(user)
- report("org", "Creating team")
- team = utils.create_team("Powerful Abolitionist", org=org)
- utils.join_team(team, user)
- projects = []
- for project_name in "Pump Station", "Prime Mover":
- report("project", 'Creating project "%s"' % project_name)
- project = utils.create_project(project_name, teams=[team], org=org)
- release = utils.create_release(project=project, user=user)
- report("event", 'Creating event for "%s"' % project_name)
- event1 = utils.create_event(project=project, release=release, platform="python")
- event2 = utils.create_event(project=project, release=release, platform="java")
- projects.append({"project": project, "release": release, "events": [event1, event2]})
- vars = {
- "org": org,
- "me": user,
- "api_token": api_token,
- "teams": [{"team": team, "projects": projects}],
- }
- scenario_map = {}
- report("docs", "Collecting scenarios")
- for scenario_ident, func in iter_scenarios():
- scenario = run_scenario(vars, scenario_ident, func)
- scenario_map[scenario_ident] = scenario
- section_mapping = {}
- report("docs", "Collecting endpoint documentation")
- for endpoint in iter_endpoints():
- report("endpoint", 'Collecting docs for "%s"' % endpoint["endpoint_name"])
- section_mapping.setdefault(endpoint["section"], []).append(endpoint)
- sections = get_sections()
- if output_format in ("json", "both"):
- output_json(sections, scenario_map, section_mapping)
- if output_format in ("markdown", "both"):
- output_markdown(sections, scenario_map, section_mapping)
- def output_json(sections, scenarios, section_mapping):
- report("docs", "Generating JSON documents")
- for id, scenario in scenarios.items():
- dump_json("scenarios/%s.json" % id, scenario)
- section_listings = {}
- for section, title in sections.items():
- entries = {}
- for endpoint in section_mapping.get(section, []):
- entries[endpoint["endpoint_name"]] = endpoint["title"]
- dump_json("endpoints/%s.json" % endpoint["endpoint_name"], endpoint)
- section_listings[section] = {"title": title, "entries": entries}
- dump_json("sections.json", {"sections": section_listings})
- def output_markdown(sections, scenarios, section_mapping):
- report("docs", "Generating markdown documents")
- for section, title in sections.items():
- i = 0
- links = []
- for endpoint in section_mapping.get(section, []):
- i += 1
- path = u"{}/{}.md".format(section, endpoint["endpoint_name"])
- auth = ""
- if len(endpoint["params"].get("auth", [])):
- auth = endpoint["params"]["auth"][0]["description"]
- payload = dict(
- title=endpoint["title"],
- sidebar_order=i,
- description="\n".join(endpoint["text"]).strip(),
- warning=endpoint["warning"],
- method=endpoint["method"],
- api_path=endpoint["path"],
- query_parameters=endpoint["params"].get("query"),
- path_parameters=endpoint["params"].get("path"),
- parameters=endpoint["params"].get("param"),
- authentication=auth,
- example_request=format_request(endpoint, scenarios),
- example_response=format_response(endpoint, scenarios),
- )
- dump_markdown(path, payload)
- links.append({"title": endpoint["title"], "path": path})
- dump_index_markdown(section, title, links)
- def dump_json(path, data):
- path = os.path.join(OUTPUT_PATH, "json", path)
- try:
- os.makedirs(os.path.dirname(path))
- except OSError:
- pass
- with open(path, "w") as f:
- for line in json.dumps(data, indent=2, sort_keys=True).splitlines():
- f.write(line.rstrip() + "\n")
- def dump_index_markdown(section, title, links):
- path = os.path.join(OUTPUT_PATH, "markdown", section, "index.md")
- try:
- os.makedirs(os.path.dirname(path))
- except OSError:
- pass
- with open(path, "w") as f:
- contents = render_to_string("sentry/apidocs/index.md", dict(title=title, links=links))
- f.write(contents)
- def dump_markdown(path, data):
- path = os.path.join(OUTPUT_PATH, "markdown", path)
- try:
- os.makedirs(os.path.dirname(path))
- except OSError:
- pass
- with open(path, "w") as f:
- template = u"""---
- # This file is automatically generated from the API using `sentry/api-docs/generator.py.`
- # Do not manually edit this file.
- {}
- ---
- """
- contents = template.format(json.dumps(data, sort_keys=True, indent=2))
- f.write(contents)
- def find_first_scenario(endpoint, scenario_map):
- for scene in endpoint["scenarios"]:
- if scene not in scenario_map:
- continue
- try:
- return scenario_map[scene]["requests"][0]
- except IndexError:
- return None
- return None
- def format_request(endpoint, scenario_map):
- scene = find_first_scenario(endpoint, scenario_map)
- if not scene:
- return ""
- request = scene["request"]
- lines = [
- u"{} {} HTTP/1.1".format(request["method"], request["path"]),
- "Host: sentry.io",
- "Authorization: Bearer <token>",
- ]
- lines.extend(format_headers(request["headers"]))
- if request["data"]:
- lines.append("")
- lines.append(json.dumps(request["data"], sort_keys=True, indent=2))
- return "\n".join(lines)
- def format_response(endpoint, scenario_map):
- scene = find_first_scenario(endpoint, scenario_map)
- if not scene:
- return ""
- response = scene["response"]
- lines = [u"HTTP/1.1 {} {}".format(response["status"], response["reason"])]
- lines.extend(format_headers(response["headers"]))
- if response["data"]:
- lines.append("")
- lines.append(json.dumps(response["data"], sort_keys=True, indent=2))
- return "\n".join(lines)
- def format_headers(headers):
- """Format headers into a list."""
- return [u"{}: {}".format(key, value) for key, value in headers.items()]
- if __name__ == "__main__":
- cli()
|