mirror of
https://github.com/reddit-archive/reddit.git
synced 2026-02-11 07:04:54 -05:00
Add api_docs decorator and parameter info. Revamp templates.
Remove OAuth2 api documentation until it lands.
This commit is contained in:
@@ -1,70 +1,142 @@
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from itertools import chain
|
||||
import inspect
|
||||
|
||||
from pylons.i18n import _
|
||||
from reddit_base import RedditController
|
||||
from r2.lib.utils import Storage
|
||||
from r2.lib.pages import BoringPage, ApiHelp
|
||||
|
||||
# API sections displayed in the documentation page.
|
||||
# Each section can have a title and a markdown-formatted description.
|
||||
section_info = {
|
||||
'account': {
|
||||
'title': _('account'),
|
||||
},
|
||||
'flair': {
|
||||
'title': _('flair'),
|
||||
},
|
||||
'links_and_comments': {
|
||||
'title': _('links & comments'),
|
||||
},
|
||||
'messages': {
|
||||
'title': _('private messages'),
|
||||
},
|
||||
'moderation': {
|
||||
'title': _('moderation'),
|
||||
},
|
||||
'misc': {
|
||||
'title': _('misc'),
|
||||
},
|
||||
'listings': {
|
||||
'title': _('listings'),
|
||||
},
|
||||
'search': {
|
||||
'title': _('search'),
|
||||
},
|
||||
'subreddits': {
|
||||
'title': _('subreddits'),
|
||||
},
|
||||
'users': {
|
||||
'title': _('users'),
|
||||
}
|
||||
}
|
||||
|
||||
api_section = Storage((k, k) for k in section_info)
|
||||
|
||||
def api_doc(section, **kwargs):
|
||||
"""
|
||||
Add documentation annotations to the decorated function.
|
||||
|
||||
See ApidocsController.docs_from_controller for a list of annotation fields.
|
||||
"""
|
||||
def add_metadata(api_function):
|
||||
doc = api_function._api_doc = getattr(api_function, '_api_doc', {})
|
||||
if 'extends' in kwargs:
|
||||
kwargs['extends'] = kwargs['extends']._api_doc
|
||||
doc.update(kwargs)
|
||||
doc['section'] = section
|
||||
return api_function
|
||||
return add_metadata
|
||||
|
||||
class ApidocsController(RedditController):
|
||||
@staticmethod
|
||||
def docs_from_controller(controller, url_prefix='/api'):
|
||||
"""
|
||||
Examines a controller for documentation. A dictionary of URLs is
|
||||
returned. For each URL, a dictionary of HTTP methods (GET, POST, etc.)
|
||||
is contained. For each URL/method pair, a dictionary containing the
|
||||
following items is available:
|
||||
Examines a controller for documentation. A dictionary index of
|
||||
sections containing dictionaries of URLs is returned. For each URL, a
|
||||
dictionary of HTTP methods (GET, POST, etc.) is contained. For each
|
||||
URL/method pair, a dictionary containing the following items is
|
||||
available:
|
||||
|
||||
- `__doc__`: Markdown-formatted docstring.
|
||||
- `oauth2_scopes`: List of OAuth2 scopes
|
||||
- *more to come...*
|
||||
- `doc`: Markdown-formatted docstring.
|
||||
- `uri`: Manually-specified URI to list the API method as
|
||||
- `uri_variants`: Alternate URIs to access the API method from
|
||||
- `extensions`: URI extensions the API method supports
|
||||
- `parameters`: Dictionary of possible parameter names and descriptions.
|
||||
- `extends`: API method from which to inherit documentation
|
||||
"""
|
||||
|
||||
api_methods = defaultdict(dict)
|
||||
api_docs = defaultdict(lambda: defaultdict(dict))
|
||||
for name, func in controller.__dict__.iteritems():
|
||||
i = name.find('_')
|
||||
if i > 0:
|
||||
method = name[:i]
|
||||
action = name[i+1:]
|
||||
else:
|
||||
method, sep, action = name.partition('_')
|
||||
if not action:
|
||||
continue
|
||||
|
||||
if func.__doc__ and method in ('GET', 'POST'):
|
||||
docs = {
|
||||
'__doc__': re.sub(r'\n +', '\n', func.__doc__).strip(),
|
||||
}
|
||||
api_doc = getattr(func, '_api_doc', None)
|
||||
if api_doc and 'section' in api_doc and method in ('GET', 'POST'):
|
||||
docs = {}
|
||||
docs['doc'] = inspect.getdoc(func)
|
||||
|
||||
if hasattr(func, 'oauth2_perms'):
|
||||
scopes = func.oauth2_perms.get('allowed_scopes')
|
||||
if scopes:
|
||||
docs['oauth2_scopes'] = scopes
|
||||
if 'extends' in api_doc:
|
||||
docs.update(api_doc['extends'])
|
||||
# parameters are handled separately.
|
||||
docs['parameters'] = {}
|
||||
docs.update(api_doc)
|
||||
|
||||
# TODO: in the future, it would be cool to introspect the
|
||||
# validators in order to generate a list of request
|
||||
# parameters. Some decorators also give a hint as to response
|
||||
# type (JSON, etc.) which could be included as well.
|
||||
uri = docs.get('uri') or '/'.join((url_prefix, action))
|
||||
if 'extensions' in docs:
|
||||
# if only one extension was specified, add it to the URI.
|
||||
if len(docs['extensions']) == 1:
|
||||
uri += '.' + docs['extensions'][0]
|
||||
del docs['extensions']
|
||||
docs['uri'] = uri
|
||||
|
||||
api_methods['/'.join((url_prefix, action))][method] = docs
|
||||
# add every variant to the index -- the templates will filter
|
||||
# out variants in the long-form documentation
|
||||
for variant in chain([uri], docs.get('uri_variants', [])):
|
||||
api_docs[docs['section']][variant][method] = docs
|
||||
|
||||
return api_methods
|
||||
return api_docs
|
||||
|
||||
def GET_docs(self):
|
||||
# controllers to gather docs from.
|
||||
from r2.controllers.api import ApiController, ApiminimalController
|
||||
from r2.controllers.apiv1 import APIv1Controller
|
||||
from r2.controllers.oauth2 import OAuth2FrontendController, OAuth2AccessController, scope_info
|
||||
from r2.controllers.front import FrontController
|
||||
from r2.controllers import listingcontroller
|
||||
|
||||
api_methods = defaultdict(dict)
|
||||
for controller, url_prefix in ((ApiController, '/api'),
|
||||
(ApiminimalController, '/api'),
|
||||
(OAuth2FrontendController, '/api/v1'),
|
||||
(OAuth2AccessController, '/api/v1'),
|
||||
(APIv1Controller, '/api/v1')):
|
||||
for url, methods in self.docs_from_controller(controller, url_prefix).iteritems():
|
||||
api_methods[url].update(methods)
|
||||
api_controllers = [
|
||||
(ApiController, '/api'),
|
||||
(ApiminimalController, '/api'),
|
||||
(FrontController, '')
|
||||
]
|
||||
for name, value in vars(listingcontroller).iteritems():
|
||||
if name.endswith('Controller'):
|
||||
api_controllers.append((value, ''))
|
||||
|
||||
# merge documentation info together.
|
||||
api_docs = defaultdict(dict)
|
||||
for controller, url_prefix in api_controllers:
|
||||
for section, contents in self.docs_from_controller(controller, url_prefix).iteritems():
|
||||
api_docs[section].update(contents)
|
||||
|
||||
return BoringPage(
|
||||
_('api documentation'),
|
||||
content=ApiHelp(
|
||||
api_methods=api_methods,
|
||||
oauth2_scopes=scope_info,
|
||||
)
|
||||
api_docs=api_docs
|
||||
),
|
||||
css_class="api-help",
|
||||
show_sidebar=False,
|
||||
show_firsttext=False
|
||||
).render()
|
||||
|
||||
@@ -464,6 +464,7 @@ def paginated_listing(default_page_size=25, max_page_size=100, backend='sql'):
|
||||
count=VCount('count'),
|
||||
target=VTarget("target"),
|
||||
show=VLength('show', 3))
|
||||
@utils.wraps_api(fn)
|
||||
def new_fn(self, before, **env):
|
||||
if c.render_style == "htmllite":
|
||||
c.link_target = env.get("target")
|
||||
@@ -532,6 +533,7 @@ def require_https():
|
||||
|
||||
def prevent_framing_and_css(allow_cname_frame=False):
|
||||
def wrap(f):
|
||||
@utils.wraps_api(f)
|
||||
def no_funny_business(*args, **kwargs):
|
||||
c.allow_styles = False
|
||||
if not (allow_cname_frame and c.cname and not c.authorized_cname):
|
||||
|
||||
@@ -44,6 +44,7 @@ from datetime import datetime, timedelta
|
||||
from curses.ascii import isprint
|
||||
import re, inspect
|
||||
import pycountry
|
||||
from itertools import chain
|
||||
|
||||
def visible_promo(article):
|
||||
is_promo = getattr(article, "promoted", None) is not None
|
||||
@@ -86,6 +87,12 @@ class Validator(object):
|
||||
|
||||
c.errors.add(error, msg_params = msg_params, field = field)
|
||||
|
||||
def param_docs(self):
|
||||
param_info = {}
|
||||
for param in filter(None, tup(self.param)):
|
||||
param_info[param] = None
|
||||
return param_info
|
||||
|
||||
def __call__(self, url):
|
||||
a = []
|
||||
if self.param:
|
||||
@@ -129,8 +136,17 @@ def _make_validated_kw(fn, simple_vals, param_vals, env):
|
||||
kw[var] = validator(env)
|
||||
return kw
|
||||
|
||||
def set_api_docs(fn, simple_vals, param_vals):
|
||||
doc = fn._api_doc = getattr(fn, '_api_doc', {})
|
||||
param_info = doc.get('parameters', {})
|
||||
for validator in chain(simple_vals, param_vals.itervalues()):
|
||||
param_info.update(validator.param_docs())
|
||||
doc['parameters'] = param_info
|
||||
doc['lineno'] = fn.func_code.co_firstlineno
|
||||
|
||||
def validate(*simple_vals, **param_vals):
|
||||
def val(fn):
|
||||
@utils.wraps_api(fn)
|
||||
def newfn(self, *a, **env):
|
||||
try:
|
||||
kw = _make_validated_kw(fn, simple_vals, param_vals, env)
|
||||
@@ -140,8 +156,7 @@ def validate(*simple_vals, **param_vals):
|
||||
except VerifiedUserRequiredException:
|
||||
return self.intermediate_redirect('/verify')
|
||||
|
||||
newfn.__name__ = fn.__name__
|
||||
newfn.__doc__ = fn.__doc__
|
||||
set_api_docs(newfn, simple_vals, param_vals)
|
||||
return newfn
|
||||
return val
|
||||
|
||||
@@ -157,6 +172,7 @@ def api_validate(response_type=None):
|
||||
def wrap(response_function):
|
||||
def _api_validate(*simple_vals, **param_vals):
|
||||
def val(fn):
|
||||
@utils.wraps_api(fn)
|
||||
def newfn(self, *a, **env):
|
||||
renderstyle = request.params.get("renderstyle")
|
||||
if renderstyle:
|
||||
@@ -187,8 +203,7 @@ def api_validate(response_type=None):
|
||||
responder.send_failure(errors.VERIFIED_USER_REQUIRED)
|
||||
return self.api_wrapper(responder.make_response())
|
||||
|
||||
newfn.__name__ = fn.__name__
|
||||
newfn.__doc__ = fn.__doc__
|
||||
set_api_docs(newfn, simple_vals, param_vals)
|
||||
return newfn
|
||||
return val
|
||||
return _api_validate
|
||||
|
||||
@@ -3787,7 +3787,7 @@ class UserIPHistory(Templated):
|
||||
super(UserIPHistory, self).__init__()
|
||||
|
||||
class ApiHelp(Templated):
|
||||
def __init__(self, api_methods, oauth2_scopes, *a, **kw):
|
||||
self.api_methods = api_methods
|
||||
self.oauth2_scopes = oauth2_scopes
|
||||
api_source_url = "https://github.com/reddit/reddit/blob/master/r2/r2/controllers/api.py"
|
||||
def __init__(self, api_docs, *a, **kw):
|
||||
self.api_docs = api_docs
|
||||
super(ApiHelp, self).__init__(*a, **kw)
|
||||
|
||||
@@ -32,6 +32,7 @@ from BeautifulSoup import BeautifulSoup
|
||||
|
||||
from time import sleep
|
||||
from datetime import datetime, timedelta
|
||||
from functools import wraps, partial, WRAPPER_ASSIGNMENTS
|
||||
from pylons import g
|
||||
from pylons.i18n import ungettext, _
|
||||
from r2.lib.filters import _force_unicode
|
||||
@@ -1384,3 +1385,8 @@ def constant_time_compare(actual, expected):
|
||||
result |= ord(actual[i]) ^ ord(expected[i % expected_len])
|
||||
return result == 0
|
||||
|
||||
def wraps_api(f):
|
||||
# work around issue where wraps() requires attributes to exist
|
||||
if not hasattr(f, '_api_doc'):
|
||||
f._api_doc = {}
|
||||
return wraps(f, assigned=WRAPPER_ASSIGNMENTS+('_api_doc',))
|
||||
|
||||
@@ -5140,3 +5140,179 @@ tr.gold-accent + tr > td {
|
||||
margin-bottom: .5em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.content.api-help {
|
||||
font-size: 1.25em;
|
||||
margin: 0 auto;
|
||||
max-width: 950px;
|
||||
}
|
||||
|
||||
.api-help .contents {
|
||||
padding: 0 20px;
|
||||
margin-left: 24em;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.api-help .contents .section {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.api-help .sidebar {
|
||||
float: left;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.api-help .sidebar .head {
|
||||
position: relative;
|
||||
background: url(../xray-snoo-head.png) top center no-repeat;
|
||||
height: 188px;
|
||||
margin-bottom: -78px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.api-help .sidebar .feet {
|
||||
position: relative;
|
||||
background: url(../xray-snoo-feet.png) top center no-repeat;
|
||||
height: 75px;
|
||||
margin-top: -42px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.api-help .toc {
|
||||
background: #181818 url(../xray-snoo-body.png) center repeat-y;
|
||||
border: 5px solid #959595;
|
||||
border-radius: 8px;
|
||||
padding: 15px 2em 0 2em;
|
||||
width: 18em;
|
||||
}
|
||||
|
||||
.api-help .contents .introduction {
|
||||
position: relative;
|
||||
border: 2px solid #ccc;
|
||||
border-radius: 12px;
|
||||
padding: 14px;
|
||||
margin-bottom: -1em;
|
||||
}
|
||||
|
||||
.api-help .introduction:before,
|
||||
.api-help .introduction:after {
|
||||
position: absolute;
|
||||
display: block;
|
||||
content: '';
|
||||
border: 15px solid;
|
||||
border-style: solid solid outset; /* mitigates firefox drawing a thicker arrow */
|
||||
}
|
||||
|
||||
.api-help .introduction:before {
|
||||
border-color: transparent #ccc transparent transparent;
|
||||
left: -31px;
|
||||
top: 58px;
|
||||
}
|
||||
|
||||
.api-help .introduction:after {
|
||||
border-color: transparent white transparent transparent;
|
||||
left: -28px;
|
||||
top: 58px;
|
||||
}
|
||||
|
||||
.api-help .toc ul {
|
||||
position: relative;
|
||||
margin-top: .5em;
|
||||
margin-bottom: 1.5em;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.api-help .toc > ul > li > strong {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.api-help .toc a.section {
|
||||
color: #888;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.api-help .toc a {
|
||||
color: #8EB0D2;
|
||||
}
|
||||
|
||||
.api-help .toc a:hover, .api-help .endpoint a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.api-help em.placeholder {
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.api-help .toc em.placeholder {
|
||||
color: #8EB0D2;
|
||||
}
|
||||
|
||||
.api-help .endpoint em.placeholder {
|
||||
color: #369;
|
||||
}
|
||||
|
||||
.api-help .endpoint, .api-help .section .description {
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
.api-help .methods h2 {
|
||||
color: black;
|
||||
font-size: 1.45em;
|
||||
text-align: middle;
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 1em;
|
||||
border-bottom: 1px solid #aaa;
|
||||
}
|
||||
|
||||
.api-help .endpoint .info {
|
||||
padding-left: 1em;
|
||||
border-left: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.api-help .endpoint h3, .api-help .endpoint .uri-variants {
|
||||
color: #369;
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
.api-help .endpoint .uri-variants {
|
||||
opacity: .85;
|
||||
font-weight: bold;
|
||||
margin-top: -.5em;
|
||||
margin-left: 3em;
|
||||
}
|
||||
|
||||
.api-help .endpoint .method, .api-help .endpoint .extensions {
|
||||
font-weight: normal;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.api-help .endpoint .extensions {
|
||||
margin-left: .5em;
|
||||
}
|
||||
|
||||
.api-help .endpoint .links {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.api-help .endpoint .links a {
|
||||
margin-left: .85em;
|
||||
opacity: .45;
|
||||
}
|
||||
|
||||
.api-help .endpoint:hover .links a {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.api-help .parameters {
|
||||
background: #f0f0f0;
|
||||
border-collapse: separate;
|
||||
border-radius: 3px;
|
||||
padding: 5px 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.api-help .parameters .name {
|
||||
font-family: 'Courier New', monospace;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
BIN
r2/r2/public/static/xray-snoo-body.png
Normal file
BIN
r2/r2/public/static/xray-snoo-body.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 236 B |
BIN
r2/r2/public/static/xray-snoo-feet.png
Normal file
BIN
r2/r2/public/static/xray-snoo-feet.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
BIN
r2/r2/public/static/xray-snoo-head.png
Normal file
BIN
r2/r2/public/static/xray-snoo-head.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@@ -1,78 +1,122 @@
|
||||
<%!
|
||||
import re
|
||||
from r2.lib.filters import safemarkdown
|
||||
from r2.controllers.api_docs import section_info
|
||||
%>
|
||||
|
||||
<div class="content apihelp">
|
||||
<div class="introduction">
|
||||
<p>This is the automatically-generated documentation for the Reddit API.</p>
|
||||
<p>It's gathered from the docstrings in the code.</p>
|
||||
<%def name="api_method_id(uri, method)">${method}_${uri.replace('/', '_').strip('_')}</%def>
|
||||
<%def name="api_uri(uri)">${unsafe(re.sub(r'{(\w+)}', r'<em class="placeholder">\1</em>', uri))}</%def>
|
||||
|
||||
<%
|
||||
api = thing.api_docs
|
||||
%>
|
||||
|
||||
<div class="sidebar">
|
||||
<div class="head"></div>
|
||||
<div class="toc">
|
||||
<ul>
|
||||
<li>
|
||||
<strong>API methods</strong>
|
||||
<ul>
|
||||
%for section in sorted(api):
|
||||
<li>
|
||||
<a href="#section_${section}" class="section">${section_info[section]['title']}</a>
|
||||
<ul>
|
||||
%for uri in sorted(api[section]):
|
||||
<% methods = sorted(api[section][uri].keys()) %>
|
||||
<li>
|
||||
<a href="#${api_method_id(uri, methods[0])}">${api_uri(uri)}</a>
|
||||
<span class="gray">(${', '.join(methods)})</span>
|
||||
</li>
|
||||
%endfor
|
||||
</ul>
|
||||
</li>
|
||||
%endfor
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="feet"></div>
|
||||
</div>
|
||||
|
||||
<div class="contents">
|
||||
<h1>Contents</h1>
|
||||
<div class="section introduction">
|
||||
<p>This is automatically-generated documentation for the reddit API. It's gathered from docstrings and annotations in the code.</p>
|
||||
<br/>
|
||||
<p>The reddit API and code are <a href="/code">open source</a>. Found a mistake or interested in helping us improve? Have a gander at <a href="${thing.api_source_url}">api.py</a> and send us a pull request.</p>
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<strong>API methods</strong>
|
||||
<ul>
|
||||
%for uri in sorted(thing.api_methods.keys()):
|
||||
<li>
|
||||
<a href="#api_method_${uri.split('/')[-1]}">${uri}</a>
|
||||
<span class="gray">(${', '.join(sorted(thing.api_methods[uri].keys()))})</span>
|
||||
</li>
|
||||
<div class="section methods">
|
||||
%for section in sorted(api):
|
||||
<h2 id="section_${section}">${section_info[section]['title']}</h2>
|
||||
%if 'description' in section_info[section]:
|
||||
<div class="description">
|
||||
${unsafe(safemarkdown(section_info[section]['description']))}
|
||||
</div>
|
||||
%endif
|
||||
%for uri in sorted(api[section]):
|
||||
%for method in sorted(api[section][uri]):
|
||||
<%
|
||||
docs = api[section][uri][method]
|
||||
# skip uri variants in the index
|
||||
if docs['uri'] != uri:
|
||||
continue
|
||||
|
||||
extends = docs.get('extends')
|
||||
%>
|
||||
<div class="endpoint" id="${api_method_id(uri, method)}">
|
||||
<div class="links">
|
||||
%if docs.get('lineno'):
|
||||
<a href="${thing.api_source_url}#L${docs['lineno']}">view code</a>
|
||||
%endif
|
||||
<a href="#${api_method_id(uri, method)}">#</a>
|
||||
</div>
|
||||
<h3>
|
||||
<span class="method">${method} </span>
|
||||
${api_uri(uri)}
|
||||
%if 'extensions' in docs:
|
||||
<span class="extensions">
|
||||
[ ${' | '.join('.'+extension for extension in docs['extensions'])} ]
|
||||
</span>
|
||||
%endif
|
||||
</h3>
|
||||
%if 'uri_variants' in docs:
|
||||
<ul class="uri-variants">
|
||||
%for variant in docs['uri_variants']:
|
||||
<li id="${api_method_id(variant, method)}">→ ${api_uri(variant)}</li>
|
||||
%endfor
|
||||
</ul>
|
||||
%endif
|
||||
<div class="info">
|
||||
${unsafe(safemarkdown(docs.get('doc')))}
|
||||
<%
|
||||
params = docs.get('parameters')
|
||||
base_params = extends.get('parameters') if extends else None
|
||||
%>
|
||||
%if params or base_params:
|
||||
<table class="parameters">
|
||||
%if params:
|
||||
%for param in sorted(params):
|
||||
<tr>
|
||||
<td class="name">${param}</td>
|
||||
<td class="desc">${params[param]}</td>
|
||||
</tr>
|
||||
%endfor
|
||||
%endif
|
||||
%if base_params:
|
||||
%for param in sorted(base_params):
|
||||
<tr class="base-param">
|
||||
<td class="name">${param}</td>
|
||||
<td class="desc">${base_params[param]}</td>
|
||||
</tr>
|
||||
%endfor
|
||||
%endif
|
||||
</table>
|
||||
%endif
|
||||
</div>
|
||||
</div>
|
||||
%endfor
|
||||
%endfor
|
||||
</ul>
|
||||
<li>
|
||||
<strong>OAuth scopes</strong>
|
||||
<ul>
|
||||
%for scope in sorted(thing.oauth2_scopes.keys()):
|
||||
<li><a href="#oauth2_scope_${scope}">${scope}</a></li>
|
||||
%endfor
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="methods">
|
||||
<h1>API methods</h1>
|
||||
|
||||
%for uri in sorted(thing.api_methods.keys()):
|
||||
<div class="endpoint">
|
||||
<a name="api_method_${uri.split('/')[-1]}"></a>
|
||||
<h2>${uri}</h2>
|
||||
%for method in sorted(thing.api_methods[uri].keys()):
|
||||
<div class="method">
|
||||
<h3>${method}</h3>
|
||||
${unsafe(safemarkdown(thing.api_methods[uri][method]))}
|
||||
</div>
|
||||
%endfor
|
||||
</div>
|
||||
%endfor
|
||||
</div>
|
||||
|
||||
<div class="apihelp scopes">
|
||||
<h1>OAuth scopes</h1>
|
||||
|
||||
<table class="oauth2-scopes">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>id</th>
|
||||
<th>name</th>
|
||||
<th>description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
%for scope in sorted(thing.oauth2_scopes.keys()):
|
||||
<tr>
|
||||
<td class="oauth2-scope-id">
|
||||
<a name="oauth2_scope_${scope}"></a>
|
||||
${scope}
|
||||
</td>
|
||||
<td class="oauth2-scope-name">${thing.oauth2_scopes[scope]['name']}</td>
|
||||
<td class="oauth2-scope-description">${thing.oauth2_scopes[scope]['description']}</td>
|
||||
</tr>
|
||||
%endfor
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user