Merge lp:~tvansteenburgh/charmworldlib/bundle-support into lp:charmworldlib/0.3

Proposed by Tim Van Steenburgh
Status: Merged
Merged at revision: 31
Proposed branch: lp:~tvansteenburgh/charmworldlib/bundle-support
Merge into: lp:charmworldlib/0.3
Diff against target: 953 lines (+761/-63)
7 files modified
charmworldlib/api.py (+21/-0)
charmworldlib/bundle.py (+112/-4)
charmworldlib/charm.py (+12/-21)
tests/test_api.py (+7/-0)
tests/test_bundle.py (+493/-32)
tests/test_bundles.py (+108/-0)
tests/test_charms.py (+8/-6)
To merge this branch: bzr merge lp:~tvansteenburgh/charmworldlib/bundle-support
Reviewer Review Type Date Requested Status
Marco Ceppi (community) Approve
charmers Pending
Review via email: mp+219745@code.launchpad.net

Description of the change

Added Bundle support

To post a comment you must log in.
Revision history for this message
Marco Ceppi (marcoceppi) wrote :

Yes. LGTM!

review: Approve
31. By Marco Ceppi

[tvansteenburgh] Add Bundle support

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'charmworldlib/api.py'
--- charmworldlib/api.py 2014-02-04 12:44:47 +0000
+++ charmworldlib/api.py 2014-05-15 18:32:26 +0000
@@ -38,6 +38,19 @@
3838
39 return r39 return r
4040
41 def search(self, endpoint, doctype, criteria=None, limit=None):
42 if type(criteria) is str:
43 criteria = {'text': criteria}
44 else:
45 criteria = criteria or {}
46
47 criteria['text'] = self._doctype_filter(criteria.get('text'), doctype)
48
49 if limit and type(limit) is int:
50 criteria['limit'] = limit
51
52 return self.get(endpoint, criteria)['result'] or []
53
41 def _earl(self):54 def _earl(self):
42 if self.port:55 if self.port:
43 return '%s://%s:%s' % (self.protocol, self.server, self.port)56 return '%s://%s:%s' % (self.protocol, self.server, self.port)
@@ -49,3 +62,11 @@
49 endpoint = '/%s' % endpoint62 endpoint = '/%s' % endpoint
5063
51 return '%s/api/%s%s' % (self._earl(), self.version, endpoint)64 return '%s/api/%s%s' % (self._earl(), self.version, endpoint)
65
66 def _doctype_filter(self, text, doctype):
67 text = (text or '').strip()
68 if not text:
69 return doctype
70 if not text.startswith(doctype + ':'):
71 return '{}:{}'.format(doctype, text)
72 return text
5273
=== modified file 'charmworldlib/bundle.py'
--- charmworldlib/bundle.py 2014-02-28 13:22:33 +0000
+++ charmworldlib/bundle.py 2014-05-15 18:32:26 +0000
@@ -1,10 +1,18 @@
1import json
1import yaml2import yaml
23
3from . import api4from . import api
5from .charm import Charm
6
7
8class BundleNotFound(Exception):
9 pass
410
511
6class Bundles(api.API):12class Bundles(api.API):
7 _base_endpoint = 'bundle'13 _base_endpoint = {1: 'bundle', 2: 'bundle', 3: 'bundle'}
14 _base_search_endpoint = {1: 'bundles', 2: 'search', 3: 'search'}
15 _doctype = 'bundle'
816
9 def proof(self, deployer_contents):17 def proof(self, deployer_contents):
10 if not self.version >= 3:18 if not self.version >= 3:
@@ -13,10 +21,110 @@
13 if type(deployer_contents) is not dict:21 if type(deployer_contents) is not dict:
14 raise Exception('Invalid deployer_contents')22 raise Exception('Invalid deployer_contents')
1523
16 return self.post('%s/proof' % self._base_endpoint,24 return self.post('%s/proof' % self._base_endpoint[self.version],
17 {'deployer_file': yaml.dump(deployer_contents)})25 {'deployer_file': yaml.dump(deployer_contents)})
1826
27 def bundle(self, basket_name, name, revision=None, owner=None):
28 data = self.get(self.bundle_endpoint(
29 basket_name, name, revision, owner))
30
31 if 'result' not in data:
32 return None
33
34 bundle_raw = data['result'][0]
35 return Bundle.from_bundledata(bundle_raw)
36
37 def bundle_endpoint(self, basket_name, name, revision=None, owner=None):
38 if owner and not owner.startswith('~'):
39 owner = "~%s" % owner
40
41 endpoint = self._base_endpoint[self.version]
42
43 if owner:
44 endpoint = "%s/%s" % (endpoint, owner)
45
46 endpoint = "%s/%s" % (endpoint, basket_name)
47
48 if isinstance(revision, int) and revision >= 0:
49 endpoint = "%s/%s" % (endpoint, revision)
50
51 endpoint = "%s/%s" % (endpoint, name)
52
53 return endpoint
54
55 def search(self, criteria=None, limit=None):
56 result = super(Bundles, self).search(
57 self._base_search_endpoint[self.version],
58 self._doctype,
59 criteria=criteria,
60 limit=limit)
61 return [Bundle.from_bundledata(bundle) for bundle in result]
62
63 def approved(self):
64 return self.search(criteria={'type': 'approved'})
65
1966
20class Bundle(object):67class Bundle(object):
21 def __init__(self):68 @classmethod
22 pass69 def from_bundledata(cls, bundle_data):
70 bundle = cls()
71 bundle._parse(bundle_data)
72
73 return bundle
74
75 def __init__(self, bundle_id=None):
76 self.charms = {}
77 self._raw = {}
78 self._api = api.API()
79
80 if bundle_id:
81 self._fetch(bundle_id)
82
83 def __getattr__(self, key):
84 return self._raw[key]
85
86 def _fetch(self, bundle_id):
87 b = Bundles()
88 owner, basket_name, revision, name = self._parse_id(bundle_id)
89 try:
90 data = self._api.get(b.bundle_endpoint(basket_name, name,
91 revision=revision,
92 owner=owner))
93 self._parse(data)
94 except Exception as e:
95 raise BundleNotFound('API request failed: %s' % str(e))
96
97 def _parse(self, bundle_data):
98 self._raw = bundle_data
99
100 for charm_name, charm_data in bundle_data['charm_metadata'].items():
101 self.charms[charm_name] = Charm.from_charmdata({
102 'charm': charm_data})
103
104 def _parse_id(self, bundle_id):
105 owner, revision = None, None
106
107 parts = bundle_id.split('/')
108
109 if len(parts) == 4:
110 owner, basket_name, revision, name = parts
111 elif len(parts) == 3:
112 if bundle_id.startswith('~'):
113 owner, basket_name, name = parts
114 else:
115 basket_name, revision, name = parts
116 elif len(parts) == 2:
117 basket_name, name = parts
118 else:
119 raise ValueError('Invalid bundle id: {}'.format(bundle_id))
120
121 if revision:
122 revision = int(revision)
123
124 return owner, basket_name, revision, name
125
126 def __str__(self):
127 return json.dumps(self._raw, indent=2)
128
129 def __repr__(self):
130 return '<Bundle %s>' % self.id
23131
=== modified file 'charmworldlib/charm.py'
--- charmworldlib/charm.py 2014-02-04 16:24:43 +0000
+++ charmworldlib/charm.py 2014-05-15 18:32:26 +0000
@@ -1,4 +1,3 @@
1
2import re1import re
3import json2import json
4from . import api3from . import api
@@ -40,6 +39,7 @@
40class Charms(api.API):39class Charms(api.API):
41 _base_endpoint = {1: 'charm', 2: 'charm', 3: 'charm'}40 _base_endpoint = {1: 'charm', 2: 'charm', 3: 'charm'}
42 _base_search_endpoint = {1: 'charms', 2: 'charms', 3: 'search'}41 _base_search_endpoint = {1: 'charms', 2: 'charms', 3: 'search'}
42 _doctype = 'charm'
4343
44 def requires(self, interfaces=[], limit=None):44 def requires(self, interfaces=[], limit=None):
45 return self.interfaces(requires=interfaces)45 return self.interfaces(requires=interfaces)
@@ -73,26 +73,6 @@
73 charm_raw = data['result'][0]73 charm_raw = data['result'][0]
74 return Charm.from_charmdata(charm_raw)74 return Charm.from_charmdata(charm_raw)
7575
76 def approved(self):
77 return self.search({'type': 'approved'})
78
79 def search(self, criteria={}, limit=None):
80 results = []
81 if type(criteria) is str:
82 criteria = {'text': criteria}
83 if limit and type(limit) is int:
84 criteria['limit'] = limit
85
86 data = self.get(self._base_search_endpoint[self.version], criteria)
87
88 if not data['result']:
89 return []
90
91 for charm in data['result']:
92 results.append(Charm.from_charmdata(charm))
93
94 return results
95
96 def charm_endpoint(self, name, series='precise', revision=None,76 def charm_endpoint(self, name, series='precise', revision=None,
97 owner=None):77 owner=None):
98 if owner and not owner.startswith('~'):78 if owner and not owner.startswith('~'):
@@ -110,6 +90,17 @@
11090
111 return endpoint91 return endpoint
11292
93 def search(self, criteria=None, limit=None):
94 result = super(Charms, self).search(
95 self._base_search_endpoint[self.version],
96 self._doctype,
97 criteria=criteria,
98 limit=limit)
99 return [Charm.from_charmdata(charm) for charm in result]
100
101 def approved(self):
102 return self.search(criteria={'type': 'approved'})
103
113104
114class Charm(object):105class Charm(object):
115 @classmethod106 @classmethod
116107
=== modified file 'tests/test_api.py'
--- tests/test_api.py 2014-01-20 17:43:03 +0000
+++ tests/test_api.py 2014-05-15 18:32:26 +0000
@@ -78,3 +78,10 @@
78 def test_post_params(self, mfetch):78 def test_post_params(self, mfetch):
79 self.a.post('end/of/the/world', {'isit'})79 self.a.post('end/of/the/world', {'isit'})
80 mfetch.assert_called_with('end/of/the/world', {'isit'}, 'post')80 mfetch.assert_called_with('end/of/the/world', {'isit'}, 'post')
81
82 def test_doctype_filter(self):
83 f = self.a._doctype_filter
84 self.assertEqual(f(None, 'charm'), 'charm')
85 self.assertEqual(f(' ', 'charm'), 'charm')
86 self.assertEqual(f('searchterm', 'charm'), 'charm:searchterm')
87 self.assertEqual(f(' charm:searchterm', 'charm'), 'charm:searchterm')
8188
=== modified file 'tests/test_bundle.py'
--- tests/test_bundle.py 2014-02-28 13:22:33 +0000
+++ tests/test_bundle.py 2014-05-15 18:32:26 +0000
@@ -1,38 +1,499 @@
1"""Unit test for API"""1"""Unit test for Bundle"""
22
3import json
4from mock import patch
3import unittest5import unittest
46
5from mock import patch7from charmworldlib.bundle import Bundle, BundleNotFound
6from charmworldlib.bundle import (8
7 Bundle,9BUNDLE_DATA = json.loads(r'''
8 Bundles,10{
9)11 "basket_name": "mediawiki",
1012 "basket_revision": 6,
1113 "branch_deleted": false,
12class BundlesTest(unittest.TestCase):14 "branch_spec": "~charmers/charms/bundles/mediawiki/bundle",
13 @classmethod15 "changes": [
14 def setUpClass(cls):16 {
15 cls.b = Bundles()17 "authors": [
1618 "Jorge O. Castro <jorge@ubuntu.com>"
17 @patch('charmworldlib.bundle.api.API.post')19 ],
18 def test_bundles_proof(self, mpost):20 "committer": "Jorge O. Castro <jorge@ubuntu.com>",
19 mpost.return_value = {'deploy'}21 "created": 1383229159.809,
20 self.b.proof({'deployment': {}})22 "message": "Initial commit\n",
21 mpost.assert_called_with('bundle/proof', {'deployer_file':23 "revno": 1
22 'deployment: {}\n'})24 }
2325 ],
24 def test_bundles_proof_ver(self):26 "charm_metadata": {
25 b = Bundles(version=2)27 "mediawiki": {
26 self.assertRaises(ValueError, b.proof, 'garbage')28 "annotations": {
2729 "gui-x": 609,
28 def test_bundles_proof_invalid(self):30 "gui-y": -15
29 self.assertRaises(Exception, self.b.proof, 'garbage')31 },
32 "categories": [
33 "applications"
34 ],
35 "code_source": {
36 "bugs_link": "https://bugs.launchpad.net/charms/+source/mediawiki",
37 "last_log": "merging lp:~dave-cheney/charms/precise/mediawiki/trunk as per https://code.launchpad.net/~dave-cheney/charms/precise/mediawiki/trunk/+merge/182803",
38 "location": "lp:~charmers/charms/precise/mediawiki/trunk",
39 "revision": "72",
40 "revisions": [
41 {
42 "authors": [
43 {
44 "email": "clint@ubuntu.com",
45 "name": "Clint Byrum"
46 }
47 ],
48 "date": "2012-06-28T00:02:47Z",
49 "message": "removing old broken munin bits",
50 "revno": 63
51 }
52 ],
53 "type": "bzr"
54 },
55 "date_created": "2012-04-16T18:29:51Z",
56 "description": "MediaWiki is a wiki engine (a program for creating a collaboratively\nedited website). It is designed to handle heavy websites containing\nlibrary-like document collections, and supports user uploads of\nimages/sounds, multilingual content, TOC autogeneration, ISBN links,\netc.\n",
57 "distro_series": "precise",
58 "downloads": 3928,
59 "downloads_in_past_30_days": 484,
60 "files": [
61 "hooks/slave-relation-departed",
62 "hooks/combine-dbservers",
63 "hooks/cache-relation-changed",
64 "hooks/website-relation-joined",
65 "revision",
66 "icon.svg",
67 "hooks/upgrade-charm",
68 "hooks/stop",
69 "README.md",
70 "hooks/db-relation-changed",
71 "hooks/db-relation-departed",
72 "hooks/install",
73 "metadata.yaml",
74 "hooks/config-changed",
75 "hooks/slave-relation-changed",
76 "config.yaml",
77 "hooks/slave-relation-broken"
78 ],
79 "id": "precise/mediawiki-10",
80 "is_approved": true,
81 "is_subordinate": false,
82 "maintainer": {
83 "email": "clint@ubuntu.com",
84 "name": "Clint Byrum"
85 },
86 "maintainers": [
87 {
88 "email": "clint@ubuntu.com",
89 "name": "Clint Byrum"
90 }
91 ],
92 "name": "mediawiki",
93 "options": {
94 "admins": {
95 "description": "Admin users to create, user:pass",
96 "type": "string"
97 },
98 "debug": {
99 "default": false,
100 "description": "turn on debugging features of mediawiki",
101 "type": "boolean"
102 },
103 "logo": {
104 "description": "URL to fetch logo from",
105 "type": "string"
106 },
107 "name": {
108 "default": "Please set name of wiki",
109 "description": "The name, or Title of the Wiki",
110 "type": "string"
111 },
112 "skin": {
113 "default": "vector",
114 "description": "skin for the Wiki",
115 "type": "string"
116 }
117 },
118 "owner": "charmers",
119 "rating_denominator": 0,
120 "rating_numerator": 0,
121 "relations": {
122 "provides": {
123 "website": {
124 "interface": "http"
125 }
126 },
127 "requires": {
128 "cache": {
129 "interface": "memcache"
130 },
131 "db": {
132 "interface": "mysql"
133 },
134 "slave": {
135 "interface": "mysql"
136 }
137 }
138 },
139 "revision": 90,
140 "summary": "Website engine for collaborative work",
141 "tested_providers": {
142 "ec2": "SUCCESS",
143 "openstack": "SUCCESS"
144 },
145 "url": "cs:precise/mediawiki-10"
146 },
147 "mysql": {
148 "annotations": {
149 "gui-x": 610,
150 "gui-y": 255
151 },
152 "categories": [
153 "databases"
154 ],
155 "code_source": {
156 "bugs_link": "https://bugs.launchpad.net/charms/+source/mysql",
157 "last_log": "Updated README",
158 "location": "lp:~charmers/charms/precise/mysql/trunk",
159 "revision": "105",
160 "revisions": [
161 {
162 "authors": [
163 {
164 "email": "marco@ceppi.net",
165 "name": "Marco Ceppi"
166 }
167 ],
168 "date": "2013-04-25T18:19:45Z",
169 "message": "Added icon.svg",
170 "revno": 96
171 }
172 ],
173 "type": "bzr"
174 },
175 "date_created": "2012-04-16T18:30:00Z",
176 "description": "MySQL is a fast, stable and true multi-user, multi-threaded SQL database\nserver. SQL (Structured Query Language) is the most popular database query\nlanguage in the world. The main goals of MySQL are speed, robustness and\nease of use.\n",
177 "distro_series": "precise",
178 "downloads": 21895,
179 "downloads_in_past_30_days": 2006,
180 "files": [
181 "hooks/munin-relation-joined",
182 "hooks/monitors.common.bash",
183 "hooks/db-relation-joined",
184 "hooks/shared-db-relation-changed",
185 "hooks/master-relation-departed",
186 "hooks/monitors-relation-departed",
187 "hooks/master-relation-broken",
188 "hooks/lib/cluster_utils.py",
189 "hooks/shared_db_relations.py",
190 "hooks/slave-relation-broken",
191 "hooks/lib/utils.py",
192 "hooks/ha-relation-changed",
193 "hooks/munin-relation-changed",
194 "hooks/common.py",
195 "hooks/start",
196 "hooks/config-changed",
197 "hooks/db-relation-broken",
198 "hooks/slave-relation-changed",
199 "hooks/shared-db-relation-joined",
200 "hooks/ha_relations.py",
201 "hooks/cluster-relation-changed",
202 "hooks/slave-relation-departed",
203 "hooks/lib/ceph_utils.py",
204 "hooks/ceph-relation-changed",
205 "metadata.yaml",
206 "hooks/ha-relation-joined",
207 "hooks/stop",
208 "hooks/db-admin-relation-joined",
209 "config.yaml",
210 "hooks/monitors-relation-joined",
211 "icon.svg",
212 "hooks/upgrade-charm",
213 "README.md",
214 "hooks/ceph-relation-joined",
215 "hooks/master-relation-changed",
216 "hooks/lib/__init__.py",
217 "hooks/slave-relation-joined",
218 "hooks/install",
219 "hooks/local-monitors-relation-joined",
220 "revision",
221 "hooks/monitors-relation-broken"
222 ],
223 "id": "precise/mysql-28",
224 "is_approved": true,
225 "is_subordinate": false,
226 "maintainer": {
227 "email": "marco@ceppi.net",
228 "name": "Marco Ceppi"
229 },
230 "maintainers": [
231 {
232 "email": "marco@ceppi.net",
233 "name": "Marco Ceppi"
234 }
235 ],
236 "name": "mysql",
237 "options": {
238 "binlog-format": {
239 "default": "MIXED",
240 "description": "If binlogging is enabled, this is the format that will be used. Ignored when tuning-level == fast.",
241 "type": "string"
242 },
243 "block-size": {
244 "default": 5,
245 "description": "Default block storage size to create when setting up MySQL block storage.\nThis value should be specified in GB (e.g. 100 not 100GB).\n",
246 "type": "int"
247 },
248 "dataset-size": {
249 "default": "80%",
250 "description": "How much data do you want to keep in memory in the DB. This will be used to tune settings in the database server appropriately. Any more specific settings will override these defaults though. This currently sets innodb_buffer_pool_size or key_cache_size depending on the setting in preferred-storage-engine. If query-cache-type is set to 'ON' or 'DEMAND' 20% of this is given to query-cache-size. Suffix this value with 'K','M','G', or 'T' to get the relevant kilo/mega/etc. bytes. If suffixed with %, one will get that percentage of RAM devoted to dataset and (if enabled) query cache.",
251 "type": "string"
252 },
253 "flavor": {
254 "default": "distro",
255 "description": "Possible values are 'distro' or 'percona'",
256 "type": "string"
257 },
258 "ha-bindiface": {
259 "default": "eth0",
260 "description": "Default network interface on which HA cluster will bind to communication\nwith the other members of the HA Cluster.\n",
261 "type": "string"
262 },
263 "ha-mcastport": {
264 "default": 5411,
265 "description": "Default multicast port number that will be used to communicate between\nHA Cluster nodes.\n",
266 "type": "int"
267 },
268 "max-connections": {
269 "default": -1,
270 "description": "Maximum connections to allow. -1 means use the server's compiled in default.",
271 "type": "int"
272 },
273 "preferred-storage-engine": {
274 "default": "InnoDB",
275 "description": "Tune the server for usage of this storage engine. Other possible value is MyISAM. Comma separated will cause settings to split resources evenly among given engines.",
276 "type": "string"
277 },
278 "query-cache-size": {
279 "default": -1,
280 "description": "Override the computed version from dataset-size. Still works if query-cache-type is \"OFF\" since sessions can override the cache type setting on their own.",
281 "type": "int"
282 },
283 "query-cache-type": {
284 "default": "OFF",
285 "description": "Query cache is usually a good idea, but can hurt concurrency. Valid values are \"OFF\", \"ON\", or \"DEMAND\". http://dev.mysql.com/doc/refman/5.1/en/server-system-variables.html#sysvar_query_cache_type",
286 "type": "string"
287 },
288 "rbd-name": {
289 "default": "mysql1",
290 "description": "The name that will be used to create the Ceph's RBD image with. If the\nimage name exists in Ceph, it will be re-used and the data will be\noverwritten.\n",
291 "type": "string"
292 },
293 "tuning-level": {
294 "default": "safest",
295 "description": "Valid values are 'safest', 'fast', and 'unsafe'. If set to safest, all settings are tuned to have maximum safety at the cost of performance. Fast will turn off most controls, but may lose data on crashes. unsafe will turn off all protections.",
296 "type": "string"
297 },
298 "vip": {
299 "description": "Virtual IP to use to front mysql in ha configuration",
300 "type": "string"
301 },
302 "vip_cidr": {
303 "default": 24,
304 "description": "Netmask that will be used for the Virtual IP",
305 "type": "int"
306 },
307 "vip_iface": {
308 "default": "eth0",
309 "description": "Network Interface where to place the Virtual IP",
310 "type": "string"
311 }
312 },
313 "owner": "charmers",
314 "rating_denominator": 0,
315 "rating_numerator": 0,
316 "relations": {
317 "provides": {
318 "db": {
319 "interface": "mysql"
320 },
321 "db-admin": {
322 "interface": "mysql-root"
323 },
324 "local-monitors": {
325 "interface": "local-monitors",
326 "scope": "container"
327 },
328 "master": {
329 "interface": "mysql-oneway-replication"
330 },
331 "monitors": {
332 "interface": "monitors"
333 },
334 "munin": {
335 "interface": "munin-node"
336 },
337 "shared-db": {
338 "interface": "mysql-shared"
339 }
340 },
341 "requires": {
342 "ceph": {
343 "interface": "ceph-client"
344 },
345 "ha": {
346 "interface": "hacluster",
347 "scope": "container"
348 },
349 "slave": {
350 "interface": "mysql-oneway-replication"
351 }
352 }
353 },
354 "revision": 309,
355 "summary": "MySQL is a fast, stable and true multi-user, multi-threaded SQL database",
356 "tested_providers": {},
357 "url": "cs:precise/mysql-28"
358 }
359 },
360 "data": {
361 "relations": [
362 [
363 "mediawiki:db",
364 "mysql:db"
365 ]
366 ],
367 "series": "precise",
368 "services": {
369 "mediawiki": {
370 "annotations": {
371 "gui-x": 609,
372 "gui-y": -15
373 },
374 "charm": "cs:precise/mediawiki-10",
375 "num_units": 1,
376 "options": {
377 "debug": false,
378 "name": "Please set name of wiki",
379 "skin": "vector"
380 }
381 },
382 "mysql": {
383 "annotations": {
384 "gui-x": 610,
385 "gui-y": 255
386 },
387 "charm": "cs:precise/mysql-28",
388 "num_units": 1,
389 "options": {
390 "binlog-format": "MIXED",
391 "block-size": 5,
392 "dataset-size": "80%",
393 "flavor": "distro",
394 "ha-bindiface": "eth0",
395 "ha-mcastport": 5411,
396 "max-connections": -1,
397 "preferred-storage-engine": "InnoDB",
398 "query-cache-size": -1,
399 "query-cache-type": "OFF",
400 "rbd-name": "mysql1",
401 "tuning-level": "safest",
402 "vip_cidr": 24,
403 "vip_iface": "eth0"
404 }
405 }
406 }
407 },
408 "deployer_file_url": "http://manage.jujucharms.com/bundle/%7Echarmers/mediawiki/6/single/json",
409 "description": "",
410 "downloads": 58,
411 "downloads_in_past_30_days": 4,
412 "files": [
413 "README.md",
414 "bundles.yaml"
415 ],
416 "first_change": {
417 "authors": [
418 "Jorge O. Castro <jorge@ubuntu.com>"
419 ],
420 "committer": "Jorge O. Castro <jorge@ubuntu.com>",
421 "created": 1383229159.809,
422 "message": "Initial commit\n",
423 "revno": 1
424 },
425 "id": "~charmers/mediawiki/6/single",
426 "last_change": {
427 "authors": [
428 "Jorge O. Castro <jorge@ubuntu.com>"
429 ],
430 "committer": "Jorge O. Castro <jorge@ubuntu.com>",
431 "created": 1394724884.008,
432 "message": "Combine single and scalable bundles into one.\n",
433 "revno": 6
434 },
435 "name": "single",
436 "owner": "charmers",
437 "permanent_url": "bundle:~charmers/mediawiki/6/single",
438 "promulgated": true,
439 "title": ""
440}''')
30441
31442
32class BundleTest(unittest.TestCase):443class BundleTest(unittest.TestCase):
33 @classmethod444 def test_from_bundledata(self):
34 def setUpClass(cls):445 b = Bundle.from_bundledata(BUNDLE_DATA)
35 cls.b = Bundle()446 self.assertIsInstance(b, Bundle)
36447 self.assertEqual(b._raw, BUNDLE_DATA)
37 def test_bundle(self):448
38 pass449 @patch('charmworldlib.bundle.api.API.get')
450 def test_init(self, get):
451 get.return_value = BUNDLE_DATA
452 self.assertEqual(
453 Bundle('~me/mediawiki/0/single')._raw,
454 Bundle.from_bundledata(BUNDLE_DATA)._raw)
455
456 def test_getattr(self):
457 b = Bundle.from_bundledata(BUNDLE_DATA)
458 for k, v in b._raw.items():
459 self.assertEqual(v, getattr(b, k))
460
461 @patch('charmworldlib.bundle.api.API.get')
462 def test_bundle_fetch_fail(self, mget):
463 msg = 'Request failed with: 500'
464 mget.side_effect = Exception(msg)
465 self.assertRaises(BundleNotFound, Bundle, bundle_id='mediawiki/single')
466
467 def test_parse(self):
468 from charmworldlib.charm import Charm
469 b = Bundle.from_bundledata(BUNDLE_DATA)
470 self.assertEqual(len(b.charms), 2)
471 self.assertTrue('mediawiki' in b.charms)
472 self.assertTrue('mysql' in b.charms)
473 for charm in b.charms.values():
474 self.assertIsInstance(charm, Charm)
475
476 def test_parse_id(self):
477 b = Bundle()
478
479 self.assertEqual(
480 b._parse_id('~me/mediawiki/0/single'),
481 ('~me', 'mediawiki', 0, 'single'))
482 self.assertEqual(
483 b._parse_id('~me/mediawiki/single'),
484 ('~me', 'mediawiki', None, 'single'))
485 self.assertEqual(
486 b._parse_id('mediawiki/0/single'),
487 (None, 'mediawiki', 0, 'single'))
488 self.assertEqual(
489 b._parse_id('mediawiki/single'),
490 (None, 'mediawiki', None, 'single'))
491 self.assertRaises(ValueError, b._parse_id, 'mediawiki')
492
493 def test_str(self):
494 b = Bundle.from_bundledata(BUNDLE_DATA)
495 self.assertEqual(str(b), json.dumps(BUNDLE_DATA, indent=2))
496
497 def test_repr(self):
498 b = Bundle.from_bundledata(BUNDLE_DATA)
499 self.assertEqual(repr(b), '<Bundle ~charmers/mediawiki/6/single>')
39500
=== added file 'tests/test_bundles.py'
--- tests/test_bundles.py 1970-01-01 00:00:00 +0000
+++ tests/test_bundles.py 2014-05-15 18:32:26 +0000
@@ -0,0 +1,108 @@
1"""Unit test for Bundles"""
2
3import unittest
4
5from mock import patch
6from charmworldlib.bundle import Bundles
7
8
9class BundlesTest(unittest.TestCase):
10 @classmethod
11 def setUpClass(cls):
12 cls.b = Bundles()
13
14 @patch('charmworldlib.bundle.api.API.post')
15 def test_bundles_proof(self, mpost):
16 mpost.return_value = {'deploy'}
17 self.b.proof({'deployment': {}})
18 mpost.assert_called_with('bundle/proof', {'deployer_file':
19 'deployment: {}\n'})
20
21 def test_bundles_proof_ver(self):
22 b = Bundles(version=2)
23 self.assertRaises(ValueError, b.proof, 'garbage')
24
25 def test_bundles_proof_invalid(self):
26 self.assertRaises(Exception, self.b.proof, 'garbage')
27
28 @patch('charmworldlib.bundle.Bundle')
29 @patch('charmworldlib.bundle.Bundles.get')
30 def test_bundles_search(self, mget, mBundle):
31 cdata = {'bundle': {'id': 'oneiric/bundle-0'}}
32 mget.return_value = {'result': [cdata]}
33 self.assertEqual([mBundle.from_bundledata()], self.b.search())
34 mget.assert_called_with('search', {'text': 'bundle'})
35 mBundle.from_bundledata.assert_called_with(cdata)
36
37 @patch('charmworldlib.bundle.Bundles.get')
38 def test_bundles_search_string(self, mget):
39 mget.return_value = {'result': None}
40 self.assertEqual([], self.b.search('blurb', 2))
41 mget.assert_called_with('search', {'text': 'bundle:blurb', 'limit': 2})
42
43 @patch('charmworldlib.bundle.Bundles.get')
44 def test_bundles_search_params(self, mget):
45 mget.return_value = {'result': None}
46 self.assertEqual([], self.b.search({'approved': True}, 1))
47 mget.assert_called_with(
48 'search', {'approved': True, 'limit': 1, 'text': 'bundle'})
49
50 @patch('charmworldlib.bundle.Bundles.get')
51 def test_bundles_search_no_results(self, mget):
52 mget.return_value = {'result': None}
53 self.assertEqual([], self.b.search('no-match'))
54 mget.assert_called_with('search', {'text': 'bundle:no-match'})
55
56 @patch('charmworldlib.bundle.Bundle')
57 @patch('charmworldlib.bundle.Bundles.get')
58 def test_bundles_search_versions(self, mget, mBundle):
59 self.b.version = 2
60 cdata = {'bundle': {'id': 'mediawiki/0/single'}}
61 mget.return_value = {'result': [cdata]}
62 self.assertEqual([mBundle.from_bundledata()], self.b.search())
63 mget.assert_called_with('search', {'text': 'bundle'})
64 mBundle.from_bundledata.assert_called_with(cdata)
65
66 @patch('charmworldlib.bundle.Bundles.get')
67 def test_bundles_approved(self, mget):
68 mget.return_value = {'result': None}
69 self.assertEqual([], self.b.approved())
70 mget.assert_called_with(
71 'search', {'type': 'approved', 'text': 'bundle'})
72
73 @patch('charmworldlib.bundle.Bundle.from_bundledata')
74 @patch('charmworldlib.bundle.Bundles.get')
75 def test_bundles_bundle(self, mget, mBundle):
76 cdata = {'bundle': {'id': 'mediawiki/0/single'}}
77 mget.return_value = {'result': [cdata]}
78 self.assertEqual(mBundle(), self.b.bundle('mediawiki', 'single',
79 revision=0))
80 mget.assert_called_with('bundle/mediawiki/0/single')
81 mBundle.assert_called_with(cdata)
82
83 @patch('charmworldlib.bundle.Bundle.from_bundledata')
84 @patch('charmworldlib.bundle.Bundles.get')
85 def test_bundles_bundle_full(self, mget, mBundle):
86 cdata = {'bundle': {'id': '~me/mediawiki/0/single'}}
87 mget.return_value = {'result': [cdata]}
88 self.assertEqual(mBundle(), self.b.bundle('mediawiki', 'single',
89 revision=0, owner='~me'))
90 mget.assert_called_with('bundle/~me/mediawiki/0/single')
91 mBundle.assert_called_with(cdata)
92
93 @patch('charmworldlib.bundle.Bundles.get')
94 def test_bundles_bundle_404(self, mget):
95 mget.return_value = {'none': None}
96 self.assertEqual(None, self.b.bundle('mediawiki', 'single',
97 revision=0, owner='~me'))
98 mget.assert_called_with('bundle/~me/mediawiki/0/single')
99
100 @patch('charmworldlib.bundle.Bundle.from_bundledata')
101 @patch('charmworldlib.bundle.Bundles.get')
102 def test_bundles_bundle_full_owner(self, mget, mBundle):
103 cdata = {'bundle': {'id': '~me/mediawiki/0/single'}}
104 mget.return_value = {'result': [cdata]}
105 self.assertEqual(mBundle(), self.b.bundle('mediawiki', 'single',
106 revision=0, owner='me'))
107 mget.assert_called_with('bundle/~me/mediawiki/0/single')
108 mBundle.assert_called_with(cdata)
0109
=== modified file 'tests/test_charms.py'
--- tests/test_charms.py 2014-01-24 17:15:21 +0000
+++ tests/test_charms.py 2014-05-15 18:32:26 +0000
@@ -17,26 +17,27 @@
17 cdata = {'charm': {'id': 'oneiric/charm-0'}}17 cdata = {'charm': {'id': 'oneiric/charm-0'}}
18 mget.return_value = {'result': [cdata]}18 mget.return_value = {'result': [cdata]}
19 self.assertEqual([mCharm.from_charmdata()], self.c.search())19 self.assertEqual([mCharm.from_charmdata()], self.c.search())
20 mget.assert_called_with('search', {})20 mget.assert_called_with('search', {'text': 'charm'})
21 mCharm.from_charmdata.assert_called_with(cdata)21 mCharm.from_charmdata.assert_called_with(cdata)
2222
23 @patch('charmworldlib.charm.Charms.get')23 @patch('charmworldlib.charm.Charms.get')
24 def test_charms_search_string(self, mget):24 def test_charms_search_string(self, mget):
25 mget.return_value = {'result': None}25 mget.return_value = {'result': None}
26 self.assertEqual([], self.c.search('blurb', 2))26 self.assertEqual([], self.c.search('blurb', 2))
27 mget.assert_called_with('search', {'text': 'blurb', 'limit': 2})27 mget.assert_called_with('search', {'text': 'charm:blurb', 'limit': 2})
2828
29 @patch('charmworldlib.charm.Charms.get')29 @patch('charmworldlib.charm.Charms.get')
30 def test_charms_search_params(self, mget):30 def test_charms_search_params(self, mget):
31 mget.return_value = {'result': None}31 mget.return_value = {'result': None}
32 self.assertEqual([], self.c.search({'approved': True}, 1))32 self.assertEqual([], self.c.search({'approved': True}, 1))
33 mget.assert_called_with('search', {'approved': True, 'limit': 1})33 mget.assert_called_with(
34 'search', {'approved': True, 'limit': 1, 'text': 'charm'})
3435
35 @patch('charmworldlib.charm.Charms.get')36 @patch('charmworldlib.charm.Charms.get')
36 def test_charms_search_no_results(self, mget):37 def test_charms_search_no_results(self, mget):
37 mget.return_value = {'result': None}38 mget.return_value = {'result': None}
38 self.assertEqual([], self.c.search('no-match'))39 self.assertEqual([], self.c.search('no-match'))
39 mget.assert_called_with('search', {'text': 'no-match'})40 mget.assert_called_with('search', {'text': 'charm:no-match'})
4041
41 @patch('charmworldlib.charm.Charm')42 @patch('charmworldlib.charm.Charm')
42 @patch('charmworldlib.charm.Charms.get')43 @patch('charmworldlib.charm.Charms.get')
@@ -45,14 +46,15 @@
45 cdata = {'charm': {'id': 'oneiric/charm-0'}}46 cdata = {'charm': {'id': 'oneiric/charm-0'}}
46 mget.return_value = {'result': [cdata]}47 mget.return_value = {'result': [cdata]}
47 self.assertEqual([mCharm.from_charmdata()], self.c.search())48 self.assertEqual([mCharm.from_charmdata()], self.c.search())
48 mget.assert_called_with('charms', {})49 mget.assert_called_with('charms', {'text': 'charm'})
49 mCharm.from_charmdata.assert_called_with(cdata)50 mCharm.from_charmdata.assert_called_with(cdata)
5051
51 @patch('charmworldlib.charm.Charms.get')52 @patch('charmworldlib.charm.Charms.get')
52 def test_charms_approved(self, mget):53 def test_charms_approved(self, mget):
53 mget.return_value = {'result': None}54 mget.return_value = {'result': None}
54 self.assertEqual([], self.c.approved())55 self.assertEqual([], self.c.approved())
55 mget.assert_called_with('search', {'type': 'approved'})56 mget.assert_called_with(
57 'search', {'type': 'approved', 'text': 'charm'})
5658
57 @patch('charmworldlib.charm.Charm.from_charmdata')59 @patch('charmworldlib.charm.Charm.from_charmdata')
58 @patch('charmworldlib.charm.Charms.get')60 @patch('charmworldlib.charm.Charms.get')

Subscribers

People subscribed via source and target branches

to all changes: