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):
    331          return self.db.get_time_type().parse_time(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