Upstream version 8.36.161.0
[platform/framework/web/crosswalk.git] / src / third_party / chromite / appengine / chromiumos-build-stats / stats.py
1 # Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
4
5 import datetime
6 import json
7 import logging
8 import os
9 import re
10
11 from google.appengine.api import datastore_errors
12 from google.appengine.ext import db
13 from google.appengine.api import users
14
15 import webapp2
16 import jinja2
17
18 import model
19
20 # Could replace this with a function if there is ever any reason
21 # to spread entries over multiple datastores.  Consistency is only
22 # gauranteed within a datastore, but access should be limited to
23 # about 1 per second.  That should not be a problem for us.
24 DATASTORE_KEY = db.Key.from_path('Stats', 'default')
25
26 JINJA_ENVIRONMENT = jinja2.Environment(
27     loader=jinja2.FileSystemLoader(os.path.dirname(__file__)),
28     extensions=['jinja2.ext.autoescape'],
29     autoescape=True)
30
31
32 class MainPage(webapp2.RequestHandler):
33   """Provide interface for interacting with DB."""
34
35   # Regex to peel SQL-like SELECT off front, if present, grabbing SELECT args.
36   # Example: "SELECT foo,bar WHERE blah blah"
37   #          ==> group(1)="foo,bar", group(2)="WHERE blah blah"
38   # Example: "SELECT foo , bar"
39   #          ==> group(1)="foo , bar", group(2)=""
40   # Example: "WHERE blah blah"
41   #          ==> No match
42   QUERY_SELECT_PREFIX_RE = re.compile(r'^\s*SELECT\s+'
43                                       r'([^\s,]+(?:\s*,\s*[^\s,]+)*)' # Group 1
44                                       r'(?:$|\s+)(.*)',               # Group 2
45                                       re.IGNORECASE | re.VERBOSE)
46
47   # Regex to determine if WHERE is present, and capture everything after it.
48   # Example: "WHERE foo=bar ORDER BY whatever"
49   #          ==> group(1)="foo=bar ORDER BY whatever"
50   # Example: "ORDER BY whatever"
51   #          ==> No match
52   QUERY_WHERE_PREFIX_RE = re.compile(r'^WHERE\s+(.+)$',
53                                      re.IGNORECASE | re.VERBOSE)
54
55   # Regex to discover ORDER BY columns in order to highlight them in results.
56   QUERY_ORDER_RE = re.compile(r'ORDER\s+BY\s+(\S+)', re.IGNORECASE)
57
58   # Regex to discover LIMIT value in query.
59   QUERY_LIMIT_RE = re.compile(r'LIMIT\s+(\d+)', re.IGNORECASE)
60
61   # Regex for separating tokens by commas, allowing spaces on either side.
62   COMMA_RE = re.compile(r'\s*,\s*')
63
64   # Default columns to show in results table if no SELECT given.
65   DEFAULT_COLUMNS = ['end_date', 'cmd_line', 'run_time', 'board',
66                      'package_count']
67
68   # All possible columns in Statistics model.
69   ALL_COLUMNS = sorted(model.Statistics.properties())
70
71   # Provide example queries in interface as a form of documentation.
72   EXAMPLE_QUERIES = [
73     ("ORDER BY end_date,run_time"
74      " LIMIT 30"),
75     ("WHERE username='mtennant'"
76      " ORDER BY end_date DESC"
77      " LIMIT 30"),
78     ("SELECT end_datetime,cmd_base,cmd_args,run_time,package_count"
79      " WHERE board='amd64-generic'"
80      " ORDER BY end_datetime"
81      " LIMIT 30"),
82     ("SELECT end_date,cmd_base,run_time,board,package_count"
83      " WHERE end_date=DATE('2012-03-28')"
84      " ORDER BY run_time"
85      " LIMIT 30"),
86     ("SELECT end_date,cmd_base,cmd_args,run_time,username"
87      " WHERE run_time>20"
88      " LIMIT 30"),
89     ]
90
91   def get(self):
92     """Support GET to stats page."""
93     # Note that google.com authorization is required to access this page, which
94     # is controlled in app.yaml and on appspot admin page.
95     orig_query = self.request.get('query')
96     logging.debug('Received raw query %r', orig_query)
97
98     # If no LIMIT was provided, default to a LIMIT of 30 for sanity.
99     if not self.QUERY_LIMIT_RE.search(orig_query):
100       orig_query += ' LIMIT 30'
101
102     query = orig_query
103
104     # Peel off "SELECT" clause from front of query.  GCL does not support SELECT
105     # filtering, but we will support it right here to select/filter columns.
106     query, columns = self._RemoveSelectFromQuery(query)
107     if query == orig_query and columns == self.DEFAULT_COLUMNS:
108       # This means there was no SELECT in query.  That is equivalent to
109       # SELECT of default columns, so show that to user.
110       orig_query = 'SELECT %s %s' % (','.join(columns), orig_query)
111
112     # All queries should have the "ancestor" WHERE clause in them, but that
113     # need not be exposed to interface.  Insert the clause intelligently.
114     query = self._AdjustWhereInQuery(query)
115
116     stat_entries = []
117     error_msg = None
118     try:
119       stat_entries = model.Statistics.gql(query, DATASTORE_KEY)
120     except datastore_errors.BadQueryError as ex:
121       error_msg = '<p>%s.</p><p>Actual GCL query used: "%s"</p>' % (ex, query)
122
123     if self.request.get('format') == 'json':
124       # Write output in the JSON format.
125       d = self._ResultsToDictionary(stat_entries, columns)
126
127       class CustomEncoder(json.JSONEncoder):
128         """Handles non-serializable classes by converting them to strings."""
129         def default(self, obj):
130           if (isinstance(obj, datetime.datetime) or
131               isinstance(obj, datetime.date) or
132               isinstance(obj, datetime.time)):
133             return obj.isoformat()
134
135           return json.JSONEncoder.default(self, obj)
136
137       self.response.content_type = 'application/json'
138       self.response.write(json.dumps(d, cls=CustomEncoder))
139     else:
140       # Write output to the HTML page.
141       results_table = self._PrepareResultsTable(stat_entries, columns)
142       template_values = {
143           'error_msg': error_msg,
144           'gcl_query': query,
145           'user_query': orig_query,
146           'user_email': users.get_current_user(),
147           'results_table': results_table,
148           'column_list': self.ALL_COLUMNS,
149           'example_queries': self.EXAMPLE_QUERIES,
150       }
151       template = JINJA_ENVIRONMENT.get_template('index.html')
152       self.response.write(template.render(template_values))
153
154   def _RemoveSelectFromQuery(self, query):
155     """Remove SELECT clause from |query|, return tuple (new_query, columns)."""
156     match = self.QUERY_SELECT_PREFIX_RE.search(query)
157     if match:
158       # A SELECT clause is present.  Remove it but save requested columns.
159       columns = self.COMMA_RE.split(match.group(1))
160       query = match.group(2)
161
162       if columns == ['*']:
163         columns = self.ALL_COLUMNS
164
165       logging.debug('Columns selected for viewing: %s', ', '.join(columns))
166       return query, columns
167     else:
168       logging.debug('Using default columns for viewing: %s',
169                     ', '.join(self.DEFAULT_COLUMNS))
170       return query, self.DEFAULT_COLUMNS
171
172   def _AdjustWhereInQuery(self, query):
173     """Insert WHERE ANCESTOR into |query| and return."""
174     match = self.QUERY_WHERE_PREFIX_RE.search(query)
175     if match:
176       return 'WHERE ANCESTOR IS :1 AND %s' % match.group(1)
177     else:
178       return 'WHERE ANCESTOR IS :1 %s' % query
179
180   def _PrepareResultsTable(self, stat_entries, columns):
181     """Prepare table for |stat_entries| using only |columns|."""
182     # One header blank for row numbers, then each column name.
183     table = [[c for c in [''] + columns]]
184     # Prepare list of table rows, one for each stat entry.
185     for stat_ix, stat_entry in enumerate(stat_entries):
186       row = [stat_ix + 1]
187       row += [getattr(stat_entry, col) for col in columns]
188       table.append(row)
189
190     return table
191
192   def _ResultsToDictionary(self, stat_entries, columns):
193     """Converts |stat_entries| to a dictionary with |columns| as keys.
194
195     Args:
196       stat_entries: A list of GqlQuery objects.
197       columns: A list of keys to use.
198
199     Returns:
200       A dictionary with |columns| as keys.
201     """
202     stats_dict = dict()
203     keys = [c for c in columns]
204     for stat_ix, stat_entry in enumerate(stat_entries):
205       stats_dict[stat_ix] = dict(
206           (col, getattr(stat_entry, col)) for col in columns)
207
208     return stats_dict
209
210
211 class PostPage(webapp2.RequestHandler):
212   """Provides interface for uploading command stats to database."""
213
214   NO_VALUE = '__NO_VALUE_AT_ALL__'
215
216   def post(self):
217     """Support POST of command stats."""
218     logging.info('Stats POST received at %r', self.request.uri)
219
220     new_stat = model.Statistics(parent=DATASTORE_KEY)
221
222     # Check each supported DB property to see if it has a value set
223     # in the POST request.
224     for prop in model.Statistics.properties():
225       # Skip properties with auto_now or auto_now_add enabled.
226       model_prop = getattr(model.Statistics, prop)
227       if ((hasattr(model_prop, 'auto_now_add') and model_prop.auto_now_add) or
228           (hasattr(model_prop, 'auto_now') and model_prop.auto_now)):
229         continue
230
231       # Note that using hasattr with self.request does not work at all.
232       # It (almost) always says the attribute is not present, when getattr
233       # does actually return a value.  Also note that self.request.get is
234       # not returning None as the default value if no explicit default value
235       # is provided, contrary to the spec for dict.get.
236       value = self.request.get(prop, self.NO_VALUE)
237
238       if value is not self.NO_VALUE:
239         # String properties must be 500 characters or less (GQL requirement).
240         if isinstance(model_prop, db.StringProperty) and len(value) > 500:
241           logging.debug('  String property %r too long.  Cutting off at 500'
242                         ' characters.', prop)
243           value = value[:500]
244
245         # Integer properties require casting
246         if isinstance(model_prop, db.IntegerProperty):
247           value = int(value)
248
249         logging.debug('  Stats POST property %r ==> %r', prop, value)
250         setattr(new_stat, prop, value)
251
252     # Use automatically set end_datetime prop to set end_date and end_time.
253     new_stat.end_time = new_stat.end_datetime.time()
254     new_stat.end_date = new_stat.end_datetime.date()
255
256     # Save to model.
257     new_stat.put()