|
1 #!/usr/bin/env python |
|
2 # |
|
3 # Copyright 2007 Google Inc. |
|
4 # |
|
5 # Licensed under the Apache License, Version 2.0 (the "License"); |
|
6 # you may not use this file except in compliance with the License. |
|
7 # You may obtain a copy of the License at |
|
8 # |
|
9 # http://www.apache.org/licenses/LICENSE-2.0 |
|
10 # |
|
11 # Unless required by applicable law or agreed to in writing, software |
|
12 # distributed under the License is distributed on an "AS IS" BASIS, |
|
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
14 # See the License for the specific language governing permissions and |
|
15 # limitations under the License. |
|
16 # |
|
17 |
|
18 """CGI for displaying info about the currently running app in dev_appserver. |
|
19 |
|
20 This serves pages under /_ah/info/ that display information about the app |
|
21 currently running in the dev_appserver. It currently serves on these URLs: |
|
22 |
|
23 /_ah/info/queries: |
|
24 A list of datastore queries run so far, grouped by kind. Used to suggest |
|
25 composite indices that should be built. |
|
26 |
|
27 /_ah/info/index.yaml: |
|
28 Produces an index.yaml file that can be uploaded to the real app |
|
29 server by appcfg.py. This information is derived from the query |
|
30 history above, by removing queries that don't need any indexes to |
|
31 be built and by combining queries that can use the same index. |
|
32 """ |
|
33 |
|
34 |
|
35 |
|
36 import cgi |
|
37 import wsgiref.handlers |
|
38 |
|
39 from google.appengine.api import apiproxy_stub_map |
|
40 from google.appengine.datastore import datastore_pb |
|
41 from google.appengine.ext import webapp |
|
42 from google.appengine.tools import dev_appserver_index |
|
43 |
|
44 |
|
45 class QueriesHandler(webapp.RequestHandler): |
|
46 """A handler that displays a list of the datastore queries run so far. |
|
47 """ |
|
48 |
|
49 HEADER = """<html> |
|
50 <head><title>Query History</title></head> |
|
51 |
|
52 <body> |
|
53 <h3>Query History</h3> |
|
54 |
|
55 <p>This is a list of datastore queries your app has run. You have to |
|
56 make composite indices for these queries before deploying your app. |
|
57 This is normally done automatically by running dev_appserver, which |
|
58 will write the file index.yaml into your app's root directory, and |
|
59 then deploying your app with appcfg, which will upload that |
|
60 index.yaml.</p> |
|
61 |
|
62 <p>You can also view a 'clean' <a href="index.yaml">index.yaml</a> |
|
63 file and save that to your app's root directory.</p> |
|
64 |
|
65 <table> |
|
66 <tr><th>Times run</th><th>Query</th></tr> |
|
67 """ |
|
68 |
|
69 ROW = """<tr><td>%(count)s</td><td>%(query)s</td></tr>""" |
|
70 |
|
71 FOOTER = """ |
|
72 </table> |
|
73 </body> |
|
74 </html>""" |
|
75 |
|
76 def Render(self): |
|
77 """Renders and returns the query history page HTML. |
|
78 |
|
79 Returns: |
|
80 A string, formatted as an HTML page. |
|
81 """ |
|
82 history = apiproxy_stub_map.apiproxy.GetStub('datastore_v3').QueryHistory() |
|
83 history_items = [(count, query) for query, count in history.items()] |
|
84 history_items.sort(reverse=True) |
|
85 rows = [self.ROW % {'query': _FormatQuery(query), |
|
86 'count': count} |
|
87 for count, query in history_items] |
|
88 return self.HEADER + '\n'.join(rows) + self.FOOTER |
|
89 |
|
90 def get(self): |
|
91 """Handle a GET. Just calls Render().""" |
|
92 self.response.out.write(self.Render()) |
|
93 |
|
94 |
|
95 class IndexYamlHandler(webapp.RequestHandler): |
|
96 """A handler that renders an index.yaml file suitable for upload.""" |
|
97 |
|
98 def Render(self): |
|
99 """Renders and returns the index.yaml file. |
|
100 |
|
101 Returns: |
|
102 A string, formatted as an index.yaml file. |
|
103 """ |
|
104 datastore_stub = apiproxy_stub_map.apiproxy.GetStub('datastore_v3') |
|
105 query_history = datastore_stub.QueryHistory() |
|
106 body = dev_appserver_index.GenerateIndexFromHistory(query_history) |
|
107 return 'indexes:\n' + body |
|
108 |
|
109 def get(self): |
|
110 """Handle a GET. Just calls Render().""" |
|
111 self.response.headers['Content-Type'] = 'text/plain' |
|
112 self.response.out.write(self.Render()) |
|
113 |
|
114 |
|
115 def _FormatQuery(query): |
|
116 """Format a Query protobuf as (very simple) HTML. |
|
117 |
|
118 Args: |
|
119 query: A datastore_pb.Query instance. |
|
120 |
|
121 Returns: |
|
122 A string containing formatted HTML. This is mostly the output of |
|
123 str(query) with '<' etc. escaped, and '<br>' inserted in front of |
|
124 Order and Filter parts. |
|
125 """ |
|
126 res = cgi.escape(str(query)) |
|
127 res = res.replace('Order', '<br>Order') |
|
128 res = res.replace('Filter', '<br>Filter') |
|
129 return res |
|
130 |
|
131 |
|
132 def _DirectionToString(direction): |
|
133 """Turn a direction enum into a string. |
|
134 |
|
135 Args: |
|
136 direction: ASCENDING or DESCENDING |
|
137 |
|
138 Returns: |
|
139 Either 'asc' or 'descending'. |
|
140 """ |
|
141 if direction == datastore_pb.Query_Order.DESCENDING: |
|
142 return 'descending' |
|
143 else: |
|
144 return 'asc' |
|
145 |
|
146 |
|
147 URL_MAP = { |
|
148 '/_ah/info/queries': QueriesHandler, |
|
149 '/_ah/info/index.yaml': IndexYamlHandler, |
|
150 |
|
151 } |
|
152 |
|
153 |
|
154 def main(): |
|
155 application = webapp.WSGIApplication(URL_MAP.items()) |
|
156 wsgiref.handlers.CGIHandler().run(application) |
|
157 |
|
158 |
|
159 if __name__ == '__main__': |
|
160 main() |