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