Package kenozooid :: Module logbook

Source Code for Module kenozooid.logbook

  1  # 
  2  # Kenozooid - dive planning and analysis toolbox. 
  3  # 
  4  # Copyright (C) 2009-2019 by Artur Wroblewski <wrobell@riseup.net> 
  5  # 
  6  # This program is free software: you can redistribute it and/or modify 
  7  # it under the terms of the GNU General Public License as published by 
  8  # the Free Software Foundation, either version 3 of the License, or 
  9  # (at your option) any later version. 
 10  # 
 11  # This program is distributed in the hope that it will be useful, 
 12  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 13  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 14  # GNU General Public License for more details. 
 15  # 
 16  # You should have received a copy of the GNU General Public License 
 17  # along with this program.  If not, see <http://www.gnu.org/licenses/>. 
 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   
42 -def find_dive_nodes(files, nodes=None, dives=None):
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
66 -def find_dive_gas_nodes(files, nodes=None):
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
88 -def find_dives(files, nodes=None, dives=None):
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
109 -def list_dives(dives):
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
204 -def upgrade_file(fin):
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)) # previous versions 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
233 -def copy_dives(files, nodes, n_dives, lfile):
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
283 -def enum_dives(files, total=1):
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) # flatten data 305 data = sorted(data, key=itemgetter(2)) 306 307 # enumerate dives with _day_ dive number and flatten the groups 308 data = ichain(enumerate(g, 1) for k, g in 309 itertools.groupby(data, itemgetter(2))) 310 311 # enumerate dives with total dive number and transform into 312 # { (f, id) => (n, k) } 313 cache = dict(((v[0], v[1]), (n, k)) for n, (k, v) in enumerate(data, total)) 314 315 # update data 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 # vim: sw=4:et:ai 332