Coverage for gramex\pptgen\__init__.py: 89%

# Coverage for gramex\pptgen\__init__.py : 89%

## 162 statements 145 run 17 missing 0 excluded 9 partial

Hot-keys on this page

r m x p toggle line displays

j k next/prev highlighted chunk

0 (zero) top of page

1 (one) first highlighted chunk

1"""PPTGen module."""

2 import io

3 import os

4 import sys

5 import copy

6 import json

7 import collections

8 import six

9 from pptx import Presentation

10 from pptx.shapes.shapetree import SlideShapes

11 from orderedattrdict import AttrDict

12 import pandas as pd

13 import gramex.data

14 import gramex.cache

15 from gramex.config import merge

16 from gramex import parse_command_line

17 from gramex.transforms import build_transform

18 from . import commands

19 from .utils import stack_shapes, delete_slide, generate_slide, manage_slides

20 from .utils import is_slide_allowed, is_group, add_new_slide, copy_slide_elem

21

22

23 _folder = os.path.dirname(os.path.abspath(__file__))

24 with io.open(os.path.join(_folder, 'release.json'), encoding='utf-8') as _release_file:

25 _release = json.load(_release_file)

26 __version__ = _release['version']

27

28 COMMANDS_LIST = commands.cmdlist

29

30

31 def commandline():

32 '''

33 Runs PPTGen from the command line.

34 This is called via setup.py console_scripts.

35 Though a trivial function, is is kept different from run_commands to allow

36 unit testing of run_commands.

37 '''

38 run_commands(sys.argv[1:], pptgen)

39

40

41 def run_commands(commands, callback):

42 '''

43 For example::

44

45 run_commands(['a.yaml', 'b.yaml', '--x=1'], method)

46

47 will do the following:

48

49 - Load a.yaml into config

50 - Set config['a'] = 1

51 - Change to directory where a.yaml is

52 - Call method(config)

53 - Load b.yaml into config

54 - Set config['a'] = 1

55 - Change to directory where b.yaml is

56 - Call method(config)

57

58 Command line arguments are passed as ``commands``.

59 Callback is a function that is called for each config file.

60 '''

61 args = parse_command_line(commands)

62 original_path = os.getcwd()

63 for config_file in args.pop('_'):

64 config = gramex.cache.open(config_file, 'config')

65 config = merge(old=config, new=args, mode='overwrite')

66 os.chdir(os.path.dirname(os.path.abspath(config_file)))

67 try:

68 callback(**config)

69 finally:

70 os.chdir(original_path)

71

72

73 def load_data(data_config, handler=None):

74 '''

75 Loads data using gramex cache.

76 '''

77 if not isinstance(data_config, (dict, AttrDict,)):

78 raise ValueError('Data argument must be a dict like object.')

79

80 data = {}

81 for key, conf in data_config.items():

82 if isinstance(conf, (dict, AttrDict,)):

83 if 'function' in conf: 83 ↛ 84line 83 didn't jump to line 84, because the condition on line 83 was never true

84 data[key] = build_transform(conf, vars={'handler': None})(handler=handler)[0]

85 elif conf.get('ext') in {'yaml', 'yml', 'json'}: 85 ↛ 86line 85 didn't jump to line 86, because the condition on line 85 was never true

86 data[key] = gramex.cache.open(conf.pop('url'), conf.pop('ext'), **dict(conf))

87 elif 'url' in conf: 87 ↛ 88line 87 didn't jump to line 88, because the condition on line 87 was never true

88 data[key] = gramex.data.filter(conf.pop('url'), **dict(conf))

89 else:

90 data[key] = conf

91 return data

92

93

94 def replicate_slides(data, prs, change, slide, slides_to_remove, index, handler):

95 '''

96 Function to replicate slides.

97 '''

98 if isinstance(data, pd.DataFrame): 98 ↛ 100line 98 didn't jump to line 100, because the condition on line 98 was never false

99 data = data.to_dict(orient='records')

100 copy_slide = copy.deepcopy(slide)

101 slides_to_remove.append(index)

102 # Stacking shapes if required.

103 stack_shapes(copy_slide.shapes, change, data, handler)

104 new_slide = generate_slide(prs, copy_slide)

105 args = {'prs': prs, 'copy_slide': True, 'source_slide': slide, 'new_slide': new_slide}

106 change_shapes(copy_slide.shapes, change, data, handler, **args)

107

108

109 def register(config):

110 """Function to register a new `command` to command list."""

111 global COMMANDS_LIST

112 resister_command = config.pop('register', {})

113 if not isinstance(resister_command, (dict,)):

114 raise ValueError('Register should be a dict like object')

115 for command_name, command_function in resister_command.items():

116 if command_name not in COMMANDS_LIST: 116 ↛ 115line 116 didn't jump to line 115, because the condition on line 116 was never false

117 if not isinstance(command_function, (dict,)):

118 command_function = {'function': command_function}

119 _vars = {'shape': None, 'spec': None, 'data': None}

120 COMMANDS_LIST[command_name] = build_transform(command_function, vars=_vars)

121

122

123 def pptgen(source, target=None, **config):

124 '''

125 Process a configuration. This loads a Presentation from source, applies the

126 (optional) configuration changes and saves it into target.

127 '''

128 # Config was being over written using PPTXHandler and data key was being

129 # removed from yaml config.

130 handler = config.pop('handler', None)

131 _config = copy.deepcopy(config)

132 if _config.get('is_formhandler', False): 132 ↛ 133line 132 didn't jump to line 133, because the condition on line 132 was never true

