Package Gnumed :: Package timelinelib :: Package dataimport :: Module timelinexml
[frames] | no frames]

Source Code for Module Gnumed.timelinelib.dataimport.timelinexml

  1  # Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018  Rickard Lindberg, Roger Lindberg 
  2  # 
  3  # This file is part of Timeline. 
  4  # 
  5  # Timeline is free software: you can redistribute it and/or modify 
  6  # it under the terms of the GNU General Public License as published by 
  7  # the Free Software Foundation, either version 3 of the License, or 
  8  # (at your option) any later version. 
  9  # 
 10  # Timeline is distributed in the hope that it will be useful, 
 11  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 12  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 13  # GNU General Public License for more details. 
 14  # 
 15  # You should have received a copy of the GNU General Public License 
 16  # along with Timeline.  If not, see <http://www.gnu.org/licenses/>. 
 17   
 18   
 19  from os.path import abspath 
 20  import base64 
 21  import re 
 22  import shutil 
 23  import StringIO 
 24   
 25  import wx 
 26   
 27  from timelinelib.calendar.bosparanian.timetype import BosparanianTimeType 
 28  from timelinelib.calendar.gregorian.timetype import GregorianTimeType 
 29  from timelinelib.calendar.num.timetype import NumTimeType 
 30  from timelinelib.canvas.data.db import MemoryDB 
 31  from timelinelib.canvas.data.exceptions import TimelineIOError 
 32  from timelinelib.canvas.data import Category 
 33  from timelinelib.canvas.data import Container 
 34  from timelinelib.canvas.data import Era 
 35  from timelinelib.canvas.data import Event 
 36  from timelinelib.canvas.data import Subevent 
 37  from timelinelib.canvas.data import TimePeriod 
 38  from timelinelib.canvas.data.milestone import Milestone 
 39  from timelinelib.db.utils import create_non_exising_path 
 40  from timelinelib.general.xmlparser import ANY 
 41  from timelinelib.general.xmlparser import OPTIONAL 
 42  from timelinelib.general.xmlparser import parse 
 43  from timelinelib.general.xmlparser import parse_fn_store 
 44  from timelinelib.general.xmlparser import SINGLE 
 45  from timelinelib.general.xmlparser import Tag 
 46  from timelinelib.utils import ex_msg 
 47   
 48   
