Page MenuHomeSoftware Heritage

No OneTemporary

diff --git a/swh/web/ui/main.py b/swh/web/ui/main.py
index da6e6178..5c141d26 100644
--- a/swh/web/ui/main.py
+++ b/swh/web/ui/main.py
@@ -1,173 +1,171 @@
# Copyright (C) 2015-2016 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
import logging
import os
from flask import Flask
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from swh.core import config
from swh.web.ui.renderers import urlize_api_links, safe_docstring_display
from swh.web.ui.renderers import revision_id_from_url, highlight_source
from swh.web.ui.renderers import SWHMultiResponse, urlize_header_links
-from swh.web.ui.renderers import escape_author_fields
from swh.storage import get_storage
DEFAULT_CONFIG = {
'storage': ('dict', {
'cls': 'remote',
'args': {
'url': 'http://127.0.0.1:5002/',
},
}),
'log_dir': ('string', '/tmp/swh/log'),
'debug': ('bool', None),
'host': ('string', '127.0.0.1'),
'port': ('int', 6543),
'secret_key': ('string', 'development key'),
'max_log_revs': ('int', 25),
'limiter': ('dict', {
'global_limits': ['60 per minute'],
'headers_enabled': True,
'strategy': 'moving-window',
'storage_uri': 'memory://',
'storage_options': {},
'in_memory_fallback': ['60 per minute'],
}),
}
class SWHFlask(Flask):
"""SWH's flask application.
"""
response_class = SWHMultiResponse
app = SWHFlask(__name__)
app.add_template_filter(urlize_api_links)
app.add_template_filter(urlize_header_links)
app.add_template_filter(safe_docstring_display)
app.add_template_filter(revision_id_from_url)
app.add_template_filter(highlight_source)
-app.add_template_filter(escape_author_fields)
def read_config(config_file):
"""Read the configuration file `config_file`, update the app with
parameters (secret_key, conf) and return the parsed configuration as a
dict"""
conf = config.read(config_file, DEFAULT_CONFIG)
config.prepare_folders(conf, 'log_dir')
conf['storage'] = get_storage(**conf['storage'])
return conf
def load_controllers():
"""Load the controllers for the application.
"""
from swh.web.ui import views, apidoc # flake8: noqa
def rules():
"""Returns rules from the application in dictionary form.
Beware, must be called after swh.web.ui.main.load_controllers funcall.
Returns:
Generator of application's rules.
"""
for rule in app.url_map._rules:
yield {'rule': rule.rule,
'methods': rule.methods,
'endpoint': rule.endpoint}
def storage():
"""Return the current application's storage.
"""
return app.config['conf']['storage']
def prepare_limiter():
"""Prepare Flask Limiter from configuration and App configuration"""
limiter = Limiter(
app,
key_func=get_remote_address,
**app.config['conf']['limiter']
)
app.limiter = limiter
def run_from_webserver(environ, start_response):
"""Run the WSGI app from the webserver, loading the configuration.
Note: This function is called on a per-request basis so beware the side
effects here!
"""
if 'conf' not in app.config:
load_controllers()
config_path = '/etc/softwareheritage/webapp/webapp.yml'
conf = read_config(config_path)
app.secret_key = conf['secret_key']
app.config['conf'] = conf
prepare_limiter()
logging.basicConfig(filename=os.path.join(conf['log_dir'], 'web-ui.log'),
level=logging.INFO)
return app(environ, start_response)
def run_debug_from(config_path, verbose=False):
"""Run the api's server in dev mode.
Note: This is called only once (contrast with the production mode
in run_from_webserver function)
Args:
conf is a dictionary of keywords:
- 'db_url' the db url's access (through psycopg2 format)
- 'content_storage_dir' revisions/directories/contents storage on disk
- 'host' to override the default 127.0.0.1 to open or not the server
to the world
- 'port' to override the default of 5000 (from the underlying layer:
flask)
- 'debug' activate the verbose logs
- 'secret_key' the flask secret key
Returns:
Never
"""
load_controllers()
conf = read_config(config_path)
app.secret_key = conf['secret_key']
app.config['conf'] = conf
host = conf.get('host', '127.0.0.1')
port = conf.get('port')
debug = conf.get('debug')
prepare_limiter()
log_file = os.path.join(conf['log_dir'], 'web-ui.log')
logging.basicConfig(level=logging.DEBUG if verbose else logging.INFO,
handlers=[logging.FileHandler(log_file),
logging.StreamHandler()])
app.run(host=host, port=port, debug=debug)
diff --git a/swh/web/ui/renderers.py b/swh/web/ui/renderers.py
index d85d1345..8a145fc6 100644
--- a/swh/web/ui/renderers.py
+++ b/swh/web/ui/renderers.py
@@ -1,288 +1,284 @@
# Copyright (C) 2015-2017 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
import re
import yaml
import json
from docutils.core import publish_parts
from docutils.writers.html4css1 import Writer, HTMLTranslator
from inspect import cleandoc
from jinja2 import escape, Markup
from flask import request, Response, render_template
from flask import g
from pygments import highlight
from pygments.lexers import guess_lexer
from pygments.formatters import HtmlFormatter
from swh.web.ui import utils
class SWHFilterEnricher():
"""Global filter on fields.
"""
@classmethod
def filter_by_fields(cls, data):
"""Extract a request parameter 'fields' if it exists to permit the
filtering on the data dict's keys.
If such field is not provided, returns the data as is.
"""
fields = request.args.get('fields')
if fields:
fields = set(fields.split(','))
data = utils.filter_field_keys(data, fields)
return data
class SWHComputeLinkHeader:
"""Add link header to response.
Mixin intended to be used for example in SWHMultiResponse
"""
@classmethod
def compute_link_header(cls, rv, options):
"""Add Link header in returned value results.
Expects rv to be a dict with 'results' and 'headers' key:
'results': the returned value expected to be shown
'headers': dictionary with link-next and link-prev
Args:
rv (dict): with keys:
- 'headers': potential headers with 'link-next'
and 'link-prev' keys
- 'results': containing the result to return
options (dict): the initial dict to update with result if any
Returns:
Dict with optional keys 'link-next' and 'link-prev'.
"""
link_headers = []
if 'headers' not in rv:
return {}
rv_headers = rv['headers']
if 'link-next' in rv_headers:
link_headers.append('<%s>; rel="next"' % (
rv_headers['link-next']))
if 'link-prev' in rv_headers:
link_headers.append('<%s>; rel="previous"' % (
rv_headers['link-prev']))
if link_headers:
link_header_str = ','.join(link_headers)
headers = options.get('headers', {})
headers.update({
'Link': link_header_str
})
return headers
return {}
class SWHTransformProcessor:
"""Transform an eventual returned value with multiple layer of
information with only what's necessary.
If the returned value rv contains the 'results' key, this is the
associated value which is returned.
Otherwise, return the initial dict without the potential 'headers'
key.
"""
@classmethod
def transform(cls, rv):
if 'results' in rv:
return rv['results']
if 'headers' in rv:
rv.pop('headers')
return rv
class SWHMultiResponse(Response, SWHFilterEnricher,
SWHComputeLinkHeader, SWHTransformProcessor):
"""
A Flask Response subclass.
Override force_type to transform dict/list responses into callable Flask
response objects whose mimetype matches the request's Accept header: HTML
template render, YAML dump or default to a JSON dump.
"""
@classmethod
def make_response_from_mimetype(cls, rv, options={}):
options = options.copy()
if not (isinstance(rv, list) or isinstance(rv, dict)):
return rv
def wants_html(best_match):
return best_match == 'text/html' and \
request.accept_mimetypes[best_match] > \
request.accept_mimetypes['application/json']
def wants_yaml(best_match):
return best_match == 'application/yaml' and \
request.accept_mimetypes[best_match] > \
request.accept_mimetypes['application/json']
rv = cls.filter_by_fields(rv)
acc_mime = ['application/json', 'application/yaml', 'text/html']
best_match = request.accept_mimetypes.best_match(acc_mime)
options['headers'] = cls.compute_link_header(rv, options)
rv = cls.transform(rv)
if wants_html(best_match):
data = json.dumps(rv, sort_keys=True,
indent=4, separators=(',', ': '))
env = g.get('doc_env', {})
env['response_data'] = data
env['headers_data'] = None
if options and 'headers' in options:
env['headers_data'] = options['headers']
env['request'] = request
rv = Response(render_template('apidoc.html', **env),
content_type='text/html',
**options)
elif wants_yaml(best_match):
rv = Response(
yaml.dump(rv),
content_type='application/yaml',
**options)
else:
# jsonify is unhappy with lists in Flask 0.10.1, use json.dumps
rv = Response(
json.dumps(rv),
content_type='application/json',
**options)
return rv
@classmethod
def force_type(cls, rv, environ=None):
if isinstance(rv, dict) or isinstance(rv, list):
rv = cls.make_response_from_mimetype(rv)
return super().force_type(rv, environ)
def error_response(error_code, error):
"""Private function to create a custom error response.
"""
error_opts = {'status': error_code}
error_data = {'error': str(error)}
return SWHMultiResponse.make_response_from_mimetype(error_data,
options=error_opts)
def urlize_api_links(text):
"""Utility function for decorating api links in browsable api.
Args:
text: whose content matching links should be transformed into
contextual API or Browse html links.
Returns
The text transformed if any link is found.
The text as is otherwise.
"""
return re.sub(r'(/api/.*/|/browse/.*/)',
r'<a href="\1">\1</a>',
str(escape(text)))
-def escape_author_fields(text):
- return re.sub(r'<(.*)>', r'&lt;\1&gt;', text)
-
-
def urlize_header_links(text):
"""Utility function for decorating headers links in browsable api.
Args
text: Text whose content contains Link header value
Returns:
The text transformed with html link if any link is found.
The text as is otherwise.
"""
return re.sub(r'<(/api/.*|/browse/.*)>', r'<<a href="\1">\1</a>>',
text)
class NoHeaderHTMLTranslator(HTMLTranslator):
"""
Docutils translator subclass to customize the generation of HTML
from reST-formatted docstrings
"""
def __init__(self, document):
super().__init__(document)
self.body_prefix = []
self.body_suffix = []
def visit_bullet_list(self, node):
self.context.append((self.compact_simple, self.compact_p))
self.compact_p = None
self.compact_simple = self.is_compactable(node)
self.body.append(self.starttag(node, 'ul', CLASS='docstring'))
DOCSTRING_WRITER = Writer()
DOCSTRING_WRITER.translator_class = NoHeaderHTMLTranslator
def safe_docstring_display(docstring):
"""
Utility function to htmlize reST-formatted documentation in browsable
api.
"""
docstring = cleandoc(docstring)
return publish_parts(docstring, writer=DOCSTRING_WRITER)['html_body']
def revision_id_from_url(url):
"""Utility function to obtain a revision's ID from its browsing URL."""
return re.sub(r'/browse/revision/([0-9a-f]{40}|[0-9a-f]{64})/.*',
r'\1', url)
def highlight_source(source_code_as_text):
"""Leverage pygments to guess and highlight source code.
Args
source_code_as_text (str): source code in plain text
Returns:
Highlighted text if possible or plain text otherwise
"""
try:
maybe_lexer = guess_lexer(source_code_as_text)
if maybe_lexer:
r = highlight(
source_code_as_text, maybe_lexer,
HtmlFormatter(linenos=True,
lineanchors='l',
anchorlinenos=True))
else:
r = '<pre>%s</pre>' % source_code_as_text
except:
r = '<pre>%s</pre>' % source_code_as_text
return Markup(r)
diff --git a/swh/web/ui/templates/apidoc.html b/swh/web/ui/templates/apidoc.html
index 090df060..b543f456 100644
--- a/swh/web/ui/templates/apidoc.html
+++ b/swh/web/ui/templates/apidoc.html
@@ -1,123 +1,123 @@
{% extends "layout.html" %}
{% block title %} API overview {% endblock %}
{% block content %}
<nav class="bread-crumbs">
<ul>
<li><a href="/">API</a></li>
<li>/</li>
<li><a href="/api/1/">endpoints</a></li>
<li>/</li>
<li>{{ request.path }}</li>
</ul>
</nav>
{% if docstring %}
<div class="docstring">
<h2> Description </h2>
{{ docstring | safe_docstring_display | safe }}
</div>
{% endif %}
{% if response_data and response_data is not none %}
<div class="response-data">
<h2> Request </h2>
<pre><strong>{{ request.method }}</strong> {{ request.url }}</pre>
{% if headers_data and headers_data is not none %}
<h2> Headers </h2>
{% for header_name, header_value in headers_data.items() %}
<pre><strong>{{ header_name }}</strong> {{ header_value | urlize_header_links | safe }}</pre>
{% endfor %}
{% endif %}
<h2> Result </h2>
- <pre>{{ response_data | escape_author_fields | urlize_api_links | safe }}</pre>
+ <pre>{{ response_data | urlize_api_links | safe }}</pre>
</div>
{% endif %}
<hr/>
<div class="doc-urls">
<table class="m-x-auto table">
<thead>
<tr>
<th>URL</th>
<th>Allowed Methods</th>
</tr>
</thead>
<tbody>
{% for url in urls %}
<tr>
<td>
{{ url['rule'] }}
</td>
<td>
{{ url['methods'] | sort | join(', ') }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<hr/>
{% if args and args|length > 0 %}
<div class="doc-args">
<h2> Args </h2>
<dl class="doc-argslist dl-horizontal">
{% for arg in args %}
<dt> {{ arg['name'] }}: {{ arg['type'] }} </dt>
<dd> {{ arg['doc'] | safe_docstring_display | safe }} </dd>
{% endfor %}
</dl>
</div>
{% endif %}
{% if params and params|length > 0 %}
<div class="doc-params">
<h2> Params </h2>
<dl class="doc-paramslist dl-horizontal">
{% for param in params %}
<dt> {{ param['name'] }}: string </dt>
<dd> {{ param['doc'] | safe_docstring_display | safe }} </dd>
{% endfor %}
</dl>
</div>
{% endif %}
{% if excs and excs|length > 0 %}
<div class="doc-excs">
<h2> Raises </h2>
<dl class="doc-excslist dl-horizontal">
{% for exc in excs %}
<dt> {{ exc['exc'] }} </dt>
<dd> {{ exc['doc'] | safe_docstring_display | safe }} </dd>
{% endfor %}
</dl>
</div>
{% endif %}
{% if headers %}
<div class="doc-headers">
<h2> Headers </h2>
<dl class="doc-headers dl-horizontal">
{% for header in headers %}
<dt> {{ header['name'] }}: string </dt>
<dd> {{ header['doc'] | safe_docstring_display | safe }} </dd>
{% endfor %}
</dl>
</div>
{% endif %}
{% if return %}
<div class="doc-return">
<h2> Returns </h2>
<dl class="doc-return dl-horizontal">
<dt> {{ return['type'] }} </dt>
<dd> {{ return['doc'] | safe_docstring_display | safe }} </dd>
</dl>
</div>
{% endif %}
{% if examples %}
<div class="doc-example">
<h2> Examples </h2>
<dl class="doc-example dl-horizontal">
{% for example in examples %}
<dd>
<a href="{{ example }}">{{ example }}</a>
</dd>
{% endfor %}
</dl>
</div>
{% endif %}
{% endblock %}

File Metadata

Mime Type
text/x-diff
Expires
Fri, Jul 4, 11:43 AM (3 w, 2 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3253022

Event Timeline