133 data = _config.pop('data')

134 _config.pop('is_formhandler')

135 else:

136 data = AttrDict(load_data(_config.pop('data', {}), handler=handler))

137 # Register a `command` if present in configuration

138 register(_config)

139

140 # Loading input template

141 prs = Presentation(source)

142 # Removing not required slides from presentation.

143 prs = manage_slides(prs, _config)

144 slides = prs.slides

145 # Loop through each change configuration

146 slides_to_remove = []

147 manage_slide_order = collections.defaultdict(list)

148

149 for key, change in _config.items():

150 # Apply it to every slide

151 slide_data = copy.deepcopy(data)

152 if 'data' in change and change['data'] is not None:

153 if not isinstance(change['data'], (dict,)): 153 ↛ 155line 153 didn't jump to line 155, because the condition on line 153 was never false

154 change['data'] = {'function': change.pop('data')}

155 slide_data = build_transform(change['data'], vars={'data': None})(slide_data)[0]

156

157 for index, slide in enumerate(slides):

158 # Restrict to specific slides, if specified

159 if not is_slide_allowed(change, slide, index + 1): 159 ↛ 160line 159 didn't jump to line 160, because the condition on line 159 was never true

160 continue

161 if change.get('replicate'):

162 is_grp = isinstance(slide_data, pd.core.groupby.DataFrameGroupBy)

163 if isinstance(slide_data, collections.Iterable): 163 ↛ 171line 163 didn't jump to line 171, because the condition on line 163 was never false

164 for _slide_data in slide_data:

165 _slide_data = _slide_data[1] if is_grp is True else _slide_data

166 replicate_slides(

167 _slide_data, prs, change, slide, slides_to_remove, index, handler)

168 # Creating dict mapping to order slides.

169 manage_slide_order[index + 1].append(len(prs.slides))

170 else:

171 raise NotImplementedError()

172 else:

173 # Stacking shapes if required.

174 stack_shapes(slide.shapes, change, slide_data, handler)

175 change_shapes(slide.shapes, change, slide_data, handler)

176

177 indexes = []

178 slides_to_remove = list(set(slides_to_remove))

179 for key in sorted(manage_slide_order.keys()):

180 indexes.append(manage_slide_order[key])

181

182 matrix = list(map(list, zip(*indexes)))

183

184 for indx_lst in matrix:

185 for idx in indx_lst:

186 src = prs.slides[idx - 1]

187 slides_to_remove.append(idx - 1)

188 copy_slide = copy.deepcopy(src)

189 new_slide = generate_slide(prs, copy_slide)

190 dest = prs.slides.add_slide(new_slide)

191 for shape in copy_slide.shapes:

192 copy_slide_elem(shape, dest)

193 add_new_slide(dest, src)

194 removed_status = 0

195 for sld_idx in set(slides_to_remove):

196 delete_slide(prs, (sld_idx - removed_status))

197 for slide_num in manage_slide_order:

198 manage_slide_order[slide_num] = [(i - 1) for i in manage_slide_order[slide_num]]

199 removed_status += 1

200 if target is None:

201 return prs

202 else:

203 prs.save(target)

204

205

206 def change_shapes(collection, change, data, handler, **kwargs):

207 '''

208 Apply changes to a collection of shapes in the context of data.

209 ``collection`` is a slide.shapes or group shapes.

210 ``change`` is typically a dict of <shape-name>: commands.

211 ``data`` is a dictionary passed to the template engine.

212 '''

213 prs = kwargs.get('prs')

214 new_slide = kwargs.get('new_slide')

215 copy_slide = kwargs.get('copy_slide', False)

216 source_slide = kwargs.get('source_slide')

217

218 dest = prs.slides.add_slide(new_slide) if copy_slide else None

219 mapping = {}

220 for shape in collection:

221 if shape.name not in change:

222 copy_slide_elem(shape, dest)

223 continue

224

225 spec = change[shape.name]

226 if shape.name not in mapping:

227 mapping[shape.name] = 0

228

229 if spec.get('data'):

230 if not isinstance(spec['data'], (dict,)):

231 spec['data'] = {'function': '{}'.format(spec['data']) if not isinstance(

232 spec['data'], (str, six.string_types,)) else spec['data']}

233 shape_data = build_transform(

234 spec['data'], vars={'data': None, 'handler': None})(data=data, handler=handler)[0]

235 else:

236 if isinstance(data, (dict, AttrDict,)) and 'handler' in data:

237 data.pop('handler')

238 shape_data = copy.deepcopy(data)

239

240 if isinstance(shape_data, (dict, AttrDict,)):

241 shape_data['handler'] = handler

242

243 if spec.get('stack'):

244 shape_data = shape_data[mapping[shape.name]]

245 mapping[shape.name] = mapping[shape.name] + 1

246 # If the shape is a group, apply spec to each sub-shape

247 if is_group(shape):

248 sub_shapes = SlideShapes(shape.element, collection)

249 change_shapes(sub_shapes, spec, shape_data, handler)

250 # Add args to shape_data

251 if hasattr(handler, 'args'):

252 args = {k: v[0] for k, v in handler.args.items() if len(v) > 0}

253 shape_data['args'] = args

254 # Run commands in the spec

255 for cmd, method in COMMANDS_LIST.items():

256 if cmd in spec:

257 method(shape, spec, shape_data)

258 copy_slide_elem(shape, dest)

259 add_new_slide(dest, source_slide)