Gnumed.timelinelib.dataimport.timelinexml

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