Squashed commit of the following:

commit bdae391f4cc0b16eb2914b3b077e8c32027e40e9
Author: Tom Keffer <tkeffer@gmail.com>
Date:   Sun Oct 18 05:44:29 2020 -0700

    Ignore attributes 'mro', 'im_func', 'func_code', '__func__', '__code__', '__init__', '__self__'

commit 81d901f2227682dac19ab739c6c0b6c6c7d3221f
Author: Tom Keffer <tkeffer@gmail.com>
Date:   Thu Sep 24 17:47:15 2020 -0700

    Pass on the option_dict.
    Get rid of no longer used aggregation_interval.
    Generalize __call__()

commit 9020b6a14da717aae5f791bdb6d9a6f5a132695e
Author: Tom Keffer <tkeffer@gmail.com>
Date:   Tue Sep 22 03:52:24 2020 -0700

    __call__ returns new instance of AggTypeBinder, rather than modify self

commit 8f4bfb9ac3d1b0cb7b07650fa87661351095e21e
Author: Tom Keffer <tkeffer@gmail.com>
Date:   Fri Sep 18 15:25:42 2020 -0700

    Introduced class AggTypeBinder.
    In anticipation of allowing tags that return JSON structures.
This commit is contained in:
Tom Keffer
2020-10-18 05:52:57 -07:00
parent e99dd45818
commit bef21515d9
3 changed files with 132 additions and 59 deletions

View File

@@ -617,17 +617,42 @@ class AssureUnicode(Cheetah.Filters.Filter):
"""Assures that whatever a search list extension might return, it will be converted into
Unicode. """
def filter(self, val, **dummy_kw):
def filter(self, val, **kwargs):
"""Convert the expression 'val' to unicode."""
# There is a 2x4 matrix of possibilities:
# input PY2 PY3
# _____ ________ _______
# bytes decode() decode()
# str decode() -done-
# unicode -done- N/A
# object str().decode() str()
if val is None:
filtered = u''
return u''
# Is it already unicode? This takes care of cells 4 and 5.
if isinstance(val, six.text_type):
filtered = val
# This conditional covers cells 1,2, and 3. That is, val is a byte string
elif isinstance(val, six.binary_type):
filtered = val.decode('utf-8')
# That leaves cells 7 and 8, that is val is an object, such as a ValueHelper
else:
# Must be an object. Convert to string
try:
# Assume val is some kind of string. Try coercing it directly into unicode
filtered = six.ensure_text(val, encoding='utf-8')
except TypeError:
# It's not Unicode nor a byte-string. Must be a primitive or a ValueHelper.
# Under Python 2, six.text_type is equivalent to calling unicode(val).
# Under Python 3, it is equivalent to calling str(val).
# So, the results always end up as Unicode.
filtered = six.text_type(val)
# For late tag bindings, the following forces the invocation of __str__(),
# which can force an XTypes query. For a tag such as $day.foobar.min, where
# 'foobar' is an unknown type, this will cause an attribute error. Be prepared
# to catch it.
filtered = str(val)
except AttributeError as e:
# Offer a debug message.
log.debug("Unrecognized: %s", kwargs.get('rawExpr', e))
# Return the raw expression, if available. Otherwise, the exception message
# concatenated with a question mark.
filtered = kwargs.get('rawExpr', str(e) + '?')
if six.PY2:
# For Python 2, we have to take an additional step of converting from
# type 'str', which is a byte string, to a unicode string
filtered = filtered.decode('utf-8')
return filtered

View File

