Merge lp:~rharding/charmworld/total-stats into lp:~juju-jitsu/charmworld/trunk

Proposed by Richard Harding
Status: Merged
Approved by: Curtis Hovey
Approved revision: 291
Merged at revision: 290
Proposed branch: lp:~rharding/charmworld/total-stats
Merge into: lp:~juju-jitsu/charmworld/trunk
Diff against target: 424 lines (+112/-33)
15 files modified
charmworld/charmstore.py (+12/-6)
charmworld/jobs/ingest.py (+3/-1)
charmworld/jobs/tests/test_cstat.py (+6/-1)
charmworld/models.py (+8/-0)
charmworld/search.py (+3/-4)
charmworld/testing/factory.py (+5/-3)
charmworld/tests/test_charmstore.py (+16/-1)
charmworld/tests/test_models.py (+9/-0)
charmworld/tests/test_search.py (+4/-4)
charmworld/views/api.py (+2/-0)
charmworld/views/tests/test_api.py (+15/-6)
charmworld/views/tests/test_charms.py (+3/-3)
charmworld/views/tests/test_search.py (+1/-1)
docs/api.rst (+7/-3)
migrations/versions/009_add_downloads_attribute.py (+18/-0)
To merge this branch: bzr merge lp:~rharding/charmworld/total-stats
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+172356@code.launchpad.net

Commit message

Add support for a total downloads attribute.

Description of the change

Add support for a total downloads attribute.

- Ingest pulls the total downloads as well as the last 30 days
- Model and tests are updated to deal with the new attribute
- A migration is put into place to init the new attribute = to the last 30 days count until next ingest runs
- Sort is updated to use the new attribute for determining most popular vs the old 30 day count.

To post a comment you must log in.
lp:~rharding/charmworld/total-stats updated
291. By Richard Harding

