templates.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. """
  2. Handles (deferred) loading of odML templates
  3. """
  4. import os
  5. import sys
  6. import tempfile
  7. import threading
  8. try:
  9. import urllib.request as urllib2
  10. from urllib.error import URLError
  11. from urllib.parse import urljoin
  12. except ImportError:
  13. import urllib2
  14. from urllib2 import URLError
  15. from urlparse import urljoin
  16. from datetime import datetime as dati
  17. from datetime import timedelta
  18. from hashlib import md5
  19. from .tools.parser_utils import ParserException
  20. from .tools.xmlparser import XMLReader
  21. REPOSITORY_BASE = 'https://templates.g-node.org/'
  22. REPOSITORY = urljoin(REPOSITORY_BASE, 'templates.xml')
  23. CACHE_AGE = timedelta(days=1)
  24. CACHE_DIR = "odml.cache"
  25. # TODO after prototyping move functions common with
  26. # terminologies to a common file.
  27. def cache_load(url):
  28. """
  29. Load the url and store the file in a temporary cache directory.
  30. Subsequent requests for this url will use the cached version until
  31. the file is older than the CACHE_AGE.
  32. Exceptions are caught and not re-raised to enable loading of nested
  33. odML files without breaking if one of the child files is unavailable.
  34. :param url: location of an odML template XML file.
  35. :return: Local file location of the requested file.
  36. """
  37. filename = '.'.join([md5(url.encode()).hexdigest(), os.path.basename(url)])
  38. cache_dir = os.path.join(tempfile.gettempdir(), CACHE_DIR)
  39. # Create temporary folder if required
  40. if not os.path.exists(cache_dir):
  41. try:
  42. os.makedirs(cache_dir)
  43. except OSError: # might happen due to concurrency
  44. if not os.path.exists(cache_dir):
  45. raise
  46. cache_file = os.path.join(cache_dir, filename)
  47. if not os.path.exists(cache_file) or \
  48. dati.fromtimestamp(os.path.getmtime(cache_file)) < (dati.now() - CACHE_AGE):
  49. try:
  50. data = urllib2.urlopen(url).read()
  51. if sys.version_info.major > 2:
  52. data = data.decode("utf-8")
  53. except (ValueError, URLError) as exc:
  54. msg = "Failed to load resource from '%s': %s" % (url, exc)
  55. exc.args = (msg,) # needs to be a tuple
  56. raise exc
  57. with open(cache_file, "w") as local_file:
  58. local_file.write(str(data))
  59. return cache_file
  60. class TemplateHandler(dict):
  61. """
  62. TemplateHandler facilitates synchronous and deferred
  63. loading, caching, browsing and importing of full or partial
  64. odML templates.
  65. """
  66. # Used for deferred loading
  67. loading = {}
  68. def browse(self, url):
  69. """
  70. Load, cache and pretty print an odML template XML file from a URL.
  71. :param url: location of an odML template XML file.
  72. :return: The odML document loaded from url.
  73. """
  74. doc = self.load(url)
  75. if not doc:
  76. raise ValueError("Failed to load resource from '%s'" % url)
  77. doc.pprint(max_depth=0)
  78. return doc
  79. def clone_section(self, url, section_name, children=True, keep_id=False):
  80. """
  81. Load a section by name from an odML template found at the provided URL
  82. and return a clone. By default it will return a clone with all child
  83. sections and properties as well as changed IDs for every entity.
  84. The named section has to be a root (direct) child of the referenced
  85. odML document.
  86. :param url: location of an odML template XML file.
  87. :param section_name: Unique name of the requested Section.
  88. :param children: Boolean whether the child entities of a Section will be
  89. returned as well. Default is True.
  90. :param keep_id: Boolean whether all returned entities will keep the
  91. original ID or have a new one assigned. Default is False.
  92. :return: The cloned odML section loaded from url.
  93. """
  94. doc = self.load(url)
  95. if not doc:
  96. raise ValueError("Failed to load resource from '%s'" % url)
  97. try:
  98. sec = doc[section_name]
  99. except KeyError:
  100. raise KeyError("Section '%s' not found in document at '%s'" % (section_name, url))
  101. return sec.clone(children=children, keep_id=keep_id)
  102. def load(self, url):
  103. """
  104. Load and cache an odML template from a URL.
  105. :param url: location of an odML template XML file.
  106. :return: The odML document loaded from url.
  107. """
  108. # Some feedback for the user when loading large or
  109. # nested (include) odML files.
  110. print("\nLoading file %s" % url)
  111. if url in self:
  112. doc = self[url]
  113. elif url in self.loading:
  114. self.loading[url].join()
  115. self.loading.pop(url, None)
  116. doc = self.load(url)
  117. else:
  118. doc = self._load(url)
  119. return doc
  120. def _load(self, url):
  121. """
  122. Cache loads an odML template for a URL and returns
  123. the result as a parsed odML document.
  124. :param url: location of an odML template XML file.
  125. :return: The odML document loaded from url.
  126. It will silently return None, if any exceptions
  127. occur to enable loading of nested odML files.
  128. """
  129. try:
  130. local_file = cache_load(url)
  131. except (ValueError, URLError):
  132. return None
  133. try:
  134. doc = XMLReader(filename=url, ignore_errors=True).from_file(local_file)
  135. doc.finalize()
  136. except ParserException as exc:
  137. print("Failed to load '%s' due to parser errors:\n %s" % (url, exc))
  138. return None
  139. self[url] = doc
  140. return doc
  141. def deferred_load(self, url):
  142. """
  143. Start a background thread to load an odML template from a URL.
  144. :param url: location of an odML template XML file.
  145. """
  146. if url in self or url in self.loading:
  147. return
  148. self.loading[url] = threading.Thread(target=self._load, args=(url,))
  149. self.loading[url].start()