@@ -14,6 +14,9 @@ from weeutil.weeutil import to_int
from weewx.units import ValueTuple
# Attributes we are to ignore. Cheetah calls these housekeeping functions.
IGNORE_ATTR = ['mro', 'im_func', 'func_code', '__func__', '__code__', '__init__', '__self__']
# ===============================================================================
# Class TimeBinder
# ===============================================================================
@@ -30,7 +33,7 @@ class TimeBinder(object):
formatter=weewx.units.Formatter(), converter=weewx.units.Converter(),
**option_dict):
"""Initialize an instance of DatabaseBinder.
db_lookup: A function with call signature db_lookup(data_binding), which returns a database
manager and where data_binding is an optional binding name. If not given, then a default
binding will be used.
@@ -257,9 +260,8 @@ class TimespanBinder(object):
returns: An instance of class ObservationBinder."""
# This is to get around bugs in the Python version of Cheetah's namemapper:
if obs_type in ['__call__', 'has_key']:
raise AttributeError
if obs_type in IGNORE_ATTR:
raise AttributeError(obs_type)
# Return an ObservationBinder: if an attribute is
# requested from it, an aggregation value will be returned.
@@ -273,11 +275,11 @@ class TimespanBinder(object):
# ===============================================================================
class ObservationBinder(object):
"""This is the final class in the chain of helper classes. It binds the
"""This is the next class in the chain of helper classes. It binds the
database, a time period, and an observation type all together.
When an aggregation type (eg, 'max') is given as an attribute to it, it runs the
query against the database, assembles the result, and returns it as a ValueHelper.
When an aggregation type (eg, 'max') is given as an attribute to it, it binds it to
an instance of AggTypeBinder and returns it.
"""
def __init__(self, obs_type, timespan, db_lookup, data_binding, context,
@@ -316,41 +318,28 @@ class ObservationBinder(object):
self.converter = converter
self.option_dict = option_dict
def max_ge(self, val):
return self._do_query('max_ge', val=val)
def __getattr__(self, aggregation_type):
"""Use the specified aggregation type
def max_le(self, val):
return self._do_query('max_le', val=val)
def min_ge(self, val):
return self._do_query('min_ge', val=val)
def min_le(self, val):
return self._do_query('min_le', val=val)
def sum_ge(self, val):
return self._do_query('sum_ge', val=val)
def sum_le(self, val):
return self._do_query('sum_le', val=val)
def __getattr__(self, aggregate_type):
"""Return statistical summary using a given aggregate type.
aggregate_type: The type of aggregation over which the summary is to be done. This is
aggregation_type: The type of aggregation over which the summary is to be done. This is
normally something like 'sum', 'min', 'mintime', 'count', etc. However, there are two
special aggregation types that can be used to determine the existence of data:
'exists': Return True if the observation type exists in the database.
'has_data': Return True if the type exists and there is a non-zero number of entries over
the aggregation period.
returns: A ValueHelper containing the aggregation data.
returns: An instance of AggTypeBinder, which is bound to the aggregation type.
"""
# This is to get around bugs in the Python version of Cheetah's namemapper:
if aggregate_type in ['__call__', 'has_key']:
raise AttributeError
return self._do_query(aggregate_type)
if aggregation_type in IGNORE_ATTR:
raise AttributeError(aggregation_type)
return AggTypeBinder(aggregation_type=aggregation_type,
obs_type=self.obs_type,
timespan=self.timespan,
db_lookup=self.db_lookup,
data_binding=self.data_binding,
context=self.context,
formatter=self.formatter, converter=self.converter,
**self.option_dict)
@property
def exists(self):
@@ -360,19 +349,68 @@ class ObservationBinder(object):
def has_data(self):
return self.db_lookup(self.data_binding).has_data(self.obs_type, self.timespan)
def _do_query(self, aggregate_type, val=None):
# ===============================================================================
# Class AggTypeBinder
# ===============================================================================
class AggTypeBinder(object):
"""This is the final class in the chain of helper classes. It binds everything needed
for a query."""
def __init__(self, aggregation_type, obs_type, timespan, db_lookup, data_binding, context,
formatter=weewx.units.Formatter(), converter=weewx.units.Converter(),
**option_dict):
self.aggregation_type = aggregation_type
self.obs_type = obs_type
self.timespan = timespan
self.db_lookup = db_lookup
self.data_binding = data_binding
self.context = context
self.formatter = formatter
self.converter = converter
self.option_dict = option_dict
def __call__(self, *args, **kwargs):
"""Offer a call option for expressions such as $month.outTemp.max_ge((90.0, 'degree_F')).
In this example, self.aggregation_type would be 'max_ge', and val would be the tuple
(90.0, 'degree_F').
"""
if len(args):
self.option_dict['val'] = args[0]
self.option_dict.update(kwargs)
return self
def __str__(self):
"""Need a string representation. Force the query, return as string."""
vh = self._do_query()
return str(vh)
def _do_query(self):
"""Run a query against the databases, using the given aggregation type."""
db_manager = self.db_lookup(self.data_binding)
try:
# If we cannot perform the aggregation, we will get an UnknownType or
# UnknownAggregation error. Be prepared to catch it.
result = weewx.xtypes.get_aggregate(self.obs_type, self.timespan, aggregate_type,
db_manager, val=val, **self.option_dict)
result = weewx.xtypes.get_aggregate(self.obs_type, self.timespan,
self.aggregation_type,
db_manager, **self.option_dict)
except (weewx.UnknownType, weewx.UnknownAggregation):
# Signal Cheetah that we don't know how to do this by raiing an AttributeError.
# Signal Cheetah that we don't know how to do this by raising an AttributeError.
raise AttributeError(self.obs_type)
return weewx.units.ValueHelper(result, self.context, self.formatter, self.converter)
def __getattr__(self, attr):
# The following is an optimization, so we avoid doing an SQL query for these kinds of
# housekeeping attribute queries done by Cheetah's NameMapper
if attr in IGNORE_ATTR:
raise AttributeError(attr)
# Do the query, getting a ValueHelper back
vh = self._do_query()
# Now seek the desired attribute of the ValueHelper and return
return getattr(vh, attr)
# ===============================================================================
# Class RecordBinder
@@ -410,7 +448,7 @@ class RecordBinder(object):
class CurrentObj(object):
"""Helper class for the "Current" record. Hits the database lazily.
This class allows tags such as:
$current.barometer
"""
@@ -427,9 +465,9 @@ class CurrentObj(object):
def __getattr__(self, obs_type):
"""Return the given observation type."""
# This is to get around bugs in the Python version of Cheetah's namemapper:
if obs_type in ['__call__', 'has_key']:
raise AttributeError
if obs_type in IGNORE_ATTR:
raise AttributeError(obs_type)
# TODO: Refactor the following to be a separate function.
@@ -474,8 +512,8 @@ class CurrentObj(object):
# ===============================================================================
class TrendObj(object):
"""Helper class that calculates trends.
"""Helper class that calculates trends.
This class allows tags such as:
$trend.barometer
"""
@@ -483,9 +521,9 @@ class TrendObj(object):
def __init__(self, time_delta, time_grace, db_lookup, data_binding,
nowtime, formatter, converter, **option_dict): # @UnusedVariable
"""Initialize a Trend object
time_delta: The time difference over which the trend is to be calculated
time_grace: A time within this amount is accepted.
"""
self.time_delta_val = time_delta
@@ -506,9 +544,8 @@ class TrendObj(object):
def __getattr__(self, obs_type):
"""Return the trend for the given observation type."""
# This is to get around bugs in the Python version of Cheetah's namemapper:
if obs_type in ['__call__', 'has_key']:
raise AttributeError
if obs_type in IGNORE_ATTR:
raise AttributeError(obs_type)
db_manager = self.db_lookup(self.data_binding)
# Get the current record, and one "time_delta" ago:

View File

@@ -22,6 +22,11 @@ log = logging.getLogger(__name__)
weeutil.logger.setup('test_cheetah', {})
class RaiseException(object):
def __str__(self):
raise AttributeError("Fine mess you got me in!")
class TestFilter(unittest.TestCase):
"Test the function filter() in AssureUnicode"
def test_none(self):
@@ -60,6 +65,12 @@ class TestFilter(unittest.TestCase):
filtered_value = au.filter(val_vh)
self.assertEqual(filtered_value, u"68.0°F")
def test_RaiseException(self):
r = RaiseException()
au = weewx.cheetahgenerator.AssureUnicode()
filtered_value = au.filter(r)
self.assertEqual(filtered_value, u'Fine mess you got me in!?')
if __name__ == '__main__':
unittest.main()