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