Fix lint errors

Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you. I think abentley must reconcile the new attr with this forthcoming branch that documents DISTRO_CHARM_DATA in charmworld/views/tests/test_charms.py. This is good to land now.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'charmworld/charmstore.py'
2--- charmworld/charmstore.py 2013-04-22 14:55:43 +0000
3+++ charmworld/charmstore.py 2013-07-01 16:01:33 +0000
4@@ -46,7 +46,7 @@
5 return date.strftime('%Y-%m-%d')
6
7 @classmethod
8- def get_stats_range_request(cls, charm, start, end):
9+ def get_stats_range_request(cls, charm, start=None, end=None):
10 """Produce a Request for retrieving download counts in a range.
11
12 :param charm: The charm to get information about.
13@@ -56,13 +56,19 @@
14 elements = ['charm-bundle', charm['series'], charm['name']]
15 if charm['store_url'].startswith('cs:~'):
16 elements.append(charm['owner'])
17- return Request(
18- 'GET', '%s/%s' % (cls.STAT_URL, ':'.join(elements)), params={
19+ params = {
20+ 'format': 'json',
21+ }
22+ if (start and end):
23+ params.update({
24 'start': cls.date_str(start),
25 'end': cls.date_str(end),
26 'by': 'day',
27- 'format': 'json',
28- }
29+ })
30+
31+ return Request(
32+ 'GET', '%s/%s' % (cls.STAT_URL, ':'.join(elements)),
33+ params=params
34 )
35
36 def get_charm_info(self, address):
37@@ -72,7 +78,7 @@
38 data['address'] = address
39 return data
40
41- def get_download_counts(self, charm, start, end):
42+ def get_download_counts(self, charm, start=None, end=None):
43 """Retrieve download counts for each day in the specified range.
44
45 :param charm: The charm to get information about.
46
47=== modified file 'charmworld/jobs/ingest.py'
48--- charmworld/jobs/ingest.py 2013-06-27 14:17:46 +0000
49+++ charmworld/jobs/ingest.py 2013-07-01 16:01:33 +0000
50@@ -336,7 +336,7 @@
51 charm['test_results'][p] = result_id
52 except:
53 log.exception("Unknown error while processing %s %s",
54- charm['branch_spec'], p)
55+ charm['branch_spec'], p)
56
57
58 def scan_artifacts(provider, charm, result):
59@@ -602,6 +602,8 @@
60 def update_download_count(store, charm):
61 count = store.count_downloads_in_days(charm, 30, date.today())
62 charm['downloads_in_past_30_days'] = count
63+ count = store.get_download_counts(charm)
64+ charm['downloads'] = count[0][0] if count else 0
65
66
67 def update_date_created(charm, log):
68
69=== modified file 'charmworld/jobs/tests/test_cstat.py'
70--- charmworld/jobs/tests/test_cstat.py 2013-06-17 15:02:59 +0000
71+++ charmworld/jobs/tests/test_cstat.py 2013-07-01 16:01:33 +0000
72@@ -32,15 +32,20 @@
73 @staticmethod
74 def send(request):
75 response = Response()
76- response._content = '[["2010-12-24", 4], ["2010-12-25", 1]]'
77+ if ('start' in request.url):
78+ response._content = '[["2010-12-24", 4], ["2010-12-25", 1]]'
79+ else:
80+ response._content = '[[25]]'
81 return response
82
83 store.session = FakeSession
84 update_counts(CharmSource(self.db, index_client), store, log)
85 db_charm = self.db.charms.find_one({'_id': charm_id})
86 self.assertEqual(5, db_charm['downloads_in_past_30_days'])
87+ self.assertEqual(25, db_charm['downloads'])
88 index_charm = index_client.get(charm_id)
89 self.assertEqual(5, index_charm['downloads_in_past_30_days'])
90+ self.assertEqual(25, index_charm['downloads'])
91
92 def test_update_no_index(self):
93 charm_id, charm = factory.makeCharm(self.db)
94
95=== modified file 'charmworld/models.py'
96--- charmworld/models.py 2013-06-27 09:25:59 +0000
97+++ charmworld/models.py 2013-07-01 16:01:33 +0000
98@@ -213,6 +213,7 @@
99 'files': {},
100
101 # Provided by the charm store job.
102+ 'downloads': 0,
103 'downloads_in_past_30_days': 0,
104 'store_data': {},
105 'store_url': '',
106@@ -415,6 +416,13 @@
107 return self._representation['store_data']
108
109 @property
110+ def downloads(self):
111+ """The number of times the charm was downloaded.
112+
113+ The number comes from the store. It does not equate to deploys."""
114+ return self._representation['downloads']
115+
116+ @property
117 def downloads_in_past_30_days(self):
118 """The number of times the charm was downloaded in the past 30 days.
119
120
121=== modified file 'charmworld/search.py'
122--- charmworld/search.py 2013-06-27 09:25:59 +0000
123+++ charmworld/search.py 2013-07-01 16:01:33 +0000
124@@ -287,9 +287,8 @@
125 :param valid_charms_only: If True, only charms that were successfully
126 parsed are returned.
127 :param sort: If None, charms are sorted by score. If 'downloaded',
128- charms are in order of most to least downloaded in the past 30
129- days. If 'new', charms are in order of most-recently to
130- least-recently created.
131+ charms are in order of most to least downloaded. If 'new', charms
132+ are in order of most-recently to least-recently created.
133 """
134 if filters is None:
135 filters = {}
136@@ -298,7 +297,7 @@
137 dsl = self._get_filtered(dsl, filters, type_, valid_charms_only)
138 if sort == 'downloaded':
139 dsl['sort'] = [{
140- 'downloads_in_past_30_days': {'order': 'desc'},
141+ 'downloads': {'order': 'desc'},
142 }]
143 elif sort == 'new':
144 dsl['sort'] = [{
145
146=== modified file 'charmworld/testing/factory.py'
147--- charmworld/testing/factory.py 2013-06-21 20:45:40 +0000
148+++ charmworld/testing/factory.py 2013-07-01 16:01:33 +0000
149@@ -141,9 +141,10 @@
150 summary=None, revno=12, maintainer=None,
151 commit_message='maintainer', provides=None, requires=None,
152 options=None, files=None, charm_error=False,
153- date_created=None, downloads_in_past_30_days=0,
154- is_featured=False, promulgated=False, categories=None,
155- branch_deleted=False, proof=None, payload=None):
156+ date_created=None, downloads=0,
157+ downloads_in_past_30_days=0, is_featured=False,
158+ promulgated=False, categories=None, branch_deleted=False,
159+ proof=None, payload=None):
160 """Return the json of a charm."""
161 if not description:
162 description = """byobu-class provides remote terminal access through
163@@ -253,6 +254,7 @@
164 'options': options,
165 },
166 'files': files,
167+ 'downloads': downloads,
168 'downloads_in_past_30_days': downloads_in_past_30_days,
169 'date_created': date_created.isoformat(),
170 })
171
172=== modified file 'charmworld/tests/test_charmstore.py'
173--- charmworld/tests/test_charmstore.py 2013-04-10 23:01:10 +0000
174+++ charmworld/tests/test_charmstore.py 2013-07-01 16:01:33 +0000
175@@ -1,4 +1,4 @@
176-# Copyright 2013 Canonical Ltd. This software is licensed under the
177+
178 # GNU Affero General Public License version 3 (see the file LICENSE).
179
180 __metaclass__ = type
181@@ -48,6 +48,21 @@
182 'https://store.juju.ubuntu.com/stats/counter/charm-bundle:foo:bar',
183 url)
184
185+ def test_get_stats_range_request_wo_dates(self):
186+ """The start and end dates are optional
187+
188+ It will fetch total counts without the start and end dates.
189+
190+ """
191+ charm = factory.get_charm_json(series='foo', name='bar', owner='baz')
192+ cs = CharmStore()
193+ req = cs.get_stats_range_request(charm)
194+ self.assertEqual('GET', req.method)
195+ self.assertStoreURL('/charm-bundle:foo:bar:baz', req.url)
196+ self.assertEqual({
197+ 'format': 'json'
198+ }, req.params)
199+
200 def test_get_download_counts(self):
201 charm = factory.get_charm_json(series='foo', name='bar', owner='baz')
202 start = date(2011, 01, 02)
203
204=== modified file 'charmworld/tests/test_models.py'
205--- charmworld/tests/test_models.py 2013-06-27 15:16:03 +0000
206+++ charmworld/tests/test_models.py 2013-07-01 16:01:33 +0000
207@@ -192,6 +192,15 @@
208 charm = Charm({})
209 self.assertEquals({}, charm.files)
210
211+ def test_downloads(self):
212+ """The downloads property is an int."""
213+ charm_data = {'downloads': 30}
214+ charm = Charm(charm_data)
215+ self.assertEqual(30, charm.downloads)
216+ # The default is zero.
217+ charm = Charm({})
218+ self.assertEquals(0, charm.downloads)
219+
220 def test_downloads_in_past_30_days(self):
221 # The downloads_in_past_30_days property is an int.
222 charm_data = {'downloads_in_past_30_days': 30}
223
224=== modified file 'charmworld/tests/test_search.py'
225--- charmworld/tests/test_search.py 2013-06-27 15:32:30 +0000
226+++ charmworld/tests/test_search.py 2013-07-01 16:01:33 +0000
227@@ -164,7 +164,7 @@
228 self.index_client.index_charm(charm)
229 for key, query in items:
230 # Skip ints and lists.
231- if key in ('revision', 'downloads_in_past_30_days'):
232+ if key in ('revision', 'downloads_in_past_30_days', 'downloads'):
233 continue
234 elif key in ('hooks', 'i_requires', 'i_provides', 'categories'):
235 query = query[0]
236@@ -433,9 +433,9 @@
237
238 def test_search_charm_sort_most_downloaded(self):
239 """Specifying 'downloaded' sorts results appropriately."""
240- self.makeCharm(name='popular1', downloads_in_past_30_days=3)
241- self.makeCharm(name='popular2', downloads_in_past_30_days=2)
242- self.makeCharm(name='popular3', downloads_in_past_30_days=1)
243+ self.makeCharm(name='popular1', downloads=3)
244+ self.makeCharm(name='popular2', downloads=2)
245+ self.makeCharm(name='popular3', downloads=1)
246 order123 = ['popular1', 'popular2', 'popular3']
247 result = self.index_client.api_search()
248 self.assertNotEqual(order123, [r['name'] for r in result])
249
250=== modified file 'charmworld/views/api.py'
251--- charmworld/views/api.py 2013-06-18 19:40:55 +0000
252+++ charmworld/views/api.py 2013-07-01 16:01:33 +0000
253@@ -169,6 +169,7 @@
254 'name': 'name',
255 'description': 'description',
256 'owner': 'owner',
257+ 'downloads': 'downloads',
258 'downloads_in_past_30_days': 'downloads_in_past_30_days',
259 'distro_series': 'series',
260 'revision': 'revision',
261@@ -226,6 +227,7 @@
262 'name': charm.name,
263 'has_icon': icon_key in charm.files,
264 'categories': charm.categories,
265+ 'downloads': charm.downloads,
266 'downloads_in_past_30_days': charm.downloads_in_past_30_days,
267 'commits_in_past_30_days': len(charm.changes_since(since)),
268 'weight': weight,
269
270=== modified file 'charmworld/views/tests/test_api.py'
271--- charmworld/views/tests/test_api.py 2013-06-18 19:40:55 +0000
272+++ charmworld/views/tests/test_api.py 2013-07-01 16:01:33 +0000
273@@ -276,7 +276,7 @@
274 for num in range(15):
275 dt = datetime.utcfromtimestamp(0)
276 last_popular_charm = self.makeCharm(
277- downloads_in_past_30_days=num, name='pop%d' % num,
278+ downloads=num, name='pop%d' % num,
279 date_created=dt)[1]
280 for num in range(15):
281 dt = datetime.utcfromtimestamp(0)
282@@ -451,6 +451,7 @@
283 },
284 'files': ['README', 'hooks/install'],
285 'tested_providers': {},
286+ 'downloads': 0,
287 'downloads_in_past_30_days': 0,
288 'is_subordinate': False,
289 },
290@@ -507,6 +508,7 @@
291 'options': self.options2,
292 'files': ['README.md', 'hooks/config-changed'],
293 'tested_providers': {},
294+ 'downloads': 0,
295 'downloads_in_past_30_days': 0,
296 'is_subordinate': True,
297 },
298@@ -628,9 +630,12 @@
299 def test_format_charm_respects_download_count(self):
300 charm = factory.get_charm_json()
301 formatted = self.api_class._format_charm(Charm(charm))
302+ self.assertEqual(0, formatted['downloads'])
303 self.assertEqual(0, formatted['downloads_in_past_30_days'])
304+ charm['downloads'] = 30
305 charm['downloads_in_past_30_days'] = 20
306 formatted = self.api_class._format_charm(Charm(charm))
307+ self.assertEqual(30, formatted['downloads'])
308 self.assertEqual(20, formatted['downloads_in_past_30_days'])
309
310 def test_format_charm_tested_providers(self):
311@@ -954,16 +959,20 @@
312 self.assertIn('charm', result)
313
314 def test_format_related(self):
315- charm = factory.get_charm_json(downloads_in_past_30_days=57)
316+ charm = factory.get_charm_json(
317+ downloads=100,
318+ downloads_in_past_30_days=57)
319 formatted = self.api_class._format_related(charm, 0)
320- self.assertItemsEqual(['id', 'name', 'categories', 'weight',
321- 'has_icon', 'downloads_in_past_30_days',
322- 'commits_in_past_30_days', 'is_approved'],
323- formatted.keys())
324+ self.assertItemsEqual([
325+ 'id', 'name', 'categories', 'weight', 'has_icon', 'downloads',
326+ 'downloads_in_past_30_days', 'commits_in_past_30_days',
327+ 'is_approved'
328+ ], formatted.keys())
329 self.assertEqual(
330 self.api_class._get_api_id(Charm(charm)), formatted['id'])
331 self.assertEqual(charm['name'], formatted['name'])
332 self.assertEqual(charm['categories'], formatted['categories'])
333+ self.assertEqual(charm['downloads'], 100)
334 self.assertEqual(charm['downloads_in_past_30_days'], 57)
335
336 def test_format_related_is_approved(self):
337
338=== modified file 'charmworld/views/tests/test_charms.py'
339--- charmworld/views/tests/test_charms.py 2013-06-25 08:59:50 +0000
340+++ charmworld/views/tests/test_charms.py 2013-07-01 16:01:33 +0000
341@@ -248,9 +248,9 @@
342 # Charms with errors are not returned by interface().
343 providing = Charm(factory.makeCharm(
344 self.db, provides={'foo': {'interface': 'httpX'}})[1])
345- requiring = Charm(factory.makeCharm(
346- self.db, requires={'bar': {'interface': 'httpX'}},
347- charm_error=True)[1])
348+ Charm(factory.makeCharm(
349+ self.db, requires={'bar': {'interface': 'httpX'}},
350+ charm_error=True)[1])
351 request = self.getRequest()
352 request.matchdict = {'interface': 'httpX'}
353 response = interface(request)
354
355=== modified file 'charmworld/views/tests/test_search.py'
356--- charmworld/views/tests/test_search.py 2013-06-25 13:14:10 +0000
357+++ charmworld/views/tests/test_search.py 2013-07-01 16:01:33 +0000
358@@ -11,7 +11,7 @@
359 from charmworld.testing import (
360 factory,
361 ViewTestBase,
362- )
363+)
364 from charmworld.views import search
365
366
367
368=== modified file 'docs/api.rst'
369--- docs/api.rst 2013-06-17 19:26:16 +0000
370+++ docs/api.rst 2013-07-01 16:01:33 +0000
371@@ -69,9 +69,9 @@
372 The ``result`` is a mapping of ``new``, ``featured`` and ``popular`` charms.
373 ``new`` is a list of the 10 most recently-created charms (according to their
374 Launchpad branches), from most to least-recently created. ``popular`` is a
375-list of the 10 most-downloaded charms in the past 30 days, from most to
376-least-downloaded. ``featured`` is a list of all charms that have been manually
377-selected as ``featured``, in unspecified order.
378+list of the 10 most-downloaded charms., from most to least-downloaded.
379+``featured`` is a list of all charms that have been manually selected as
380+``featured``, in unspecified order.
381
382 Retrieving an individual charm
383 ==============================
384@@ -120,6 +120,8 @@
385 },
386 /* The URL of the charm in the store. */
387 "url": "cs:precise/apache2-10",
388+ /* The number of downloads of this charm */
389+ "downloads": 51,
390 /* The number of downloads of this charm in the past 30 days */
391 "downloads_in_past_30_days": 41,
392 /* See Terminology_ */
393@@ -251,6 +253,8 @@
394 "has_icon": true,
395 /* The categories associated with the charm. */
396 "categories": [],
397+ /* The number of downloads of this charm */
398+ "downloads": 51,
399 /* The number of downloads of this charm in the past 30 days */
400 "downloads_in_past_30_days": 41,
401 /* The number of commits to this charm in the past 30 days */
402
403=== added file 'migrations/versions/009_add_downloads_attribute.py'
404--- migrations/versions/009_add_downloads_attribute.py 1970-01-01 00:00:00 +0000
405+++ migrations/versions/009_add_downloads_attribute.py 2013-07-01 16:01:33 +0000
406@@ -0,0 +1,18 @@
407+# Copyright 2013 Canonical Ltd. This software is licensed under the
408+# GNU Affero General Public License version 3 (see the file LICENSE).
409+from charmworld.search import reindex
410+
411+
412+def upgrade(db, index_client):
413+ """Add the downloads attribute to each charm.
414+
415+ Default to using their 30day acount and it'll be updated to be more
416+ accurate on the next injest run.
417+
418+ """
419+ charms = db.charms.find({})
420+ for charm in charms:
421+ charm['downloads'] = charm['downloads_in_past_30_days']
422+ db.charms.save(charm)
423+
424+ reindex(index_client, list(db.charms.find({})))

Subscribers

People subscribed via source and target branches