49 -def import_db_from_timeline_xml(path):
50 db = MemoryDB() 51 db.path = path 52 db.set_time_type(GregorianTimeType()) 53 Parser(db, path).parse() 54 db.clear_transactions() 55 return db
56 57
58 -class ParseException(Exception):
59 """Thrown if parsing of data read from file fails.""" 60 pass
61 62
63 -class Parser(object):
64
65 - def __init__(self, db, path):
66 self.db = db 67 self.path = path 68 self._containers_by_cid = {}
69
70 - def parse(self):
71 self._load()
72
73 - def _load(self):
74 try: 75 # _parse_version will create the rest of the schema dynamically 76 partial_schema = Tag("timeline", SINGLE, None, [ 77 Tag("version", SINGLE, self._parse_version) 78 ]) 79 tmp_dict = { 80 "partial_schema": partial_schema, 81 "category_map": {}, 82 "hidden_categories": [], 83 } 84 parse(self.path, partial_schema, tmp_dict) 85 except Exception as e: 86 msg = _("Unable to read timeline data from '%s'.") 87 whole_msg = (msg + "\n\n%s") % (abspath(self.path), ex_msg(e)) 88 raise TimelineIOError(whole_msg)
89
90 - def _parse_version(self, text, tmp_dict):
91 match = re.search(r"^(\d+).(\d+).(\d+)(.*)$", text) 92 if match: 93 (x, y, z) = (int(match.group(1)), int(match.group(2)), 94 int(match.group(3))) 95 self._backup((x, y, z)) 96 tmp_dict["version"] = (x, y, z) 97 self._create_rest_of_schema(tmp_dict) 98 else: 99 raise ParseException("Could not parse version number from '%s'." 100 % text)
101
102 - def _backup(self, current_version):
103 (x, _, _) = current_version 104 if x == 0: 105 shutil.copy(self.path, 106 create_non_exising_path(self.path, "pre100bak"))
107
108 - def _create_rest_of_schema(self, tmp_dict):
109 """ 110 Ensure all versions of the xml format can be parsed with this schema. 111 112 tmp_dict["version"] can be used to create different schemas depending 113 on the version. 114 """ 115 tmp_dict["partial_schema"].add_child_tags([ 116 Tag("timetype", OPTIONAL, self._parse_timetype), 117 Tag("eras", OPTIONAL, None, [ 118 Tag("era", ANY, self._parse_era, [ 119 Tag("name", SINGLE, parse_fn_store("tmp_name")), 120 Tag("start", SINGLE, parse_fn_store("tmp_start")), 121 Tag("end", SINGLE, parse_fn_store("tmp_end")), 122 Tag("color", SINGLE, parse_fn_store("tmp_color")), 123 Tag("ends_today", OPTIONAL, parse_fn_store("tmp_ends_today")), 124 ]) 125 ]), 126 Tag("categories", SINGLE, None, [ 127 Tag("category", ANY, self._parse_category, [ 128 Tag("name", SINGLE, parse_fn_store("tmp_name")), 129 Tag("color", SINGLE, parse_fn_store("tmp_color")), 130 Tag("progress_color", OPTIONAL, parse_fn_store("tmp_progress_color")), 131 Tag("done_color", OPTIONAL, parse_fn_store("tmp_done_color")), 132 Tag("font_color", OPTIONAL, parse_fn_store("tmp_font_color")), 133 Tag("parent", OPTIONAL, parse_fn_store("tmp_parent")), 134 ]) 135 ]), 136 Tag("events", SINGLE, None, [ 137 Tag("event", ANY, self._parse_event, [ 138 Tag("start", SINGLE, parse_fn_store("tmp_start")), 139 Tag("end", SINGLE, parse_fn_store("tmp_end")), 140 Tag("text", SINGLE, parse_fn_store("tmp_text")), 141 Tag("progress", OPTIONAL, parse_fn_store("tmp_progress")), 142 Tag("fuzzy", OPTIONAL, parse_fn_store("tmp_fuzzy")), 143 Tag("locked", OPTIONAL, parse_fn_store("tmp_locked")), 144 Tag("ends_today", OPTIONAL, parse_fn_store("tmp_ends_today")), 145 Tag("category", OPTIONAL, parse_fn_store("tmp_category")), 146 Tag("description", OPTIONAL, parse_fn_store("tmp_description")), 147 Tag("alert", OPTIONAL, parse_fn_store("tmp_alert")), 148 Tag("hyperlink", OPTIONAL, parse_fn_store("tmp_hyperlink")), 149 Tag("icon", OPTIONAL, parse_fn_store("tmp_icon")), 150 Tag("default_color", OPTIONAL, parse_fn_store("tmp_default_color")), 151 Tag("milestone", OPTIONAL, parse_fn_store("tmp_milestone")), 152 ]) 153 ]), 154 Tag("view", SINGLE, None, [ 155 Tag("displayed_period", OPTIONAL, 156 self._parse_displayed_period, [ 157 Tag("start", SINGLE, parse_fn_store("tmp_start")), 158 Tag("end", SINGLE, parse_fn_store("tmp_end")), 159 ]), 160 Tag("hidden_categories", OPTIONAL, 161 self._parse_hidden_categories, [ 162 Tag("name", ANY, self._parse_hidden_category), 163 ]), 164 ]), 165 Tag("now", OPTIONAL, self._parse_saved_now), 166 ])
167
168 - def _parse_timetype(self, text, tmp_dict):
169 self.db.set_time_type(None) 170 valid_time_types = (GregorianTimeType(), BosparanianTimeType(), NumTimeType()) 171 for timetype in valid_time_types: 172 if text == timetype.get_name(): 173 self.db.set_time_type(timetype) 174 break 175 if self.db.get_time_type() is None: 176 raise ParseException("Invalid timetype '%s' found." % text)
177
178 - def _parse_category(self, text, tmp_dict):
179 name = tmp_dict.pop("tmp_name") 180 color = parse_color(tmp_dict.pop("tmp_color")) 181 progress_color = self._parse_optional_color(tmp_dict, "tmp_progress_color", None) 182 done_color = self._parse_optional_color(tmp_dict, "tmp_done_color", None) 183 font_color = self._parse_optional_color(tmp_dict, "tmp_font_color") 184 parent_name = tmp_dict.pop("tmp_parent", None) 185 if parent_name: 186 parent = tmp_dict["category_map"].get(parent_name, None) 187 if parent is None: 188 raise ParseException("Parent category '%s' not found." % parent_name) 189 else: 190 parent = None 191 category = Category().update(name, color, font_color, parent=parent) 192 if progress_color: 193 category.set_progress_color(progress_color) 194 if done_color: 195 category.set_done_color(done_color) 196 old_category = self.db.get_category_by_name(name) 197 if old_category is not None: 198 category = old_category 199 if name not in tmp_dict["category_map"]: 200 tmp_dict["category_map"][name] = category 201 self.db.save_category(category)
202
203 - def _parse_event(self, text, tmp_dict):
204 start = self._parse_time(tmp_dict.pop("tmp_start")) 205 end = self._parse_time(tmp_dict.pop("tmp_end")) 206 text = tmp_dict.pop("tmp_text") 207 progress = self._parse_optional_int(tmp_dict, "tmp_progress") 208 fuzzy = self._parse_optional_bool(tmp_dict, "tmp_fuzzy") 209 locked = self._parse_optional_bool(tmp_dict, "tmp_locked") 210 ends_today = self._parse_optional_bool(tmp_dict, "tmp_ends_today") 211 category_text = tmp_dict.pop("tmp_category", None) 212 if category_text is None: 213 category = None 214 else: 215 category = tmp_dict["category_map"].get(category_text, None) 216 if category is None: 217 raise ParseException("Category '%s' not found." % category_text) 218 description = tmp_dict.pop("tmp_description", None) 219 alert_string = tmp_dict.pop("tmp_alert", None) 220 alert = parse_alert_string(self.db.get_time_type(), alert_string) 221 icon_text = tmp_dict.pop("tmp_icon", None) 222 if icon_text is None: 223 icon = None 224 else: 225 icon = parse_icon(icon_text) 226 hyperlink = tmp_dict.pop("tmp_hyperlink", None) 227 milestone = self._parse_optional_bool(tmp_dict, "tmp_milestone") 228 if self._is_container_event(text): 229 cid, text = self._extract_container_id(text) 230 event = Container().update(start, end, text, category) 231 self._containers_by_cid[cid] = event 232 elif self._is_subevent(text): 233 cid, text = self._extract_subid(text) 234 event = Subevent().update( 235 start, 236 end, 237 text, 238 category, 239 locked=locked, 240 ends_today=ends_today 241 ) 242 event.container = self._containers_by_cid[cid] 243 elif milestone: 244 event = Milestone().update(start, start, text) 245 event.set_category(category) 246 else: 247 if self._text_starts_with_added_space(text): 248 text = self._remove_added_space(text) 249 event = Event().update(start, end, text, category, fuzzy, locked, ends_today) 250 default_color = tmp_dict.pop("tmp_default_color", "200,200,200") 251 event.set_data("description", description) 252 event.set_data("icon", icon) 253 event.set_data("alert", alert) 254 event.set_data("hyperlink", hyperlink) 255 event.set_data("progress", int(progress)) 256 event.set_data("default_color", parse_color(default_color)) 257 self.db.save_event(event)
258
259 - def _parse_era(self, text, tmp_dict):
260 name = tmp_dict.pop("tmp_name") 261 start = self._parse_time(tmp_dict.pop("tmp_start")) 262 end = self._parse_time(tmp_dict.pop("tmp_end")) 263 color = parse_color(tmp_dict.pop("tmp_color")) 264 ends_today = self._parse_optional_bool(tmp_dict, "tmp_ends_today") 265 era = Era().update(start, end, name, color) 266 era.set_ends_today(ends_today) 267 self.db.save_era(era)
268
269 - def _text_starts_with_added_space(self, text):
270 return text[0:2] in (" (", " [")
271
272 - def _remove_added_space(self, text):
273 return text[1:]
274
275 - def _is_container_event(self, text):
276 return text.startswith("[")
277
278 - def _is_subevent(self, text):
279 return text.startswith("(")
280
281 - def _extract_container_id(self, text):
282 str_id, text = text.split("]", 1) 283 try: 284 str_id = str_id[1:] 285 cid = int(str_id) 286 except: 287 cid = -1 288 return cid, text
289
290 - def _extract_subid(self, text):
291 cid, text = text.split(")", 1) 292 try: 293 cid = int(cid[1:]) 294 except: 295 cid = -1 296 return cid, text
297
298 - def _parse_optional_bool(self, tmp_dict, cid):
299 if cid in tmp_dict: 300 return tmp_dict.pop(cid) == "True" 301 else: 302 return False
303
304 - def _parse_optional_int(self, tmp_dict, cid):
305 if cid in tmp_dict: 306 return int(tmp_dict.pop(cid)) 307 else: 308 return 0
309
310 - def _parse_optional_color(self, tmp_dict, cid, missing_value=(0, 0, 0)):
311 if cid in tmp_dict: 312 return parse_color(tmp_dict.pop(cid)) 313 else: 314 return missing_value
315
316 - def _parse_displayed_period(self, text, tmp_dict):
317 start = self._parse_time(tmp_dict.pop("tmp_start")) 318 end = self._parse_time(tmp_dict.pop("tmp_end")) 319 self.db.set_displayed_period(TimePeriod(start, end))
320
321 - def _parse_hidden_category(self, text, tmp_dict):
322 category = tmp_dict["category_map"].get(text, None) 323 if category is None: 324 raise ParseException("Category '%s' not found." % text) 325 tmp_dict["hidden_categories"].append(category)
326
327 - def _parse_hidden_categories(self, text, tmp_dict):
328 self.db.set_hidden_categories(tmp_dict.pop("hidden_categories"))
329
330 - def _parse_time(self, time_string):
332
333 - def _parse_saved_now(self, text, tmp_dict):
334 time = self.db.time_type.parse_time(text) 335 self.db.set_saved_now(time)
336 337
338 -def parse_color(color_string):
339 """ 340 Expected format 'r,g,b'. 341 342 Return a tuple (r, g, b). 343 """ 344 def verify_255_number(num): 345 if num < 0 or num > 255: 346 raise ParseException("Color number not in range [0, 255], " 347 "color string = '%s'" % color_string)
348 match = re.search(r"^(\d+),(\d+),(\d+)$", color_string) 349 if match: 350 r, g, b = int(match.group(1)), int(match.group(2)), int(match.group(3)) 351 verify_255_number(r) 352 verify_255_number(g) 353 verify_255_number(b) 354 return (r, g, b) 355 else: 356 raise ParseException("Color not on correct format, color string = '%s'" 357 % color_string) 358 359
360 -def parse_icon(string):
361 """ 362 Expected format: base64 encoded png image. 363 364 Return a wx.Bitmap. 365 """ 366 try: 367 icon_string = StringIO.StringIO(base64.b64decode(string)) 368 image = wx.ImageFromStream(icon_string, wx.BITMAP_TYPE_PNG) 369 return image.ConvertToBitmap() 370 except: 371 raise ParseException("Could not parse icon from '%s'." % string)
372 373
374 -def parse_alert_string(time_type, alert_string):
375 if alert_string is not None: 376 try: 377 time_string, alert_text = alert_string.split(";", 1) 378 alert_time = time_type.parse_time(time_string) 379 alert = (alert_time, alert_text) 380 except: 381 raise ParseException("Could not parse alert from '%s'." % alert_string) 382 else: 383 alert = None 384 return alert
385