# -*- coding: utf-8 -*-
# Author: Florian Mayer <florian.mayer@bitsrc.org>
#
# This module was developed with funding provided by
# the ESA Summer of Code (2011).
#
# pylint: disable=C0103,R0903
"""
Attributes that can be used to construct VSO queries. Attributes are the
fundamental building blocks of queries that, together with the two
operations of AND and OR (and in some rare cases XOR) can be used to
construct complex queries. Most attributes can only be used once in an
AND-expression, if you still attempt to do so it is called a collision,
for a quick example think about how the system should handle
Instrument('aia') & Instrument('eit').
"""
from __future__ import absolute_import
from datetime import datetime
from sunpy.net.attr import (
Attr, ValueAttr, AttrWalker, AttrAnd, AttrOr, DummyAttr, ValueAttr
)
from sunpy.util.util import to_angstrom
from sunpy.util.multimethod import MultiMethod
from sunpy.time import parse_time
TIMEFORMAT = '%Y%m%d%H%M%S'
class _Range(object):
def __init__(self, min_, max_, create):
self.min = min_
self.max = max_
self.create = create
def __xor__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
new = DummyAttr()
if self.min < other.min:
new |= self.create(self.min, min(other.min, self.max))
if other.max < self.max:
new |= self.create(other.max, self.max)
return new
def __contains__(self, other):
return self.min <= other.min and self.max >= other.max
[docs]class Wave(Attr, _Range):
def __init__(self, wavemin, wavemax, waveunit='Angstrom'):
self.min, self.max = sorted(
to_angstrom(v, waveunit) for v in [float(wavemin), float(wavemax)]
)
self.unit = 'Angstrom'
Attr.__init__(self)
_Range.__init__(self, self.min, self.max, self.__class__)
[docs] def collides(self, other):
return isinstance(other, self.__class__)
[docs]class Time(Attr, _Range):
def __init__(self, start, end, near=None):
self.start = parse_time(start)
self.end = parse_time(end)
self.near = None if near is None else parse_time(near)
_Range.__init__(self, self.start, self.end, self.__class__)
Attr.__init__(self)
[docs] def collides(self, other):
return isinstance(other, self.__class__)
def __xor__(self, other):
if not isinstance(other, self.__class__):
raise TypeError
if self.near is not None or other.near is not None:
raise TypeError
return _Range.__xor__(self, other)
[docs] def pad(self, timedelta):
return Time(self.start - timedelta, self.start + timedelta)
def __repr__(self):
return '<Time(%r, %r, %r)>' % (self.start, self.end, self.near)
[docs]class Extent(Attr):
# pylint: disable=R0913
def __init__(self, x, y, width, length, type_):
Attr.__init__(self)
self.x = x
self.y = y
self.width = width
self.length = length
self.type = type_
[docs] def collides(self, other):
return isinstance(other, self.__class__)
[docs]class Field(ValueAttr):
def __init__(self, fielditem):
ValueAttr.__init__(self, {
('field', 'fielditem'): fielditem
})
class _VSOSimpleAttr(Attr):
""" A _SimpleAttr is an attribute that is not composite, i.e. that only
has a single value, such as, e.g., Instrument('eit'). """
def __init__(self, value):
Attr.__init__(self)
self.value = value
def collides(self, other):
return isinstance(other, self.__class__)
def __repr__(self):
return "<%s(%r)>" % (self.__class__.__name__, self.value)
[docs]class Provider(_VSOSimpleAttr):
pass
[docs]class Source(_VSOSimpleAttr):
pass
[docs]class Instrument(_VSOSimpleAttr):
pass
[docs]class Physobs(_VSOSimpleAttr):
pass
[docs]class Pixels(_VSOSimpleAttr):
pass
[docs]class Level(_VSOSimpleAttr):
pass
[docs]class Resolution(_VSOSimpleAttr):
pass
[docs]class Detector(_VSOSimpleAttr):
pass
[docs]class Filter(_VSOSimpleAttr):
pass
[docs]class Sample(_VSOSimpleAttr):
pass
[docs]class Quicklook(_VSOSimpleAttr):
pass
[docs]class PScale(_VSOSimpleAttr):
pass
# The walker specifies how the Attr-tree is converted to a query the
# server can handle.
walker = AttrWalker()
# The _create functions make a new VSO query from the attribute tree,
# the _apply functions take an existing query-block and update it according
# to the attribute tree passed in as root. Different attributes require
# different functions for conversion into query blocks.
@walker.add_creator(ValueAttr, AttrAnd)
# pylint: disable=E0102,C0103,W0613
def _create(wlk, root, api):
""" Implementation detail. """
value = api.factory.create('QueryRequestBlock')
wlk.apply(root, api, value)
return [value]
@walker.add_applier(ValueAttr)
# pylint: disable=E0102,C0103,W0613
def _apply(wlk, root, api, queryblock):
""" Implementation detail. """
for k, v in root.attrs.iteritems():
lst = k[-1]
rest = k[:-1]
block = queryblock
for elem in rest:
block = block[elem]
block[lst] = v
@walker.add_applier(AttrAnd)
# pylint: disable=E0102,C0103,W0613
def _apply(wlk, root, api, queryblock):
""" Implementation detail. """
for attr in root.attrs:
wlk.apply(attr, api, queryblock)
@walker.add_creator(AttrOr)
# pylint: disable=E0102,C0103,W0613
def _create(wlk, root, api):
""" Implementation detail. """
blocks = []
for attr in root.attrs:
blocks.extend(wlk.create(attr, api))
return blocks
@walker.add_creator(DummyAttr)
# pylint: disable=E0102,C0103,W0613
def _create(wlk, root, api):
""" Implementation detail. """
return api.factory.create('QueryRequestBlock')
@walker.add_applier(DummyAttr)
# pylint: disable=E0102,C0103,W0613
def _apply(wlk, root, api, queryblock):
""" Implementation detail. """
pass
# Converters take a type unknown to the walker and convert it into one
# known to it. All of those convert types into ValueAttrs, which are
# handled above by just assigning according to the keys and values of the
# attrs member.
walker.add_converter(Extent)(
lambda x: ValueAttr(
dict((('extent', k), v) for k, v in vars(x).iteritems())
)
)
walker.add_converter(Time)(
lambda x: ValueAttr({
('time', 'start'): x.start.strftime(TIMEFORMAT),
('time', 'end'): x.end.strftime(TIMEFORMAT) ,
('time', 'near'): (
x.near.strftime(TIMEFORMAT) if x.near is not None else None),
})
)
walker.add_converter(_VSOSimpleAttr)(
lambda x: ValueAttr({(x.__class__.__name__.lower(), ): x.value})
)
walker.add_converter(Wave)(
lambda x: ValueAttr({
('wave', 'wavemin'): x.min,
('wave', 'wavemax'): x.max,
('wave', 'waveunit'): x.unit,
})
)
# The idea of using a multi-method here - that means a method which dispatches
# by type but is not attached to said class - is that the attribute classes are
# designed to be used not only in the context of VSO but also elsewhere (which
# AttrAnd and AttrOr obviously are - in the HEK module). If we defined the
# filter method as a member of the attribute classes, we could only filter
# one type of data (that is, VSO data).
filter_results = MultiMethod(lambda *a, **kw: (a[0], ))
# If we filter with ANDed together attributes, the only items are the ones
# that match all of them - this is implementing by ANDing the pool of items
# with the matched items - only the ones that match everything are there
# after this.
@filter_results.add_dec(AttrAnd)
def _(attr, results):
res = set(results)
for elem in attr.attrs:
res &= filter_results(elem, res)
return res
# If we filter with ORed attributes, the only attributes that should be
# removed are the ones that match none of them. That's why we build up the
# resulting set by ORing all the matching items.
@filter_results.add_dec(AttrOr)
def _(attr, results):
res = set()
for elem in attr.attrs:
res |= filter_results(elem, results)
return res
# Filter out items by comparing attributes.
@filter_results.add_dec(_VSOSimpleAttr)
def _(attr, results):
attrname = attr.__class__.__name__.lower()
return set(
item for item in results
# Some servers seem to obmit some fields. No way to filter there.
if not hasattr(item, attrname) or
getattr(item, attrname).lower() == attr.value.lower()
)
# The dummy attribute does not filter at all.
@filter_results.add_dec(DummyAttr, Field)
def _(attr, results):
return set(results)
@filter_results.add_dec(Wave)
def _(attr, results):
return set(
it for it in results
if
it.wave.wavemax is not None
and
attr.min <= to_angstrom(float(it.wave.wavemax), it.wave.waveunit)
and
it.wave.wavemin is not None
and
attr.max >= to_angstrom(float(it.wave.wavemin), it.wave.waveunit)
)
@filter_results.add_dec(Time)
def _(attr, results):
return set(
it for it in results
if
it.time.end is not None
and
attr.min <= datetime.strptime(it.time.end, TIMEFORMAT)
and
it.time.start is not None
and
attr.max >= datetime.strptime(it.time.start, TIMEFORMAT)
)
@filter_results.add_dec(Extent)
def _(attr, results):
return set(
it for it in results
if
it.extent.type is not None
and
it.extent.type.lower() == attr.type.lower()
)