Hide keyboard shortcuts

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.""" 

2import io 

3import os 

4import sys 

5import copy 

6import json 

7import collections 

8import six 

9from pptx import Presentation 

10from pptx.shapes.shapetree import SlideShapes 

11from orderedattrdict import AttrDict 

12import pandas as pd 

13import gramex.data 

14import gramex.cache 

15from gramex.config import merge 

16from gramex import parse_command_line 

17from gramex.transforms import build_transform 

18from . import commands 

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

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

21 

22 

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

24with 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 

28COMMANDS_LIST = commands.cmdlist 

29 

30 

31def 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 

41def 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 

73def 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 

94def 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 

109def 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 

123def 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 

206def 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)