add initial web presentation
authorHelmut Grohne <helmut@subdivi.de>
Tue, 5 Mar 2019 19:15:19 +0000 (20:15 +0100)
committerHelmut Grohne <helmut@subdivi.de>
Tue, 5 Mar 2019 19:15:19 +0000 (20:15 +0100)
webapp.py [new file with mode: 0644]

diff --git a/webapp.py b/webapp.py
new file mode 100644 (file)
index 0000000..cfdf434
--- /dev/null
+++ b/webapp.py
@@ -0,0 +1,155 @@
+#!/usr/bin/python3
+
+import datetime
+import lzma
+
+import apt_pkg
+apt_pkg.init()
+version_compare = apt_pkg.version_compare
+
+import flask
+import flask_sqlalchemy
+import jinja2
+import sqlalchemy
+import werkzeug
+
+app = flask.Flask("crossqa")
+app.config["SQLALCHEMY_DATABASE_URI"] = 'sqlite:///db'
+db = flask_sqlalchemy.SQLAlchemy(app)
+
+src_template = """<!DOCTYPE html>
+<html>
+ <head>
+ </head>
+ <body>
+  <title>{{ sourcepackage|e }} - Debian cross build</title>
+  <h1>{{ sourcepackage|e }}</h1>
+  <h3>Cross satisfiability</h3>
+  <table>
+   <tr>
+    <th>state</th>
+    <th>architectures</th>
+   </tr>
+   {%- set okarchs = depresult.pop(None, None) -%}
+   {%- for reason, archs in depresult.items()|sort -%}
+    <tr>
+     <td>{{ reason|e }}</td>
+     <td>{{ archs|arch_format }}</td>
+    </tr>
+   {%- endfor -%}
+   {%- if okarchs -%}
+    <tr>
+     <td>ok</td>
+     <td>{{ okarchs|arch_format }}</td>
+    </tr>
+   {%- endif -%}
+  </table>
+  <h5>See also</h5>
+  <ul>
+   <li>
+    <a href="https://bootstrap.debian.net/cross_all/{{ sourcepackage|e }}.html">bootstrap.debian.net</a>
+   </li>
+   <li>
+    <a href="https://qa.debian.org/dose/debcheck/cross_unstable_main_amd64/">debcheck</a>
+   </li>
+  </ul>
+  {%- if builds -%}
+   <h3>Cross builds</h3>
+   <table>
+    <tr>
+     <th>started</th>
+     <th>version</th>
+     <th>architecture</th>
+     <th>result</th>
+    </tr>
+    {%- for build in builds|sort(attribute='starttime', reverse=true) -%}
+     <tr>
+      <td>
+       <span title="{{ build.starttime|sqltimestamp }}">
+        {{- build.starttime|sqltimestamp|formatts -}}
+       </span>
+      </td>
+      <td>{{ build.version|e }}</td>
+      <td>{{ build.architecture|e }}</td>
+      <td>
+       <a href="{{ url_for("show_log", filename=build.filename[:-3]) }}">
+        {{- "ok" if build.success else "failed" -}}
+       </a>
+       <a href="{{ url_for("show_log", filename=build.filename) }}">xz</a>
+      </td>
+     </tr>
+    {%- endfor -%}
+   </table>
+  {%- endif -%}
+ </body>
+</html>
+"""
+
+@app.template_filter("sqltimestamp")
+def sqltimestamp_filter(s):
+    return datetime.datetime.strptime(s, "%Y-%m-%d %H:%M:%S.%f")
+
+@app.template_filter("formatts")
+def formatts_filter(ts):
+    assert isinstance(ts, datetime.datetime)
+    dt = datetime.datetime.utcnow() - ts
+    if dt < datetime.timedelta(seconds=1):
+        return "now"
+    if dt < datetime.timedelta(seconds=100):
+        return "%d s" % dt.seconds
+    if dt < datetime.timedelta(minutes=100):
+        return "%d m" % (dt.seconds // 60)
+    if dt < datetime.timedelta(days=1):
+        return "%d h" % (dt.seconds // (60 * 60))
+    return "%d d" % dt.days
+
+@app.template_filter('arch_format')
+@jinja2.contextfilter
+def arch_format_filter(context, some_archs):
+    if context["architectures"] == some_archs:
+        return "any"
+    if len(some_archs) > 3:
+        return jinja2.Markup('<span title="%s">%d architectures</span>' %
+                             (", ".join(sorted(some_archs)), len(some_archs)))
+    return ", ".join(sorted(some_archs))
+
+def collect_depstate(conn, source):
+    version = None
+    depstate = None
+    query = sqlalchemy.text("""
+        SELECT version, architecture, satisfiable, reason
+            FROM depstate WHERE source = :source;""")
+    for row in conn.execute(query, source=source):
+        if version is None or version_compare(version, row.version) > 0:
+            version = row.version
+            depstate = {}
+        depstate[row.architecture] = None if row.satisfiable else row.reason
+    if version is None:
+        raise werkzeug.exceptions.NotFound()
+    depresult = {}
+    for arch, reason in depstate.items():
+        depresult.setdefault(reason, set()).add(arch)
+    return version, depresult
+
+@app.route("/src/<source>")
+def show_source(source):
+    context = dict(sourcepackage=source)
+    with db.engine.connect() as conn:
+        query = sqlalchemy.text("SELECT architecture FROM depcheck;")
+        context["architectures"] = set(row[0] for row in conn.execute(query))
+        context["version"], context["depresult"] = collect_depstate(conn,
+                                                                    source)
+        query = sqlalchemy.text("""
+            SELECT version, architecture, success, starttime, filename
+                FROM builds WHERE source = :source;""")
+        context["builds"] = list(conn.execute(query, source=source))
+    return flask.render_template_string(src_template, **context)
+
+@app.route("/build/<path:filename>")
+def show_log(filename):
+    if filename.endswith(".xz"):
+        return flask.send_from_directory("logs", filename,
+                                         mimetype="application/octet-stream")
+    filename += ".xz"
+    return flask.send_file(lzma.open(flask.safe_join("logs", filename), "rb"),
+                           mimetype="text/plain")