Merge lp:~bcsaller/charm-tools/remove-bundletester-dep into lp:charm-tools/1.6
- remove-bundletester-dep
- Merge into 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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Marco Ceppi (community) | Approve | ||
Review via email: mp+269810@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
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', |
LGTM