Merge lp:~rharding/charmworld/bundle-metadata2 into lp:~juju-jitsu/charmworld/trunk

Proposed by Richard Harding
Status: Merged
Approved by: Richard Harding
Approved revision: 414
Merged at revision: 406
Proposed branch: lp:~rharding/charmworld/bundle-metadata2
Merge into: lp:~juju-jitsu/charmworld/trunk
Diff against target: 383 lines (+232/-8)
5 files modified
charmworld/models.py (+67/-4)
charmworld/testing/factory.py (+1/-1)
charmworld/tests/test_models.py (+118/-1)
charmworld/views/api.py (+22/-0)
charmworld/views/tests/test_api.py (+24/-2)
To merge this branch: bzr merge lp:~rharding/charmworld/bundle-metadata2
Reviewer Review Type Date Requested Status
Juju Gui Bot continuous-integration Approve
Benji York (community) Approve
Review via email: mp+189406@code.launchpad.net

Commit message

Add extra file/charm_metadata support to the bundle details api call.

Description of the change

Add extra file/charm_metadata support to the bundle details api call.

The bundle details view needs to provide metadata for the Gui to properly render the token and information about it. This provides the list of files (to find the README for instance) in the bundle as well as charm details of the charms in the bundle.

The charms are keyed off their keys in the bundle services list so that the gui can match from the name used in the bundle to the charm model in the charm_metadata.

The charms are formatted per the normal charm_details method by casting to a Charm object and then running through _format_charm.

QA:

http://charmworld:2464/api/3/bundle/~bac/wiki/wiki

Should output a new large suite of json for each charm in the bundle. It should also show the list of files so that the front end can implement a hasIcon as per the way gui handles charms. It also allows searching for the README file (README, readme.md, etc) just as we do with the charms in the Gui.

To post a comment you must log in.
Revision history for this message
Benji York (benji) wrote :

The branch looks good. I saw a few little things that deserve your
attention.

Lines 44 and 85 of the diff: The name "data" is somewhat
information-free, maybe bundle_data would be more informative.

Line 45 of the diff: The docstring summery might be better rendered as
"Construct a query that specifies a charm from data about that charm."

Line 47 of the diff: The bulleted lists in the docstring have
inconsistent indentation.

Lines 64 and 77 of the diff: Please capitalize the sentence in the
comment.

Line 71 of the diff: I would be paranoid and pass "/trunk" to the
.endswith() call so a branch name like "who-is-that-in-the-trunk" won't
be matched.

Line 196 of the diff: It would be slightly better to use .assertIsNone().

Line 357 of the diff: There is an extra space after the colon. I think
lint will pick that up, so you should run lint to see if there is more.

Line 205 of the diff: The TestBundleLoadingCharms test case could use a
test that shows what a request with too-little information to find a
charm will produce.

review: Approve
Revision history for this message
Richard Harding (rharding) wrote :

> Lines 44 and 85 of the diff: The name "data" is somewhat
> information-free, maybe bundle_data would be more informative.

Updated. It's actually the services bit from the bundle so went with bundle_charm_info.

> Line 45 of the diff: The docstring summery might be better rendered as
> "Construct a query that specifies a charm from data about that charm."

Updated

> Line 47 of the diff: The bulleted lists in the docstring have
> inconsistent indentation.

Same

> Lines 64 and 77 of the diff: Please capitalize the sentence in the
> comment.

Thanks

> Line 71 of the diff: I would be paranoid and pass "/trunk" to the
> .endswith() call so a branch name like "who-is-that-in-the-trunk" won't
> be matched.

Makes sense.

> Line 196 of the diff: It would be slightly better to use .assertIsNone().

Updated

> Line 357 of the diff: There is an extra space after the colon. I think
> lint will pick that up, so you should run lint to see if there is more.

Thanks, also had the extra line setting the maxdiff to none that lint caught and removed.

> Line 205 of the diff: The TestBundleLoadingCharms test case could use a
> test that shows what a request with too-little information to find a
> charm will produce.

