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

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
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']
28COMMANDS_LIST = commands.cmdlist
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)
41def run_commands(commands, callback):
42 '''
43 For example::
45 run_commands(['a.yaml', 'b.yaml', '--x=1'], method)
47 will do the following:
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)
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)
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.')
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
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)
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)
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)
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)
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]
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)
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])
182 matrix = list(map(list, zip(*indexes)))
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)
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')
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
225 spec = change[shape.name]
226 if shape.name not in mapping:
227 mapping[shape.name] = 0
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)
240 if isinstance(shape_data, (dict, AttrDict,)):
241 shape_data['handler'] = handler
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)