9e69a18e4476f4a7bbba56d9a5f16bbdfbdd5d50
[~helmut/crossqa.git] / webapp.py
1 #!/usr/bin/python3
2
3 import datetime
4 import lzma
5
6 import apt_pkg
7 apt_pkg.init()
8 version_compare = apt_pkg.version_compare
9
10 import flask
11 import flask_sqlalchemy
12 import jinja2
13 import sqlalchemy
14 import werkzeug
15
16 app = flask.Flask("crossqa")
17 app.config["SQLALCHEMY_DATABASE_URI"] = 'sqlite:///db'
18 app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
19 db = flask_sqlalchemy.SQLAlchemy(app)
20
21 src_template = """<!DOCTYPE html>
22 <html>
23  <head>
24  </head>
25  <body>
26   <title>{{ sourcepackage|e }} - Debian cross build</title>
27   <h1>{{ sourcepackage|e }}</h1>
28   <h3>Cross satisfiability</h3>
29   <table>
30    <tr>
31     <th>state</th>
32     <th>architectures</th>
33    </tr>
34    {%- set okarchs = depresult.pop(None, None) -%}
35    {%- for reason, archs in depresult.items()|sort -%}
36     <tr>
37      <td>{{ reason|e }}</td>
38      <td>{{ archs|arch_format }}</td>
39     </tr>
40    {%- endfor -%}
41    {%- if okarchs -%}
42     <tr>
43      <td>ok</td>
44      <td>{{ okarchs|arch_format }}</td>
45     </tr>
46    {%- endif -%}
47   </table>
48   <h5>See also</h5>
49   <ul>
50    {%- if show_bootstrapdn -%}
51     <li>
52      <a href="https://bootstrap.debian.net/cross_all/{{ sourcepackage|e }}.html">bootstrap.debian.net</a>
53     </li>
54    {%- endif -%}
55    <li>
56     <a href="https://qa.debian.org/dose/debcheck/cross_unstable_main_amd64/">debcheck</a>
57    </li>
58   </ul>
59   {%- if builds -%}
60    <h3>Cross builds</h3>
61    <table>
62     <tr>
63      <th>started</th>
64      <th>version</th>
65      <th>architecture</th>
66      <th>result</th>
67     </tr>
68     {%- for build in builds|sort(attribute='starttime', reverse=true) -%}
69      <tr>
70       <td>
71        <span title="{{ build.starttime|sqltimestamp }}">
72         {{- build.starttime|sqltimestamp|formatts -}}
73        </span>
74       </td>
75       <td>{{ build.version|e }}</td>
76       <td>{{ build.architecture|e }}</td>
77       <td>
78        <a href="{{ url_for("show_log", filename=build.filename[:-3]) }}">
79         {{- "ok" if build.success else "failed" -}}
80        </a>
81        <a href="{{ url_for("show_log", filename=build.filename) }}">xz</a>
82       </td>
83      </tr>
84     {%- endfor -%}
85    </table>
86   {%- endif -%}
87  </body>
88 </html>
89 """
90
91 @app.template_filter("sqltimestamp")
92 def sqltimestamp_filter(s):
93     try:
94         return datetime.datetime.strptime(s, "%Y-%m-%d %H:%M:%S.%f")
95     except ValueError:
96         return datetime.datetime.strptime(s, "%Y-%m-%d %H:%M:%S")
97
98 @app.template_filter("formatts")
99 def formatts_filter(ts):
100     assert isinstance(ts, datetime.datetime)
101     dt = datetime.datetime.utcnow() - ts
102     if dt < datetime.timedelta(seconds=1):
103         return "now"
104     if dt < datetime.timedelta(seconds=100):
105         return "%d s" % dt.seconds
106     if dt < datetime.timedelta(minutes=100):
107         return "%d m" % (dt.seconds // 60)
108     if dt < datetime.timedelta(days=1):
109         return "%d h" % (dt.seconds // (60 * 60))
110     return "%d d" % dt.days
111
112 @app.template_filter('arch_format')
113 @jinja2.contextfilter
114 def arch_format_filter(context, some_archs):
115     if context["architectures"] == some_archs:
116         return "any"
117     return ", ".join(sorted(some_archs))
118
119 def collect_depstate(conn, source):
120     version = None
121     depstate = None
122     query = sqlalchemy.text("""
123         SELECT version, architecture, satisfiable, reason
124             FROM depstate WHERE source = :source;""")
125     for row in conn.execute(query, source=source):
126         if version is None or version_compare(version, row.version) > 0:
127             version = row.version
128             depstate = {}
129         depstate[row.architecture] = None if row.satisfiable else row.reason
130     if version is None:
131         raise werkzeug.exceptions.NotFound()
132     depresult = {}
133     for arch, reason in depstate.items():
134         depresult.setdefault(reason, set()).add(arch)
135     return version, depresult
136
137 @app.route("/src/<source>")
138 def show_source(source):
139     context = dict(sourcepackage=source)
140     with db.engine.connect() as conn:
141         query = sqlalchemy.text("SELECT architecture FROM depcheck;")
142         context["architectures"] = set(row[0] for row in conn.execute(query))
143         context["version"], context["depresult"] = collect_depstate(conn,
144                                                                     source)
145         query = sqlalchemy.text("""
146             SELECT version, architecture, success, starttime, filename
147                 FROM builds WHERE source = :source;""")
148         context["builds"] = list(conn.execute(query, source=source))
149         context["show_bootstrapdn"] = \
150                 any(reason and not reason.startswith("skew ")
151                     for reason in context["depresult"].keys())
152     return flask.render_template_string(src_template, **context)
153
154 @app.route("/build/<path:filename>")
155 def show_log(filename):
156     if filename.endswith(".xz"):
157         return flask.send_from_directory("logs", filename,
158                                          mimetype="application/octet-stream")
159     filename += ".xz"
160     return flask.send_file(lzma.open(flask.safe_join("logs", filename), "rb"),
161                            mimetype="text/plain")