Merge lp:~abentley/juju-release-tools/stanzas-to-streams into lp:juju-release-tools

Proposed by Aaron Bentley
Status: Merged
Merged at revision: 222
Proposed branch: lp:~abentley/juju-release-tools/stanzas-to-streams
Merge into: lp:juju-release-tools
Prerequisite: lp:~abentley/juju-release-tools/fix-lint
Diff against target: 258 lines (+249/-0)
2 files modified
stanzas_to_streams.py (+95/-0)
tests/test_stanzas_to_streams.py (+154/-0)
To merge this branch: bzr merge lp:~abentley/juju-release-tools/stanzas-to-streams
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+274010@code.launchpad.net

Commit message

Implement stanzas_to_streams

Description of the change

This branch implements stanzas-to-streams, which we use to create simplestreams.

The input is a list of items, with the same data provided by sstream-query.
Any number of files may be supplied, up to OS limits.

The output is formatted as juju expects:
  - index2.json is the real index
  - index.json provides com.ubuntu.juju:released:tools at most

For compatibility with Windows, all content_id files use '-' rather than ':' as a separator.

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'stanzas_to_streams.py'
2--- stanzas_to_streams.py 1970-01-01 00:00:00 +0000
3+++ stanzas_to_streams.py 2015-10-09 17:43:29 +0000
4@@ -0,0 +1,95 @@
5+#!/usr/bin/env python3
6+# Copyright (C) 2013, 2015 Canonical Ltd.
7+
8+from argparse import ArgumentParser
9+import json
10+import os
11+import sys
12+
13+from simplestreams import util
14+
15+from generate_simplestreams import (
16+ FileNamer,
17+ Item,
18+ items2content_trees,
19+ json_dump,
20+ write_streams,
21+ )
22+
23+
24+class JujuFileNamer(FileNamer):
25+
26+ @classmethod
27+ def get_index_path(cls):
28+ return "%s/%s" % (cls.streamdir, 'index2.json')
29+
30+ @classmethod
31+ def get_content_path(cls, content_id):
32+ return "%s/%s.json" % (cls.streamdir, content_id.replace(':', '-'))
33+
34+
35+def dict_to_item(item_dict):
36+ """Convert a dict into an Item, mutating input."""
37+ item_dict.pop('item_url', None)
38+ item_dict['size'] = int(item_dict['size'])
39+ content_id = item_dict.pop('content_id')
40+ product_name = item_dict.pop('product_name')
41+ version_name = item_dict.pop('version_name')
42+ item_name = item_dict.pop('item_name')
43+ return Item(content_id, product_name, version_name, item_name, item_dict)
44+
45+
46+def read_items_file(filename):
47+ with open(filename) as items_file:
48+ item_list = json.load(items_file)
49+ return (dict_to_item(item) for item in item_list)
50+
51+
52+def write_release_index(out_d):
53+ in_path = os.path.join(out_d, JujuFileNamer.get_index_path())
54+ with open(in_path) as in_file:
55+ full_index = json.load(in_file)
56+ full_index['index'] = dict(
57+ (k, v) for k, v in list(full_index['index'].items())
58+ if k == 'com.ubuntu.juju:released:tools')
59+ out_path = os.path.join(out_d, FileNamer.get_index_path())
60+ json_dump(full_index, out_path)
61+ return out_path
62+
63+
64+def filenames_to_streams(filenames, updated, out_d):
65+ """Convert a list of filenames into simplestreams.
66+
67+ File contents must be json simplestream stanzas.
68+ 'updated' is the date to use for 'updated' in the streams.
69+ out_d is the directory to create streams in.
70+ """
71+ items = []
72+ for items_file in filenames:
73+ items.extend(read_items_file(items_file))
74+
75+ data = {'updated': updated, 'datatype': 'content-download'}
76+ trees = items2content_trees(items, data)
77+ out_filenames = write_streams(out_d, trees, updated, JujuFileNamer)
78+ out_filenames.append(write_release_index(out_d))
79+
80+
81+def parse_args(argv=None):
82+ parser = ArgumentParser()
83+ parser.add_argument(
84+ 'items_file', metavar='items-file', help='File to read items from',
85+ nargs='+')
86+ parser.add_argument(
87+ 'out_d', metavar='output-dir',
88+ help='The directory to write stream files to.')
89+ return parser.parse_args(argv)
90+
91+
92+def main():
93+ args = parse_args()
94+ updated = util.timestamp()
95+ filenames_to_streams(args.items_file, updated, args.out_d)
96+
97+
98+if __name__ == '__main__':
99+ sys.exit(main())
100
101=== added file 'tests/test_stanzas_to_streams.py'
102--- tests/test_stanzas_to_streams.py 1970-01-01 00:00:00 +0000
103+++ tests/test_stanzas_to_streams.py 2015-10-09 17:43:29 +0000
104@@ -0,0 +1,154 @@
105+import json
106+import os
107+from StringIO import StringIO
108+from tempfile import NamedTemporaryFile
109+from unittest import TestCase
110+
111+from mock import patch
112+
113+from generate_simplestreams import (
114+ FileNamer,
115+ generate_index,
116+ Item,
117+ items2content_trees,
118+ json_dump as json_dump_verbose,
119+ )
120+from stanzas_to_streams import (
121+ dict_to_item,
122+ filenames_to_streams,
123+ JujuFileNamer,
124+ read_items_file,
125+ write_release_index,
126+ )
127+from test_generate_simplestreams import load_stream_dir
128+from utils import temp_dir
129+
130+
131+class TestJujuFileNamer(TestCase):
132+
133+ def test_get_index_path(self):
134+ self.assertEqual('streams/v1/index2.json',
135+ JujuFileNamer.get_index_path())
136+
137+ def test_get_content_path(self):
138+ self.assertEqual('streams/v1/foo-bar-baz.json',
139+ JujuFileNamer.get_content_path('foo:bar-baz'))
140+
141+
142+def json_dump(json, filename):
143+ with patch('sys.stderr', StringIO()):
144+ json_dump_verbose(json, filename)
145+
146+
147+class TestDictToItem(TestCase):
148+
149+ def test_dict_to_item(self):
150+ pedigree = {
151+ 'content_id': 'cid', 'product_name': 'pname',
152+ 'version_name': 'vname', 'item_name': 'iname',
153+ }
154+ item_dict = {'size': '27'}
155+ item_dict.update(pedigree)
156+ item = dict_to_item(item_dict)
157+ self.assertEqual(Item(data={'size': 27}, **pedigree), item)
158+
159+
160+class TestReadItemsFile(TestCase):
161+
162+ def test_read_items_file(self):
163+ pedigree = {
164+ 'content_id': 'cid', 'product_name': 'pname',
165+ 'version_name': 'vname', 'item_name': 'iname',
166+ }
167+ with NamedTemporaryFile() as items_file:
168+ item_dict = {'size': '27'}
169+ item_dict.update(pedigree)
170+ json_dump([item_dict], items_file.name)
171+ items = list(read_items_file(items_file.name))
172+ self.assertEqual([Item(data={'size': 27}, **pedigree)], items)
173+
174+
175+class TestWriteReleaseIndex(TestCase):
176+
177+ def write_full_index(self, out_d, content):
178+ os.mkdir(os.path.join(out_d, 'streams'))
179+ os.mkdir(os.path.join(out_d, 'streams/v1'))
180+ path = os.path.join(out_d, JujuFileNamer.get_index_path())
181+ json_dump(content, path)
182+
183+ def read_release_index(self, out_d):
184+ path = os.path.join(out_d, FileNamer.get_index_path())
185+ with open(path) as release_index_file:
186+ return json.load(release_index_file)
187+
188+ def test_empty_index(self):
189+ with temp_dir() as out_d:
190+ self.write_full_index(out_d, {'index': {}, 'foo': 'bar'})
191+ with patch('sys.stderr', StringIO()):
192+ write_release_index(out_d)
193+ release_index = self.read_release_index(out_d)
194+ self.assertEqual({'foo': 'bar', 'index': {}}, release_index)
195+
196+ def test_release_index(self):
197+ with temp_dir() as out_d:
198+ self.write_full_index(out_d, {
199+ 'index': {'com.ubuntu.juju:released:tools': 'foo'},
200+ 'foo': 'bar'})
201+ with patch('sys.stderr', StringIO()):
202+ write_release_index(out_d)
203+ release_index = self.read_release_index(out_d)
204+ self.assertEqual({'foo': 'bar', 'index': {
205+ 'com.ubuntu.juju:released:tools': 'foo'}
206+ }, release_index)
207+
208+ def test_multi_index(self):
209+ with temp_dir() as out_d:
210+ self.write_full_index(out_d, {
211+ 'index': {
212+ 'com.ubuntu.juju:proposed:tools': 'foo',
213+ 'com.ubuntu.juju:released:tools': 'foo',
214+ },
215+ 'foo': 'bar'})
216+ with patch('sys.stderr', StringIO()):
217+ write_release_index(out_d)
218+ release_index = self.read_release_index(out_d)
219+ self.assertEqual({'foo': 'bar', 'index': {
220+ 'com.ubuntu.juju:released:tools': 'foo'}
221+ }, release_index)
222+
223+
224+class TestFilenamesToStreams(TestCase):
225+
226+ def test_filenames_to_streams(self):
227+ item = {
228+ 'content_id': 'foo:1',
229+ 'product_name': 'bar',
230+ 'version_name': 'baz',
231+ 'item_name': 'qux',
232+ 'size': '27',
233+ }
234+ item2 = dict(item)
235+ item2.update({
236+ 'size': '42',
237+ 'item_name': 'quxx'})
238+ updated = 'updated'
239+ file_a = NamedTemporaryFile()
240+ file_b = NamedTemporaryFile()
241+ with temp_dir() as out_d, file_a, file_b:
242+ json_dump([item], file_a.name)
243+ json_dump([item2], file_b.name)
244+ stream_dir = os.path.join(out_d, 'streams/v1')
245+ with patch('sys.stderr', StringIO()):
246+ filenames_to_streams([file_a.name, file_b.name], updated,
247+ out_d)
248+ content = load_stream_dir(stream_dir)
249+ self.assertItemsEqual(content.keys(), ['index.json', 'index2.json',
250+ 'foo-1.json'])
251+ items = [dict_to_item(item), dict_to_item(item2)]
252+ trees = items2content_trees(items, {
253+ 'updated': updated, 'datatype': 'content-download'})
254+ expected = generate_index(trees, 'updated', JujuFileNamer)
255+ self.assertEqual(expected, content['index2.json'])
256+ index_expected = generate_index({}, 'updated', FileNamer)
257+ self.assertEqual(index_expected, content['index.json'])
258+ self.assertEqual(trees['foo:1'], content['foo-1.json'])

Subscribers

People subscribed via source and target branches