Merge lp:~bcsaller/charm-tools/remove-bundletester-dep into lp:charm-tools/1.6

Proposed by Marco Ceppi
Status: Merged
Merged at revision: 361
Proposed branch: lp:~bcsaller/charm-tools/remove-bundletester-dep
Merge into: lp:charm-tools/1.6
Diff against target: 367 lines (+324/-7)
4 files modified
charmtools/compose/fetchers.py (+5/-5)
charmtools/fetchers.py (+318/-0)
requirements.txt (+0/-1)
setup.py (+1/-1)
To merge this branch: bzr merge lp:~bcsaller/charm-tools/remove-bundletester-dep
Reviewer Review Type Date Requested Status
Marco Ceppi (community) Approve
Review via email: mp+269810@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Marco Ceppi (marcoceppi) wrote :

LGTM

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'charmtools/compose/fetchers.py'
2--- charmtools/compose/fetchers.py 2015-08-20 21:13:28 +0000
3+++ charmtools/compose/fetchers.py 2015-09-01 21:21:03 +0000
4@@ -3,11 +3,11 @@
5 import os
6
7 import requests
8-from bundletester import fetchers
9-from bundletester.fetchers import (git, # noqa
10- Fetcher,
11- get_fetcher,
12- FetchError)
13+from charmtools import fetchers
14+from charmtools.fetchers import (git, # noqa
15+ Fetcher,
16+ get_fetcher,
17+ FetchError)
18
19 from path import path
20
21
22=== added file 'charmtools/fetchers.py'
23--- charmtools/fetchers.py 1970-01-01 00:00:00 +0000
24+++ charmtools/fetchers.py 2015-09-01 21:21:03 +0000
25@@ -0,0 +1,318 @@
26+import logging
27+import os
28+import re
29+import shlex
30+import shutil
31+import subprocess
32+import tempfile
33+
34+import requests
35+import yaml
36+
37+from charmworldlib.bundle import Bundle
38+
39+log = logging.getLogger(__name__)
40+
41+REQUEST_TIMEOUT_SECS = 45
42+
43+
44+def get(*args, **kw):
45+ if 'timeout' not in kw:
46+ kw['timeout'] = REQUEST_TIMEOUT_SECS
47+
48+ return requests.get(*args, **kw)
49+
50+
51+def rename(dir_):
52+ """If ``dir_`` is a charm directory, rename it to match the
53+ charm name, otherwise do nothing.
54+
55+ :param dir_: directory path
56+ :return: the new directory name (possibly unchanged).
57+
58+ """
59+ dir_ = dir_.rstrip(os.sep)
60+ metadata = os.path.join(dir_, "metadata.yaml")
61+ if not os.path.exists(metadata):
62+ return dir_
63+ metadata = yaml.safe_load(open(metadata))
64+ name = metadata.get("name")
65+ if not name:
66+ return dir_
67+ new_dir = os.path.join(os.path.dirname(dir_), name)
68+ os.rename(dir_, new_dir)
69+ return new_dir
70+
71+
72+class Fetcher(object):
73+ def __init__(self, url, **kw):
74+ self.revision = ''
75+ self.url = url
76+ for k, v in kw.items():
77+ setattr(self, k, v)
78+
79+ @classmethod
80+ def can_fetch(cls, url):
81+ match = cls.MATCH.search(url)
82+ return match.groupdict() if match else {}
83+
84+ def get_revision(self, dir_):
85+ dirlist = os.listdir(dir_)
86+ if '.bzr' in dirlist:
87+ rev_info = check_output('bzr revision-info', cwd=dir_)
88+ return rev_info.split()[1]
89+ elif '.git' in dirlist:
90+ return check_output('git rev-parse HEAD', cwd=dir_)
91+ elif '.hg' in dirlist:
92+ return check_output(
93+ "hg log -l 1 --template '{node}\n' -r .", cwd=dir_)
94+ else:
95+ return self.revision
96+
97+
98+class BzrFetcher(Fetcher):
99+ MATCH = re.compile(r"""
100+ ^(lp:|launchpad:|https?://((code|www)\.)?launchpad.net/)
101+ (?P<repo>[^@]*)(@(?P<revision>.*))?$
102+ """, re.VERBOSE)
103+
104+ @classmethod
105+ def can_fetch(cls, url):
106+ matchdict = super(BzrFetcher, cls).can_fetch(url)
107+ return matchdict if '/+merge/' not in matchdict.get('repo', '') else {}
108+
109+ def fetch(self, dir_):
110+ dir_ = tempfile.mkdtemp(dir=dir_)
111+ url = 'lp:' + self.repo
112+ cmd = 'branch --use-existing-dir {} {}'.format(url, dir_)
113+ if self.revision:
114+ cmd = '{} -r {}'.format(cmd, self.revision)
115+ bzr(cmd)
116+ return rename(dir_)
117+
118+
119+class BzrMergeProposalFetcher(BzrFetcher):
120+ @classmethod
121+ def can_fetch(cls, url):
122+ matchdict = super(BzrFetcher, cls).can_fetch(url)
123+ return matchdict if '/+merge/' in matchdict.get('repo', '') else {}
124+
125+ def fetch(self, dir_):
126+ dir_ = tempfile.mkdtemp(dir=dir_)
127+ api_base = 'https://api.launchpad.net/devel/'
128+ url = api_base + self.repo
129+ merge_data = get(url).json()
130+ target = 'lp:' + merge_data['target_branch_link'][len(api_base):]
131+ source = 'lp:' + merge_data['source_branch_link'][len(api_base):]
132+ bzr('branch --use-existing-dir {} {}'.format(target, dir_))
133+ bzr('merge {}'.format(source), cwd=dir_)
134+ bzr('commit --unchanged -m "Merge commit"', cwd=dir_)
135+ return rename(dir_)
136+
137+
138+class GithubFetcher(Fetcher):
139+ MATCH = re.compile(r"""
140+ ^(gh:|github:|https?://(www\.)?github.com/)
141+ (?P<repo>[^@]*)(@(?P<revision>.*))?$
142+ """, re.VERBOSE)
143+
144+ def fetch(self, dir_):
145+ dir_ = tempfile.mkdtemp(dir=dir_)
146+ url = 'https://github.com/' + self.repo
147+ git('clone {} {}'.format(url, dir_))
148+ if self.revision:
149+ git('checkout {}'.format(self.revision), cwd=dir_)
150+ return rename(dir_)
151+
152+
153+class BitbucketFetcher(Fetcher):
154+ MATCH = re.compile(r"""
155+ ^(bb:|bitbucket:|https?://(www\.)?bitbucket.org/)
156+ (?P<repo>[^@]*)(@(?P<revision>.*))?$
157+ """, re.VERBOSE)
158+
159+ def fetch(self, dir_):
160+ dir_ = tempfile.mkdtemp(dir=dir_)
161+ url = 'https://bitbucket.org/' + self.repo
162+ if url.endswith('.git'):
163+ return self._fetch_git(url, dir_)
164+ return self._fetch_hg(url, dir_)
165+
166+ def _fetch_git(self, url, dir_):
167+ git('clone {} {}'.format(url, dir_))
168+ if self.revision:
169+ git('checkout {}'.format(self.revision), cwd=dir_)
170+ return rename(dir_)
171+
172+ def _fetch_hg(self, url, dir_):
173+ cmd = 'clone {} {}'.format(url, dir_)
174+ if self.revision:
175+ cmd = '{} -u {}'.format(cmd, self.revision)
176+ hg(cmd)
177+ return rename(dir_)
178+
179+
180+class LocalFetcher(Fetcher):
181+ @classmethod
182+ def can_fetch(cls, url):
183+ src = os.path.abspath(
184+ os.path.join(os.getcwd(), os.path.expanduser(url)))
185+ if os.path.exists(src):
186+ return dict(path=src)
187+ return {}
188+
189+ def fetch(self, dir_):
190+ dst = os.path.join(dir_, os.path.basename(self.path.rstrip(os.sep)))
191+ shutil.copytree(self.path, dst, symlinks=True)
192+ return dst
193+
194+
195+class StoreCharm(object):
196+ STORE_URL = 'https://store.juju.ubuntu.com/charm-info'
197+
198+ def __init__(self, name):
199+ self.name = name
200+ self.data = self.fetch()
201+
202+ def __getattr__(self, key):
203+ return self.data[key]
204+
205+ def fetch(self):
206+ params = {
207+ 'stats': 0,
208+ 'charms': self.name,
209+ }
210+ r = get(self.STORE_URL, params=params).json()
211+ charm_data = r[self.name]
212+ if 'errors' in charm_data:
213+ raise FetchError(
214+ 'Error retrieving "{}" from charm store: {}'.format(
215+ self.name, '; '.join(charm_data['errors']))
216+ )
217+ return charm_data
218+
219+
220+class CharmstoreDownloader(Fetcher):
221+ MATCH = re.compile(r"""
222+ ^cs:(?P<charm>.*)$
223+ """, re.VERBOSE)
224+
225+ STORE_URL = 'https://store.juju.ubuntu.com/charm/'
226+
227+ def __init__(self, *args, **kw):
228+ super(CharmstoreDownloader, self).__init__(*args, **kw)
229+ self.charm = StoreCharm(self.charm)
230+
231+ def fetch(self, dir_):
232+ url = self.charm.data['canonical-url'][len('cs:'):]
233+ url = self.STORE_URL + url
234+ archive = self.download_file(url, dir_)
235+ charm_dir = self.extract_archive(archive, dir_)
236+ return rename(charm_dir)
237+
238+ def extract_archive(self, archive, dir_):
239+ tempdir = tempfile.mkdtemp(dir=dir_)
240+ log.debug("Extracting %s to %s", archive, tempdir)
241+ # Can't extract with python due to bug that drops file
242+ # permissions: http://bugs.python.org/issue15795
243+ # In particular, it's important that executable test files in the
244+ # archive remain executable, otherwise the tests won't be run.
245+ # Instead we use a shell equivalent of the following:
246+ # archive = zipfile.ZipFile(archive, 'r')
247+ # archive.extractall(tempdir)
248+ check_call('unzip {} -d {}'.format(archive, tempdir))
249+ return tempdir
250+
251+ def download_file(self, url, dir_):
252+ _, filename = tempfile.mkstemp(dir=dir_)
253+ log.debug("Downloading %s", url)
254+ r = get(url, stream=True)
255+ with open(filename, 'wb') as f:
256+ for chunk in r.iter_content(chunk_size=1024):
257+ if chunk: # filter out keep-alive new chunks
258+ f.write(chunk)
259+ f.flush()
260+ return filename
261+
262+ def get_revision(self, dir_):
263+ return self.charm.revision
264+
265+
266+class BundleDownloader(Fetcher):
267+ MATCH = re.compile(r"""
268+ ^bundle:(?P<bundle>.*)$
269+ """, re.VERBOSE)
270+
271+ def fetch(self, dir_):
272+ url = Bundle(self.bundle).deployer_file_url
273+ bundle_dir = self.download_file(url, dir_)
274+ return bundle_dir
275+
276+ def download_file(self, url, dir_):
277+ bundle_dir = tempfile.mkdtemp(dir=dir_)
278+ bundle_file = os.path.join(bundle_dir, 'bundles.yaml')
279+ log.debug("Downloading %s to %s", url, bundle_file)
280+ r = get(url, stream=True)
281+ with open(bundle_file, 'w') as f:
282+ for chunk in r.iter_content(chunk_size=1024):
283+ if chunk: # filter out keep-alive new chunks
284+ f.write(chunk)
285+ f.flush()
286+ return bundle_dir
287+
288+ def get_revision(self, dir_):
289+ return Bundle(self.bundle).basket_revision
290+
291+
292+def bzr(cmd, **kw):
293+ check_call('bzr ' + cmd, **kw)
294+
295+
296+def git(cmd, **kw):
297+ check_call('git ' + cmd, **kw)
298+
299+
300+def hg(cmd, **kw):
301+ check_call('hg ' + cmd, **kw)
302+
303+
304+def check_call(cmd, **kw):
305+ return check_output(cmd, **kw)
306+
307+
308+class FetchError(Exception):
309+ pass
310+
311+
312+def check_output(cmd, **kw):
313+ args = shlex.split(cmd)
314+ p = subprocess.Popen(
315+ args,
316+ stdout=subprocess.PIPE,
317+ stderr=subprocess.STDOUT,
318+ **kw
319+ )
320+ out, _ = p.communicate()
321+ if p.returncode != 0:
322+ raise FetchError(out)
323+ log.debug('%s: %s', cmd, out)
324+ return out
325+
326+
327+FETCHERS = [
328+ BzrFetcher,
329+ BzrMergeProposalFetcher,
330+ GithubFetcher,
331+ BitbucketFetcher,
332+ LocalFetcher,
333+ CharmstoreDownloader,
334+ BundleDownloader,
335+]
336+
337+
338+def get_fetcher(url):
339+ for fetcher in FETCHERS:
340+ matchdict = fetcher.can_fetch(url)
341+ if matchdict:
342+ return fetcher(url, **matchdict)
343+ raise FetchError('No fetcher for url: %s' % url)
344
345=== modified file 'requirements.txt'
346--- requirements.txt 2015-08-27 19:40:24 +0000
347+++ requirements.txt 2015-09-01 21:21:03 +0000
348@@ -1,6 +1,5 @@
349 PyYAML==3.11
350 blessings==1.6
351-bundletester==0.5.2
352 bzr>=2.6.0
353 charmworldlib>=0.4.2
354 coverage==3.7.1
355
356=== modified file 'setup.py'
357--- setup.py 2015-09-01 14:27:31 +0000
358+++ setup.py 2015-09-01 21:21:03 +0000
359@@ -14,7 +14,7 @@
360 install_requires=['launchpadlib', 'argparse', 'cheetah', 'pyyaml',
361 'pycrypto', 'paramiko', 'bzr', 'requests',
362 'charmworldlib', 'blessings', 'ruamel.yaml',
363- 'pathspec', 'bundletester', 'otherstuf', "path.py",
364+ 'pathspec', 'otherstuf', "path.py",
365 "jujubundlelib"],
366 include_package_data=True,
367 maintainer='Marco Ceppi',

Subscribers

People subscribed via source and target branches