Thanks! This misbehaved and the test caught that and got it adjusted.

Revision history for this message
Juju Gui Bot (juju-gui-bot) wrote :

FAILED: Autolanding.
More details in the following jenkins job:
http://162.213.35.27:8080/job/charmworld-autoland-lxc/15/
Executed test runs:

review: Needs Fixing (continuous-integration)
Revision history for this message
Richard Harding (rharding) wrote :

Updated and sync'd with trunk. Will try to land again.

Revision history for this message
Juju Gui Bot (juju-gui-bot) wrote :

FAILED: Autolanding.
More details in the following jenkins job:
http://162.213.35.27:8080/job/charmworld-autoland-lxc/16/
Executed test runs:

review: Needs Fixing (continuous-integration)
Revision history for this message
Juju Gui Bot (juju-gui-bot) wrote :

FAILED: Autolanding.
More details in the following jenkins job:
http://162.213.35.27:8080/job/charmworld-autoland-lxc/17/
Executed test runs:

review: Needs Fixing (continuous-integration)
Revision history for this message
Juju Gui Bot (juju-gui-bot) wrote :

FAILED: Autolanding.
More details in the following jenkins job:
http://162.213.35.27:8080/job/charmworld-autoland-lxc/18/
Executed test runs:

review: Needs Fixing (continuous-integration)
Revision history for this message
Juju Gui Bot (juju-gui-bot) :
review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'charmworld/models.py'
--- charmworld/models.py 2013-10-01 00:38:01 +0000
+++ charmworld/models.py 2013-10-07 14:35:40 +0000
@@ -1288,7 +1288,7 @@
1288class Bundle:1288class Bundle:
1289 """A model to represent bundles of charms."""1289 """A model to represent bundles of charms."""
12901290
1291 _COMMON_REPRESENTATION = {1291 _MONGO_FIELDS = {
1292 'owner': '',1292 'owner': '',
1293 'basket_name': None,1293 'basket_name': None,
1294 'basket_revision': None,1294 'basket_revision': None,
@@ -1305,7 +1305,7 @@
1305 'inheriting from other bundles is not supported, the bundle '1305 'inheriting from other bundles is not supported, the bundle '
1306 'must be preprocessed to incorporate any inheritance')1306 'must be preprocessed to incorporate any inheritance')
1307 self._raw_representation = data1307 self._raw_representation = data
1308 self._representation = dict(self._COMMON_REPRESENTATION)1308 self._representation = dict(self._MONGO_FIELDS)
1309 self._representation.update(data)1309 self._representation.update(data)
13101310
1311 @classmethod1311 @classmethod
@@ -1333,7 +1333,7 @@
1333 and self.permanent_url == other.permanent_url)1333 and self.permanent_url == other.permanent_url)
13341334
1335 def __getattr__(self, name):1335 def __getattr__(self, name):
1336 if name in self._COMMON_REPRESENTATION:1336 if name in self._MONGO_FIELDS:
1337 return self._representation[name]1337 return self._representation[name]
1338 else:1338 else:
1339 raise AttributeError1339 raise AttributeError
@@ -1406,7 +1406,7 @@
1406 """1406 """
1407 yield 'id', self.id1407 yield 'id', self.id
1408 yield 'permanent_url', self.permanent_url1408 yield 'permanent_url', self.permanent_url
1409 for key in self._COMMON_REPRESENTATION:1409 for key in self._MONGO_FIELDS:
1410 yield key, self.__getattr__(key)1410 yield key, self.__getattr__(key)
14111411
1412 @property1412 @property
@@ -1487,6 +1487,69 @@
1487 sort, max_scan, as_class, all_revisions, **kwargs))1487 sort, max_scan, as_class, all_revisions, **kwargs))
14881488
14891489
1490def _build_charm_query_from_bundle_info(bundle_charm_info, series):
1491 """Construct a query for a charm based on the info available in the bundle
1492
1493 Bundles may provide:
1494 - branch
1495 - charm (name)
1496 - If so, the series is required to determine which charm with that
1497 name. This also assumes a promulgated charm.
1498 - charm (store url)
1499 """
1500 charm_query = None
1501 branch, rev = None, None
1502 charm_branch = bundle_charm_info.get('branch')
1503 store_url = bundle_charm_info.get(
1504 'charm',
1505 bundle_charm_info.get('charm_url', None))
1506
1507 if (store_url and store_url.startswith('cs:')):
1508 charm_query = {'store_url': store_url}
1509 elif charm_branch:
1510 branch, sep, revision = charm_branch.partition('@')
1511
1512 # Strip the lp:charms to match the fields in the db.
1513 if branch.startswith('lp:'):
1514 branch = re.sub('^lp:', '', branch)
1515 # If the branch url doesn't start with an owner assume it's a
1516 # ~charmers branch
1517 if branch[0] != '~':
1518 branch = '~charmers/' + branch
1519 if not branch.endswith('/trunk'):
1520 branch = branch + '/trunk'
1521 charm_query = {u'branch_spec': str(branch)}
1522 if (revision):
1523 charm_query['store_data.revision'] = int(revision)
1524 elif store_url and series:
1525 # The last resort, if there's a series in the bundle config, and we
1526 # have a charm name, we can attempt to load the latest version of the
1527 # charm with that address.
1528 address = 'cs:' + '/'.join([series, store_url])
1529 charm_query = {u'address': address}
1530 return charm_query
1531
1532
1533def resolve_charm_from_bundle_info(db, bundle_charm_info, series):
1534 """Find the charm that the bundle is looking for.
1535
1536 Bundles may provide charm info in several ways. Attempt to find the best
1537 charm we can based on that information.
1538
1539 """
1540 charm_query = _build_charm_query_from_bundle_info(
1541 bundle_charm_info, series)
1542
1543 if charm_query:
1544 # Order all results by revision so that we grab the latest match
1545 # as we might not always have a revision to work with.
1546 sort = [('store_data.revision', pymongo.DESCENDING)]
1547 results = db.charms.find(charm_query, sort=sort).limit(1)
1548 if results.count() > 0:
1549 return results[0]
1550 return None
1551
1552
1490def _find_charms(collection, spec=None, fields=None, skip=0, limit=0,1553def _find_charms(collection, spec=None, fields=None, skip=0, limit=0,
1491 timeout=True, snapshot=False, tailable=False, sort=None,1554 timeout=True, snapshot=False, tailable=False, sort=None,
1492 max_scan=None, as_class=None, all_revisions=False, **kwargs):1555 max_scan=None, as_class=None, all_revisions=False, **kwargs):
14931556
=== modified file 'charmworld/testing/factory.py'
--- charmworld/testing/factory.py 2013-10-03 17:13:56 +0000
+++ charmworld/testing/factory.py 2013-10-07 14:35:40 +0000
@@ -256,7 +256,7 @@
256 charm = dict(payload)256 charm = dict(payload)
257 add_store_data(charm, promulgated, charm_error, store_revision)257 add_store_data(charm, promulgated, charm_error, store_revision)
258 charm.update({258 charm.update({
259 '_id': construct_charm_id(charm),259 '_id': construct_charm_id(charm, revision),
260 'address': get_address(charm, promulgated),260 'address': get_address(charm, promulgated),
261 "branch_dir": os.path.join(branch_root, charm['name']),261 "branch_dir": os.path.join(branch_root, charm['name']),
262 "categories": categories,262 "categories": categories,
263263
=== modified file 'charmworld/tests/test_models.py'
--- charmworld/tests/test_models.py 2013-09-10 14:45:25 +0000
+++ charmworld/tests/test_models.py 2013-10-07 14:35:40 +0000
@@ -27,6 +27,7 @@
2727
28from charmworld.models import (28from charmworld.models import (
29 acquire_session_secret,29 acquire_session_secret,
30 _build_charm_query_from_bundle_info,
30 Bundle,31 Bundle,
31 Charm,32 Charm,
32 CharmFile,33 CharmFile,
@@ -40,6 +41,7 @@
40 options_to_storage,41 options_to_storage,
41 QAData,42 QAData,
42 QADataSource,43 QADataSource,
44 resolve_charm_from_bundle_info,
43 slurp_files,45 slurp_files,
44 storage_to_options,46 storage_to_options,
45 store_bundles,47 store_bundles,
@@ -1644,7 +1646,7 @@
1644 bundle_data = {}1646 bundle_data = {}
1645 bundle = Bundle(bundle_data)1647 bundle = Bundle(bundle_data)
1646 self.assertIs(bundle_data, bundle._raw_representation)1648 self.assertIs(bundle_data, bundle._raw_representation)
1647 self.assertEqual(bundle._COMMON_REPRESENTATION, bundle._representation)1649 self.assertEqual(bundle._MONGO_FIELDS, bundle._representation)
16481650
1649 def test_init_with_minimal_data(self):1651 def test_init_with_minimal_data(self):
1650 bundle_data = {1652 bundle_data = {
@@ -1852,6 +1854,51 @@
1852 '~bac/sample_bundle/1',1854 '~bac/sample_bundle/1',
1853 bundle.basket_id)1855 bundle.basket_id)
18541856
1857 def test_charm_resolving_from_bundle_branch(self):
1858 service_data = {
1859 'charm': 'mediawiki',
1860 'branch': 'lp:charms/precise/mediawiki'
1861 }
1862 query = _build_charm_query_from_bundle_info(service_data, None)
1863 self.assertEqual(
1864 '~charmers/charms/precise/mediawiki/trunk',
1865 query['branch_spec'])
1866 self.assertNotIn('store_data.revision', query)
1867
1868 # And supports a version.
1869 service_data['branch'] = 'lp:charms/precise/mediawiki@7'
1870 query = _build_charm_query_from_bundle_info(service_data, None)
1871 self.assertEqual(
1872 '~charmers/charms/precise/mediawiki/trunk',
1873 query['branch_spec'])
1874 self.assertEqual(7, query['store_data.revision'])
1875
1876 def test_charm_resolving_from_bundle_charm_csurl(self):
1877 service_data = {
1878 'charm': 'cs:precise/mediawiki-7'
1879 }
1880 query = _build_charm_query_from_bundle_info(service_data, None)
1881 self.assertEqual(
1882 'cs:precise/mediawiki-7',
1883 query['store_url'])
1884
1885 def test_charm_resolving_from_bundle_charm_and_series(self):
1886 service_data = {
1887 'charm': 'mediawiki'
1888 }
1889 series = 'precise'
1890 query = _build_charm_query_from_bundle_info(service_data, series)
1891 self.assertEqual(
1892 'cs:precise/mediawiki',
1893 query['address'])
1894
1895 def test_charm_resolving_from_bundle_empty_missing_info(self):
1896 service_data = {
1897 'charm': 'mediawiki'
1898 }
1899 query = _build_charm_query_from_bundle_info(service_data, None)
1900 self.assertIsNone(query)
1901
18551902
1856class TestMakeBundleDoc(TestCase):1903class TestMakeBundleDoc(TestCase):
18571904
@@ -1953,6 +2000,76 @@
1953 self.assertEqual(expected, filelist)2000 self.assertEqual(expected, filelist)
19542001
19552002
2003class TestBundleLoadingCharms(MongoTestBase):
2004
2005 def setUp(self):
2006 """Create 3 of the same charms at different revisions"""
2007 super(TestBundleLoadingCharms, self).setUp()
2008 charm_args = {
2009 'name': 'wikipedia',
2010 'series': 'precise',
2011 'owner': 'charmers',
2012 'promulgated': True
2013 }
2014 charm_args['revision'] = 1
2015 charm_args['store_revision'] = 1
2016 factory.makeCharm(self.db, **charm_args)
2017 charm_args['revision'] = 2
2018 charm_args['store_revision'] = 2
2019 factory.makeCharm(self.db, **charm_args)
2020 charm_args['revision'] = 3
2021 charm_args['store_revision'] = 3
2022 factory.makeCharm(self.db, **charm_args)
2023
2024 def test_resolve_by_store_url(self):
2025 data = {
2026 'charm': 'cs:precise/wikipedia-1'
2027 }
2028 charm = resolve_charm_from_bundle_info(self.db, data, None)
2029 self.assertIsNotNone(charm)
2030 self.assertEqual('~charmers/precise/wikipedia/1', charm['_id'])
2031 self.assertEqual('wikipedia', charm['name'])
2032 self.assertEqual(1, charm['store_data']['revision'])
2033 self.assertEqual(1, charm['revision'])
2034
2035 def test_resolve_by_name_and_series(self):
2036 data = {
2037 'charm': 'wikipedia'
2038 }
2039 charm = resolve_charm_from_bundle_info(self.db, data, 'precise')
2040 self.assertIsNotNone(charm)
2041 self.assertEqual('wikipedia', charm['name'])
2042 self.assertEqual(3, charm['store_data']['revision'])
2043 self.assertEqual(3, charm['revision'])
2044
2045 def test_resolve_by_branch(self):
2046 data = {
2047 'branch': 'lp:charms/precise/wikipedia'
2048 }
2049 charm = resolve_charm_from_bundle_info(self.db, data, 'precise')
2050 self.assertIsNotNone(charm)
2051 self.assertEqual('wikipedia', charm['name'])
2052 self.assertEqual(3, charm['store_data']['revision'])
2053 self.assertEqual(3, charm['revision'])
2054
2055 def test_resolve_by_branch_with_revision(self):
2056 data = {
2057 'branch': 'lp:charms/precise/wikipedia@2'
2058 }
2059 charm = resolve_charm_from_bundle_info(self.db, data, 'precise')
2060 self.assertIsNotNone(charm)
2061 self.assertEqual('wikipedia', charm['name'])
2062 self.assertEqual(2, charm['store_data']['revision'])
2063 self.assertEqual(2, charm['revision'])
2064
2065 def test_not_enough_info(self):
2066 data = {
2067 'name': 'wikipedia'
2068 }
2069 charm = resolve_charm_from_bundle_info(self.db, data, None)
2070 self.assertIsNone(charm)
2071
2072
1956class TestFeaturedSource(MongoTestBase):2073class TestFeaturedSource(MongoTestBase):
19572074
1958 def test_set_and_get_work(self):2075 def test_set_and_get_work(self):
19592076
=== modified file 'charmworld/views/api.py'
--- charmworld/views/api.py 2013-10-03 17:13:56 +0000
+++ charmworld/views/api.py 2013-10-07 14:35:40 +0000
@@ -29,6 +29,7 @@
29 FeaturedSource,29 FeaturedSource,
30 getfs,30 getfs,
31 QADataSource,31 QADataSource,
32 resolve_charm_from_bundle_info,
32)33)
33from charmworld.search import (34from charmworld.search import (
34 BUNDLE,35 BUNDLE,
@@ -427,6 +428,25 @@
427 query, sort=[('store_data.revision', pymongo.DESCENDING)])428 query, sort=[('store_data.revision', pymongo.DESCENDING)])
428 return charm_id, trailing, charm429 return charm_id, trailing, charm
429430
431 def _build_bundle_metadata(self, bundle, db):
432 bundle_dict = dict(bundle)
433 # Add the list of files in the bundle.
434 bundle_dict['files'] = bundle.get_file_list(db)
435 bundle_dict['charm_metadata'] = {}
436 # Now load the charm information we require for the services in the
437 # bundle.
438 for service, data in bundle.data['services'].iteritems():
439 charm = resolve_charm_from_bundle_info(
440 db,
441 data,
442 bundle_dict.get('series')
443 )
444 if charm:
445 formatted = self._format_charm(Charm(charm))
446 bundle_dict['charm_metadata'][service] = formatted
447
448 return bundle_dict
449
430 def _find_bundle(self, path):450 def _find_bundle(self, path):
431 try:451 try:
432 bundle_id, trailing, bundle_bits = self._parse_bundle_id(path)452 bundle_id, trailing, bundle_bits = self._parse_bundle_id(path)
@@ -463,6 +483,7 @@
463 if charm_data is None:483 if charm_data is None:
464 return json_response(484 return json_response(
465 404, {'type': 'no_such_charm', 'charm_id': charm_id})485 404, {'type': 'no_such_charm', 'charm_id': charm_id})
486
466 charm = Charm(charm_data)487 charm = Charm(charm_data)
467 if trailing is None:488 if trailing is None:
468 return self._charm_details(charm_data)489 return self._charm_details(charm_data)
@@ -503,6 +524,7 @@
503 result = {'type': 'no_such_bundle', 'bundle_id': '/'.join(path)}524 result = {'type': 'no_such_bundle', 'bundle_id': '/'.join(path)}
504 return json_response(status, result)525 return json_response(status, result)
505 if not trailing:526 if not trailing:
527 bundle = self._build_bundle_metadata(bundle, self.request.db)
506 return json_response(200, bundle)528 return json_response(200, bundle)
507 elif trailing and trailing[0] == self.ICON_TRAILING:529 elif trailing and trailing[0] == self.ICON_TRAILING:
508 return self._bundle_file(bundle, filename)530 return self._bundle_file(bundle, filename)
509531
=== modified file 'charmworld/views/tests/test_api.py'
--- charmworld/views/tests/test_api.py 2013-10-03 17:13:56 +0000
+++ charmworld/views/tests/test_api.py 2013-10-07 14:35:40 +0000
@@ -779,7 +779,20 @@
779 self.assertEqual(200, response.status_code)779 self.assertEqual(200, response.status_code)
780780
781 def test_results_match(self):781 def test_results_match(self):
782 self.makeBundle(name='bat', owner='bac', basket_with_rev='byobu/4')782 # Make the charm that we'll use as a service.
783 _id, charm = factory.makeCharm(
784 self.db,
785 description=''
786 )
787 services = {
788 u'charm': {
789 u'branch': charm['branch_spec'],
790 u'charm': charm['name']
791 }
792 }
793 self.makeBundle(
794 name='bat', owner='bac',
795 basket_with_rev='byobu/4', services=services)
783 response = self.get_response('bundle', '~bac/byobu/4/bat')796 response = self.get_response('bundle', '~bac/byobu/4/bat')
784 self.assertEqual(200, response.status_code)797 self.assertEqual(200, response.status_code)
785 self.assertEqual(798 self.assertEqual(
@@ -788,12 +801,21 @@
788 u'basket_name': u'byobu',801 u'basket_name': u'byobu',
789 u'basket_revision': 4,802 u'basket_revision': 4,
790 u'branch_deleted': False,803 u'branch_deleted': False,
804 u'charm_metadata': {
805 u'charm': self.api_class._format_charm(Charm(charm))
806 },
791 u'data': {807 u'data': {
792 u'series': u'precise',808 u'series': u'precise',
793 u'services': {},809 u'services': {
810 u'charm': {
811 u'branch': charm['branch_spec'],
812 u'charm': charm['name']
813 }
814 },
794 u'relations': {},815 u'relations': {},
795 },816 },
796 u'description': u'',817 u'description': u'',
818 u'files': [],
797 u'id': u'~bac/byobu/4/bat',819 u'id': u'~bac/byobu/4/bat',
798 u'name': u'bat',820 u'name': u'bat',
799 u'owner': u'bac',821 u'owner': u'bac',

Subscribers

People subscribed via source and target branches