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