1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 """
21 Dive logbook functionality.
22
23 Dive, dive site and buddy data display and management is implemented.
24 """
25
26 import lxml.etree as et
27 import os.path
28 import logging
29 import itertools
30 ichain = itertools.chain.from_iterable
31 from itertools import zip_longest as lzip
32 from operator import itemgetter
33 import pkg_resources
34
35 import kenozooid.uddf as ku
36 from kenozooid.util import min2str, FMT_DIVETIME
37 from kenozooid.units import K2C
38
39 log = logging.getLogger('kenozooid.logbook')
40
41
43 """
44 Find dive nodes in UDDF files using optional numeric ranges or total
45 dive number as search parameters.
46
47 The collection of dive nodes is returned.
48
49 :Parameters:
50 files
51 Collection of UDDF files.
52 nodes
53 Numeric ranges of nodes, `None` if all nodes.
54 dives
55 Numeric range of total dive number, `None` if any dive.
56
57 .. seealso:: :py:func:`parse_range`
58 .. seealso:: :py:func:`find_dives`
59 """
60 nodes = [] if nodes is None else nodes
61 data = (ku.find(f, ku.XP_FIND_DIVES, nodes=q, dives=dives) \
62 for q, f in lzip(nodes, files))
63 return ichain(data)
64
65
67 """
68 Find gas nodes referenced by dives in UDDF files using optional node
69 ranges as search parameter.
70
71 The collection of gas nodes is returned.
72
73 :Parameters:
74 files
75 Collection of UDDF files.
76 nodes
77 Numeric ranges of nodes, `None` if all nodes.
78
79 .. seealso:: :py:func:`parse_range`
80 """
81 nodes = [] if nodes is None else nodes
82 data = (ku.find(f, ku.XP_FIND_DIVE_GASES, nodes=q) \
83 for q, f in lzip(nodes, files))
84 nodes_by_id = ((n.get('id'), n) for n in ichain(data))
85 return dict(nodes_by_id).values()
86
87
89 """
90 Find dive data in UDDF files using optional node ranges or total dive
91 number as search parameters.
92
93 The collection of dive data is returned.
94
95 :Parameters:
96 files
97 Collection of UDDF files.
98 nodes
99 Numeric ranges of nodes, `None` if all nodes.
100 dives
101 Numeric range of total dive number, `None` if any dive.
102
103 .. seealso:: :py:func:`parse_range`
104 .. seealso:: :py:func:`find_dive_nodes`
105 """
106 return (ku.dive_data(n) for n in find_dive_nodes(files, nodes, dives))
107
108
110 """
111 Get generator of preformatted dive data.
112
113 The dives are fetched from logbook file and for
114 each dive a tuple of formatted dive information
115 is returned
116
117 - dive number, i.e. 102
118 - date and time of dive, i.e. 2011-03-19 14:56
119 - maximum depth, i.e. 6.0m
120 - dive average depth, i.e. 2.0m
121 - duration of dive, i.e. 33:42
122 - temperature, i.e. 8.2°C
123
124 :Parameters:
125 dives
126 Collection of dive data.
127 """
128 for dive in dives:
129 try:
130 duration = min2str(dive.duration / 60.0)
131 depth = '{:.1f}m'.format(dive.depth)
132 temp = ''
133 if dive.temp is not None:
134 temp = '{:.1f}\u00b0C'.format(K2C(dive.temp))
135 avg_depth = ''
136 if dive.avg_depth is not None:
137 avg_depth = '{:.1f}m'.format(dive.avg_depth)
138 yield (dive.number, format(dive.datetime, FMT_DIVETIME), depth,
139 avg_depth, duration, temp)
140 except TypeError as ex:
141 log.debug(ex)
142 log.warn('invalid dive data, skipping dive')
143
144
145 -def add_dive(dive, lfile, qsite=None, qbuddies=()):
146 """
147 Add new dive to logbook file.
148
149 The logbook file is created if it does not exist.
150
151 If dive number is specified and dive cannot be found then ValueError
152 exception is thrown.
153
154 :Parameters:
155 dive
156 Dive data.
157 lfile
158 Logbook file.
159 qsite
160 Dive site search term.
161 qbuddies
162 Buddy search terms.
163 """
164 if os.path.exists(lfile):
165 doc = et.parse(lfile).getroot()
166 else:
167 doc = ku.create()
168
169 if qbuddies is None:
170 qbuddies = []
171
172 site_id = None
173 if qsite:
174 nodes = ku.find(lfile, ku.XP_FIND_SITE, site=qsite)
175 n = next(nodes, None)
176 if n is None:
177 raise ValueError('Cannot find dive site in logbook file')
178 if next(nodes, None) is not None:
179 raise ValueError('Found more than one dive site')
180
181 site_id = n.get('id')
182
183 buddy_ids = []
184 log.debug('looking for buddies {}'.format(qbuddies))
185 for qb in qbuddies:
186 log.debug('looking for buddy {}'.format(qb))
187 nodes = ku.find(lfile, ku.XP_FIND_BUDDY, buddy=qb)
188 n = next(nodes, None)
189 if n is None:
190 raise ValueError('Cannot find buddy {} in logbook file'.format(qb))
191 if next(nodes, None) is not None:
192 raise ValueError('Found more than one buddy for {}'.format(qb))
193
194 buddy_ids.append(n.get('id'))
195
196 log.debug('creating dive data')
197 ku.create_dive_data(doc, datetime=dive.datetime, depth=dive.depth,
198 duration=dive.duration, site=site_id, buddies=buddy_ids)
199
200 ku.reorder(doc)
201 ku.save(doc, lfile)
202
203
205 """
206 Upgrade UDDF file to newer version.
207
208 :Parameters:
209 fin
210 File with UDDF data to upgrade.
211 """
212 current = (3, 2)
213 versions = ((3, 0), (3, 1))
214 xslt = ('uddf-3.0.0-3.1.0.xslt', 'uddf-3.1.0-3.2.0.xslt')
215
216 ver = ku.get_version(fin)
217 if ver == current:
218 raise ValueError('File is at UDDF {}.{} version already' \
219 .format(*current))
220 try:
221 k = versions.index(ver)
222 except ValueError:
223 raise ValueError('Cannot upgrade UDDF file version {}.{}'.format(*ver))
224
225 doc = ku.parse(fin, ver_check=False)
226 for i in range(k, len(versions)):
227 fs = pkg_resources.resource_stream('kenozooid', 'uddf/{}'.format(xslt[i]))
228 transform = et.XSLT(et.parse(fs))
229 doc = transform(doc)
230 return doc
231
232
234 """
235 Copy dive nodes to logbook file.
236
237 The logbook file is created if it does not exist.
238
239 :Parameters:
240 files
241 Collection of files.
242 nodes
243 Collection of dive ranges.
244 n_dives
245 Numeric range of total dive number, `None` if any dive.
246 lfile
247 Logbook file.
248 """
249 if os.path.exists(lfile):
250 doc = et.parse(lfile).getroot()
251 else:
252 doc = ku.create()
253
254 dives = find_dive_nodes(files, nodes, n_dives)
255 gases = find_dive_gas_nodes(files, nodes)
256
257 _, rg = ku.create_node('uddf:profiledata/uddf:repetitiongroup',
258 parent=doc)
259 gn = ku.xp_first(doc, 'uddf:gasdefinitions')
260 existing = gn is not None
261 if not existing:
262 *_, gn = ku.create_node('uddf:gasdefinitions', parent=doc)
263
264 with ku.NodeCopier(doc) as nc:
265 copied = False
266 for n in gases:
267 copied = nc.copy(n, gn) is not None or copied
268 if not existing and not copied:
269 p = gn.getparent()
270 p.remove(gn)
271
272 copied = False
273 for n in dives:
274 copied = nc.copy(n, rg) is not None or copied
275
276 if copied:
277 ku.reorder(doc)
278 ku.save(doc, lfile)
279 else:
280 log.debug('no dives copied')
281
282
284 """
285 Enumerate dives with day dive number (when UDDF 3.2 is introduced) and
286 total dive number.
287
288 :Parameters:
289 files
290 Collection of UDDF files having dives to enumerate.
291 total
292 Start of total dive number.
293 """
294 fields = ('id', 'date')
295 queries = (
296 ku.XPath('@id'),
297 ku.XPath('uddf:informationbeforedive/uddf:datetime/text()'),
298 )
299 parsers = (str, lambda dt: ku.dparse(dt).date())
300
301 fnodes = ((f, n) for f in files for n in
302 ku.find(f, ku.XP_FIND_DIVES, nodes=None, dives=None))
303 data = ((f, ku.dive_data(n, fields, queries, parsers)) for f, n in fnodes)
304 data = ((item[0], item[1].id, item[1].date) for item in data)
305 data = sorted(data, key=itemgetter(2))
306
307
308 data = ichain(enumerate(g, 1) for k, g in
309 itertools.groupby(data, itemgetter(2)))
310
311
312
313 cache = dict(((v[0], v[1]), (n, k)) for n, (k, v) in enumerate(data, total))
314
315
316 for f in files:
317 doc = ku.parse(f)
318 for n in ku.XP_FIND_DIVES(doc, nodes=None, dives=None):
319 id = n.get('id')
320 dnn = ku.xp_first(n, 'uddf:informationbeforedive/uddf:divenumber')
321 if dnn is None:
322 pn = ku.xp_first(n, 'uddf:informationbeforedive/uddf:internaldivenumber')
323 if pn is None:
324 pn = ku.xp_first(n, 'uddf:informationbeforedive/uddf:datetime')
325 *_, dnn = ku.create_node('uddf:divenumber')
326 pn.addprevious(dnn)
327 dnn.text = str(cache[f, id][0])
328 ku.save(doc.getroot(), f)
329
330
331
332