LSSTApplications  11.0-13-gbb96280,12.1+18,12.1+7,12.1-1-g14f38d3+72,12.1-1-g16c0db7+5,12.1-1-g5961e7a+84,12.1-1-ge22e12b+23,12.1-11-g06625e2+4,12.1-11-g0d7f63b+4,12.1-19-gd507bfc,12.1-2-g7dda0ab+38,12.1-2-gc0bc6ab+81,12.1-21-g6ffe579+2,12.1-21-gbdb6c2a+4,12.1-24-g941c398+5,12.1-3-g57f6835+7,12.1-3-gf0736f3,12.1-37-g3ddd237,12.1-4-gf46015e+5,12.1-5-g06c326c+20,12.1-5-g648ee80+3,12.1-5-gc2189d7+4,12.1-6-ga608fc0+1,12.1-7-g3349e2a+5,12.1-7-gfd75620+9,12.1-9-g577b946+5,12.1-9-gc4df26a+10
LSSTDataManagementBasePackage
posixStorage.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 
3 #
4 # LSST Data Management System
5 # Copyright 2016 LSST Corporation.
6 #
7 # This product includes software developed by the
8 # LSST Project (http://www.lsst.org/).
9 #
10 # This program is free software: you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation, either version 3 of the License, or
13 # (at your option) any later version.
14 #
15 # This program is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
19 #
20 # You should have received a copy of the LSST License Statement and
21 # the GNU General Public License along with this program. If not,
22 # see <http://www.lsstcorp.org/LegalNotices/>.
23 #
24 from future import standard_library
25 standard_library.install_aliases()
26 from past.builtins import basestring
27 import sys
28 import copy
29 import pickle
30 import importlib
31 import os
32 import urllib.parse
33 
34 import yaml
35 
36 from . import LogicalLocation, Persistence, Policy, StorageList, Registry, Storage, RepositoryCfg, safeFileIo
37 from lsst.log import Log
38 import lsst.pex.policy as pexPolicy
39 from .safeFileIo import SafeFilename
40 
41 
43 
44  def __init__(self, uri):
45  """Initializer
46 
47  :return:
48  """
49  self.log = Log.getLogger("daf.persistence.butler")
50  self.root = parseRes = urllib.parse.urlparse(uri).path
51  if self.root and not os.path.exists(self.root):
52  os.makedirs(self.root)
53 
54  # Always use an empty Persistence policy until we can get rid of it
55  persistencePolicy = pexPolicy.Policy()
56  self.persistence = Persistence.getPersistence(persistencePolicy)
57 
58  self.registry = Registry.create(location=self.root)
59 
60  def __repr__(self):
61  return 'PosixStorage(root=%s)' % self.root
62 
63  @staticmethod
65  """Get a persisted RepositoryCfg
66  """
67  repositoryCfg = None
68  parseRes = urllib.parse.urlparse(uri)
69  loc = os.path.join(parseRes.path, 'repositoryCfg.yaml')
70  if os.path.exists(loc):
71  with open(loc, 'r') as f:
72  repositoryCfg = yaml.load(f)
73  if repositoryCfg.root is None:
74  repositoryCfg.root = uri
75  return repositoryCfg
76 
77  @staticmethod
78  def getRepositoryCfg(uri):
79  repositoryCfg = PosixStorage._getRepositoryCfg(uri)
80  if repositoryCfg is not None:
81  return repositoryCfg
82 
83  return repositoryCfg
84 
85  @staticmethod
86  def putRepositoryCfg(cfg, loc=None):
87  if loc is None or cfg.root == loc:
88  # the cfg is at the root location of the repository so don't write root, let it be implicit in the
89  # location of the cfg.
90  cfg = copy.copy(cfg)
91  loc = cfg.root
92  cfg.root = None
93  # This class supports schema 'file' and also treats no schema as 'file'.
94  # Split the URI and take only the path; remove the schema fom loc if it's there.
95  parseRes = urllib.parse.urlparse(loc)
96  loc = parseRes.path
97  if not os.path.exists(loc):
98  os.makedirs(loc)
99  loc = os.path.join(loc, 'repositoryCfg.yaml')
100  with safeFileIo.FileForWriteOnceCompareSame(loc) as f:
101  yaml.dump(cfg, f)
102 
103  @staticmethod
104  def getMapperClass(root):
105  """Get the mapper class associated with a repository root.
106 
107  Supports the legacy _parent symlink search (which was only ever posix-only. This should not be used by
108  new code and repositories; they should use the Repository parentCfg mechanism.
109 
110  :param root: the location of a persisted ReositoryCfg is (new style repos), or the location where a
111  _mapper file is (old style repos).
112  :return: a class object or a class instance, depending on the state of the mapper when the repository
113  was created.
114  """
115  if not (root):
116  return None
117 
118  cfg = PosixStorage._getRepositoryCfg(root)
119  if cfg is not None:
120  return cfg.mapper
121 
122  # Find a "_mapper" file containing the mapper class name
123  basePath = root
124  mapperFile = "_mapper"
125  while not os.path.exists(os.path.join(basePath, mapperFile)):
126  # Break abstraction by following _parent links from CameraMapper
127  if os.path.exists(os.path.join(basePath, "_parent")):
128  basePath = os.path.join(basePath, "_parent")
129  else:
130  mapperFile = None
131  break
132 
133  if mapperFile is not None:
134  mapperFile = os.path.join(basePath, mapperFile)
135 
136  # Read the name of the mapper class and instantiate it
137  with open(mapperFile, "r") as f:
138  mapperName = f.readline().strip()
139  components = mapperName.split(".")
140  if len(components) <= 1:
141  raise RuntimeError("Unqualified mapper name %s in %s" %
142  (mapperName, mapperFile))
143  pkg = importlib.import_module(".".join(components[:-1]))
144  return getattr(pkg, components[-1])
145 
146  return None
147 
148  @staticmethod
150  """For Butler V1 Repositories only, if a _parent symlink exists, get the location pointed to by the
151  symlink.
152 
153  Parameters
154  ----------
155  root : string
156  A path to the folder on the local filesystem.
157 
158  Returns
159  -------
160  string or None
161  A path to the parent folder indicated by the _parent symlink, or None if there is no _parent
162  symlink at root.
163  """
164  linkpath = os.path.join(root, '_parent')
165  if os.path.exists(linkpath):
166  try:
167  return os.readlink(os.path.join(root, '_parent'))
168  except OSError:
169  # some of the unit tests rely on a folder called _parent instead of a symlink to aother
170  # location. Allow that; return the path of that folder.
171  return os.path.join(root, '_parent')
172  return None
173 
174  def mapperClass(self):
175  """Get the class object for the mapper specified in the stored repository"""
176  return PosixStorage.getMapperClass(self.root)
177 
178  def write(self, butlerLocation, obj):
179  """Writes an object to a location and persistence format specified by ButlerLocation
180 
181  :param butlerLocation: the location & formatting for the object to be written.
182  :param obj: the object to be written.
183  :return: None
184  """
185  self.log.debug("Put location=%s obj=%s", butlerLocation, obj)
186 
187  additionalData = butlerLocation.getAdditionalData()
188  storageName = butlerLocation.getStorageName()
189  locations = butlerLocation.getLocations()
190 
191  pythonType = butlerLocation.getPythonType()
192  if pythonType is not None:
193  if isinstance(pythonType, basestring):
194  # import this pythonType dynamically
195  pythonTypeTokenList = pythonType.split('.')
196  importClassString = pythonTypeTokenList.pop()
197  importClassString = importClassString.strip()
198  importPackage = ".".join(pythonTypeTokenList)
199  importType = __import__(importPackage, globals(), locals(), [importClassString], 0)
200  pythonType = getattr(importType, importClassString)
201  # todo this effectively defines the butler posix "do serialize" command to be named "put". This has
202  # implications; write now I'm worried that any python type that can be written to disk and has a
203  # method called 'put' will be called here (even if it's e.g. destined for FitsStorage).
204  # We might want a somewhat more specific API.
205  if hasattr(pythonType, 'butlerWrite'):
206  pythonType.butlerWrite(obj, butlerLocation=butlerLocation)
207  return
208 
209  with SafeFilename(locations[0]) as locationString:
210  logLoc = LogicalLocation(locationString, additionalData)
211 
212  if storageName == "PickleStorage":
213  with open(logLoc.locString(), "wb") as outfile:
214  pickle.dump(obj, outfile, pickle.HIGHEST_PROTOCOL)
215  return
216 
217  if storageName == "ConfigStorage":
218  obj.save(logLoc.locString())
219  return
220 
221  if storageName == "FitsCatalogStorage":
222  flags = additionalData.getInt("flags", 0)
223  obj.writeFits(logLoc.locString(), flags=flags)
224  return
225 
226  # Create a list of Storages for the item.
227  storageList = StorageList()
228  storage = self.persistence.getPersistStorage(storageName, logLoc)
229  storageList.append(storage)
230 
231  if storageName == 'FitsStorage':
232  self.persistence.persist(obj, storageList, additionalData)
233  return
234 
235  # Persist the item.
236  if hasattr(obj, '__deref__'):
237  # We have a smart pointer, so dereference it.
238  self.persistence.persist(obj.__deref__(), storageList, additionalData)
239  else:
240  self.persistence.persist(obj, storageList, additionalData)
241 
242  def read(self, butlerLocation):
243  """Read from a butlerLocation.
244 
245  :param butlerLocation:
246  :return: a list of objects as described by the butler location. One item for each location in
247  butlerLocation.getLocations()
248  """
249  additionalData = butlerLocation.getAdditionalData()
250  # Create a list of Storages for the item.
251  storageName = butlerLocation.getStorageName()
252  results = []
253  locations = butlerLocation.getLocations()
254  pythonType = butlerLocation.getPythonType()
255  if pythonType is not None:
256  if isinstance(pythonType, basestring):
257  # import this pythonType dynamically
258  pythonTypeTokenList = pythonType.split('.')
259  importClassString = pythonTypeTokenList.pop()
260  importClassString = importClassString.strip()
261  importPackage = ".".join(pythonTypeTokenList)
262  importType = __import__(importPackage, globals(), locals(), [importClassString], 0)
263  pythonType = getattr(importType, importClassString)
264 
265  # see note re. discomfort with the name 'butlerWrite' in the write method, above.
266  # Same applies to butlerRead.
267  if hasattr(pythonType, 'butlerRead'):
268  results = pythonType.butlerRead(butlerLocation=butlerLocation)
269  return results
270 
271  for locationString in locations:
272  logLoc = LogicalLocation(locationString, additionalData)
273 
274  if storageName == "PafStorage":
275  finalItem = pexPolicy.Policy.createPolicy(logLoc.locString())
276  elif storageName == "YamlStorage":
277  finalItem = Policy(filePath=logLoc.locString())
278  elif storageName == "PickleStorage":
279  if not os.path.exists(logLoc.locString()):
280  raise RuntimeError("No such pickle file: " + logLoc.locString())
281  with open(logLoc.locString(), "rb") as infile:
282  # py3: We have to specify encoding since some files were written
283  # by python2, and 'latin1' manages that conversion safely. See:
284  # http://stackoverflow.com/questions/28218466/unpickling-a-python-2-object-with-python-3/28218598#28218598
285  if sys.version_info.major >= 3:
286  finalItem = pickle.load(infile, encoding="latin1")
287  else:
288  finalItem = pickle.load(infile)
289  elif storageName == "FitsCatalogStorage":
290  if not os.path.exists(logLoc.locString()):
291  raise RuntimeError("No such FITS catalog file: " + logLoc.locString())
292  hdu = additionalData.getInt("hdu", 0)
293  flags = additionalData.getInt("flags", 0)
294  finalItem = pythonType.readFits(logLoc.locString(), hdu, flags)
295  elif storageName == "ConfigStorage":
296  if not os.path.exists(logLoc.locString()):
297  raise RuntimeError("No such config file: " + logLoc.locString())
298  finalItem = pythonType()
299  finalItem.load(logLoc.locString())
300  else:
301  storageList = StorageList()
302  storage = self.persistence.getRetrieveStorage(storageName, logLoc)
303  storageList.append(storage)
304  itemData = self.persistence.unsafeRetrieve(
305  butlerLocation.getCppType(), storageList, additionalData)
306  finalItem = pythonType.swigConvert(itemData)
307  results.append(finalItem)
308 
309  return results
310 
311  def exists(self, location):
312  """Check if 'location' exists relative to root.
313 
314  :param location:
315  :return:
316  """
317  return os.path.exists(os.path.join(self.root, location))
318 
319  def locationWithRoot(self, location):
320  """Get the full path to the location.
321 
322  :param location:
323  :return:
324  """
325  return os.path.join(self.root, location)
326 
327  def lookup(self, *args, **kwargs):
328  """Perform a lookup in the registry"""
329  return self.registry.lookup(*args, **kwargs)
330 
331  @staticmethod
332  def v1RepoExists(root):
333  """Test if a Version 1 Repository exists.
334 
335  Version 1 Repositories only exist in posix storages and do not have a RepositoryCfg file.
336  To "exist" the folder at root must exist and contain files or folders.
337 
338  Parameters
339  ----------
340  root : string
341  A path to a folder on the local filesystem.
342 
343  Returns
344  -------
345  bool
346  True if the repository at root exists, else False.
347  """
348  return os.path.exists(root) and bool(os.listdir(root))
349 
350 
351 Storage.registerStorageClass(scheme='', cls=PosixStorage)
352 Storage.registerStorageClass(scheme='file', cls=PosixStorage)
a container for holding hierarchical configuration data in memory.
Definition: Policy.h:169
Abstract base class for storage implementations.
Definition: Storage.h:60