# -*- coding: utf-8 -*-
from ..dataset.serializable import Serializable
from .. import pal
from ..dataset.baseproduct import BaseProduct
from ..dataset.odict import ODict
from ..dataset.metadata import tabulate
from ..utils.common import bstr
from collections import OrderedDict
from collections import UserDict
import logging
# create logger
logger = logging.getLogger(__name__)
#logger.debug('level %d' % (logger.getEffectiveLevel()))
[docs]class ContextRuleException(ValueError):
pass
[docs]class RefContainer(ODict):
""" A map where Rules of its owner Context are applied when put(k,v) is called, and the owner MapContext's ID can be put to v's parents list.
Implemented as an ODict that RefContainer has a _STID when json.loads'ed.
A MapContext has a _data, which has a refs:RefContainer, which has a datap, which has a name:ProductRef.
when used as context.refs.get('x').product.description, the RefContainer is called with get() or __getitem__(), which calls superclass composite's __getitem__()
"""
[docs] def __init__(self, **kwds):
"""
"""
super().__init__(**kwds)
self.setOwner(None)
@property
def owner(self):
""" Property """
return self.getOwner()
@owner.setter
def owner(self, owner):
"""
"""
self.setOwner(owner)
[docs] def getOwner(self):
""" Returns the reference container mapping
"""
return self._owner
[docs] def setOwner(self, owner):
""" records who owns this container.
Due to reason described in __setitem__ doc, existing refs will be set again upon owner set
"""
if hasattr(self, '_owner') and self._owner is not None and id(self._owner) == id(owner):
return
self._owner = owner
old = self.data
self.data = dict()
for k, r in old.items():
self.__setitem__(k, r)
del old
def __setitem__(self, key, ref):
"""
when deserialized contained refs will be set with _owner==None
then the following are done when the assembled refContainer is
set('refs',refC) to the owner context.
"""
if ref is None:
raise ValueError()
# during deserialization __setitem__ is called before _owner is set by Context,setRefs()
if hasattr(self, '_owner') and self._owner is not None:
from .productref import ProductRef
if isinstance(ref, ProductRef):
ref.addParent(self._owner)
else:
raise TypeError(type(ref).__name__ +
' is not a Product Reference')
if not self._owner.applyRule(key, ref):
ContextRuleException('Ref %s fails rule test of %s.' %
(ref.toString(level=2),
self._owner.__class__.__name__)
)
super().__setitem__(key, ref)
def __delitem__(self, key):
if key is None:
raise KeyError()
# during deserialization __setitem__ are called before _owner is set by Context,setRefs()
if hasattr(self, '_owner'):
from .productref import ProductRef
ref = self.__getitem__(key)
if isinstance(ref, ProductRef):
ref.removeParent(self._owner)
super().__delitem__(key)
[docs] def clear(self):
""" remove all productRefs """
ks = list(self.keys())
for k in ks:
self.__delitem__(k)
[docs] def set(self, key, ref):
""" set label-ref pair after validating then add parent to the ref
"""
self.__setitem__(key, ref)
put = set
[docs] def get(self, key, *args, **kwds):
"""
"""
return self.__getitem__(key, *args, **kwds)
[docs] def size(self):
""" """
return len(self.keys())
def __getstate__(self):
""" Can be encoded with serializableEncoder """
s = super().__getstate__()
# there is no need to persist owner as it is setted by `__setitem__` during deserializing
# s.update(_ATTR_owner=self.owner)
return s
[docs] def toString(self, level=0, tablefmt='grid', **kwds):
cn = self.__class__.__name__
if level > 0:
s = f'{cn}('
s += ', '.join('%s: %s' % (n, r.urn) for n, r in self.items())
s += ')'
else:
dat = [(n, r.urn, '\n'.join(str(id(p)) for p in r.parents), len(r.meta)
if r.meta else '(none)') for n, r in self.items()]
headers = ['lable', 'urn', 'parents', 'meta']
s = tabulate.tabulate(dat, headers=headers,
stralign='center', tablefmt=tablefmt)
return s
string = toString
txt = toString
[docs]class AbstractContext():
""" A special kind of Product that can hold references to other Products.
This abstract product introduces the lazy loading and saving of references to Products or ProductRefs that it is holding. It remembers its state.
http://herschel.esac.esa.int/hcss-doc-15.0/load/hcss_drm/api/herschel/ia/pal/Context.html
"""
[docs] def __init__(self, *args, **kwds):
""" Sets ``rule`` to ``None`` if ``zInfo`` does not have ``refs``, else to ``zInfo['refs']``.
"""
super().__init__(*args, **kwds)
self._dirty = False
if 'refs' not in self.zInfo:
self._rule = None # None means there is no rule.
else:
self._rule = self.zInfo['refs']
refC = None
# this line must stay after _rule is set.
self.setRefs(refC)
@property
def refs(self):
""" Property """
return self.getRefs()
@refs.setter
def refs(self, refs):
"""
"""
self.setRefs(refs)
[docs] def setRefs(self, refs):
""" Changes/Adds the mapping container that holds references.
"""
self.set('refs', refs)
[docs] def set(self, name, refs):
""" add owner to RefContainer or add named references.
:name: name of the new `RefContainer`. if is ``refs``, set owner of ``refs`` to `self`.
"""
if isinstance(refs, RefContainer) and name == 'refs':
refs.setOwner(self)
super().set(name, refs)
[docs] def getRefs(self):
""" Returns the reference container mapping
"""
return self.get('refs')
[docs] def getAllRefs(self, recursive, includeContexts):
""" Provides a set of the unique references stored in this context.
"""
raise NotImplementedError()
[docs] def hasDirtyReferences(self, storage):
""" Returns a logical to specify whether this context has dirty references or not.
"""
raise NotImplementedError()
[docs] @staticmethod
def isContext(cls):
""" Yields true if specified class belongs to the family of contexts.
"""
return issubclass(cls, AbstractContext)
[docs] def isValid(self):
""" Provides a mechanism to ensure whether it is valid to store this context in its current state.
"""
raise NotImplementedError()
[docs] def readDataset(self, storage, table, defaultPoolId):
""" Reads a dataset with information within this context that is normally not accessible from the normal Product interface."""
raise NotImplementedError()
[docs] def refsChanged(self):
""" Indicates that the references have been changed in memory, which marks this context as dirty.
"""
self._dirty = True
[docs] def writeDataset(self, storage):
""" Creates a dataset with information within this context that is normally not accessible from the normal Product interface.
"""
raise NotImplementedError()
[docs] def getRule(self):
""" Get the rule that controls the products to be added into the context.
"""
return self._rule
[docs] def setRule(self, rule):
""" Set the rule that controls the products to be added into the context.
"""
self._rule = rule
[docs] def addRule(self, rule):
""" Add to the rule that controls the products to be added into the context.
The new rule will be old rule AND added rule.
"""
raise NotImplementedError()
[docs] def applyRule(self, *args):
""" returns True if the input ProductRef passes rule.
If ``refs`` is not specified in ``zInfo`` return True.
If ``zInfo['refs'] is empty, return True.
Subclasses can override this method according to what is written in zInfo.
"""
if self._rule is None:
return True
if len(self._rule) == 0:
return True
return True
[docs]class Context(AbstractContext, BaseProduct):
""" See docstring of AbstractContext.
"""
[docs] def __init__(self, *args, **kwds):
"""
"""
super().__init__(*args, **kwds)
[docs] def applyRule(self, *args):
""" returns True if the input ProductRef passes rule.
Default behavior is return the superclass.applyRule() result.
Subclasses can override this method according to what is written in zInfo.
"""
return super().applyRule(*args)
[docs] def getAllRefs(self, recursive=False, includeContexts=True, seen=None):
""" Provides a set of the unique references stored in this `Context`.
This includes references that are contexts, but not the contents of these subcontexts. This is equivalent to getAllRefs(recursive=false, includeContexts= true).
recursive - if true, include references in subcontexts
includeContexts - if true, include references to contexts, not including this one
"""
if issubclass(self.__class__, pal.productref.ProductRef) and not Context.isContext(self.__class__):
raise TypeError('This ref does not point to a context')
if seen is None:
seen = list()
rs = list()
surn = self.get('_urn', None)
for x in self.refs.values():
if Context.isContext(x.getType()):
if includeContexts:
if surn and x.getUrn() == surn.urn:
# pointing to self
pass
else:
if x not in rs:
rs.append(x)
if recursive:
if x not in seen:
seen.append(x)
# enter the context
rs += x.getProduct().getAllRefs(recursive=recursive,
includeContexts=includeContexts,
seen=seen)
else:
pass
else:
# not contex
# records
if x not in rs:
rs.append(x)
return rs
[docs] def hasDirtyReferences(self, storage):
""" Returns a logical to specify whether this context has dirty references or not.
"""
return self._dirty
[docs]class MapContext(Context):
""" Allows grouping Products into a map of (String, ProductRef) pairs.
New entries can be added if they comply to the adding rules of this context. The default behaviour is to allow adding any (String,ProductRef) given that ``rule`` is not set.
An example::
image = ImageProduct(description="hi")
spectrum = SpectrumProduct(description="there")
simple = Product(description="everyone")
context=MapContext()
context.refs.put("x",ProductRef(image))
context.refs.put("y",ProductRef(spectrum))
context.refs.put("z",ProductRef(simple))
print context.refs.size() # 3
print context.refs.get('x').product.description # hi
print context.refs.get('y').product.description # there
print context.refs.get('z').product.description # everyone
It is possible to insert a ProductRef at a specific key in the MapContext. The same insertion behaviour is followed as for a Python dict, in that if there is already an existing ProductRef for the given key, that ProductRef is replaced with the new one::
product4=SpectrumProduct(description="everybody")
context.refs.put("y", ProductRef(product4))
product5=SpectrumProduct(description="here")
context.refs.put("a", ProductRef(product5))
print context.refs.get('x').product.description # hi
print context.refs.get('y').product.description # everybody
print context.refs.get('z').product.description # everyone
print context.refs.get('a').product.description # here
Note that the rules are only applied when putting an entry to the map!
Be aware that
1. the put() method of the map view may throw a ContextRuleException if the data added to the context violates the rules applied to the context.
2. the put() method of the map view may throw a ValueError if either of the arguments to the put() method are null.
BaseProduct--Product
\\
AbstractContext------Contex---------MapContext
"""
[docs] def __init__(self, *args, **kwds):
"""
"""
super().__init__(*args, **kwds)
refC = RefContainer()
# this line must stay after _rule is set.
self.setRefs(refC)
[docs] def addRule(self, rule):
""" Add to the rule that controls the products to be added into the context.
The new rule will be old rule AND added rule.
"""
self._rule.update(rule)
[docs] def applyRule(self, key, ref):
""" returns True if the input key name and ProductRef pass rule.
Default behavior is return True if the fully qualified class name of the product which ref refers to equals ``zInfo['refs'][key]``, OR the superclass.applyRule() result.
Subclasses can override this method according to what is written in zInfo.
"""
if super().applyRule():
return True
if 'refs' in self.zInfo:
return ref.urnobj.getTypeName() in self.zInfo['refs']