diff options
Diffstat (limited to 'webapp.py')
-rw-r--r-- | webapp.py | 155 |
1 files changed, 155 insertions, 0 deletions
diff --git a/webapp.py b/webapp.py new file mode 100644 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") |